проектные команды и ссылки на источник
- заведён internal/source: уровни ссылаются друг на друга путём на диске или git-репозиторием, ревизия закрепляется хвостом #ref; клон делается заново и удаляется, кэша нет - добавлены init, add, pull, list, check в проекте — манифест .conventions.toml, сборка копий по разу на компонент, маркер локальной части, READING.md рядом - проверки формы развязаны с набором: принимают lang.Vocabulary, а язык копии узнаётся по строке о версии — манифеста рядом с ней нет
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
// ProjectName is the name of the manifest of a consuming repository. Which of
|
||||
// the two manifests lies next to you tells you where you are, so the project
|
||||
// one is never named like the suite one.
|
||||
const ProjectName = ".conventions.toml"
|
||||
|
||||
// Component is one addressee of assembly inside a project: a region where every
|
||||
// selected layer holds at once — one language, one set of tools, one kind of
|
||||
// application (META-36).
|
||||
//
|
||||
// Lang and Stack are lists because a component may well take two stack layers
|
||||
// at a time — sqlite and postgres in the schema topic hold together, being
|
||||
// different tables of one service. Two languages never do: a line of code is
|
||||
// written in one of them, which is what a component exists to separate.
|
||||
type Component struct {
|
||||
Dir string `toml:"dir"`
|
||||
Lang []string `toml:"lang"`
|
||||
Stack []string `toml:"stack"`
|
||||
Topics []string `toml:"topics"`
|
||||
}
|
||||
|
||||
// Project is a parsed .conventions.toml: where the copies are taken from and
|
||||
// which components take what.
|
||||
type Project struct {
|
||||
// Source is the reference to the suite. How the tool reaches it — a path
|
||||
// on disk, a git repository — is a matter of the reference itself.
|
||||
Source string `toml:"source"`
|
||||
Components map[string]Component `toml:"components"`
|
||||
|
||||
// Path is where the manifest was read from.
|
||||
Path string `toml:"-"`
|
||||
// Root is the directory the manifest lies in; every path in it is relative
|
||||
// to that directory.
|
||||
Root string `toml:"-"`
|
||||
// Undecoded lists keys the tool does not know: a typo in a component name
|
||||
// or in a key would otherwise cost a whole subscription in silence.
|
||||
Undecoded []string `toml:"-"`
|
||||
}
|
||||
|
||||
// LoadProject reads the project manifest from directory root.
|
||||
func LoadProject(root string) (*Project, error) {
|
||||
path := filepath.Join(root, ProjectName)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading the project manifest: %w", err)
|
||||
}
|
||||
|
||||
var p Project
|
||||
meta, err := toml.Decode(string(data), &p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing %s: %w", path, err)
|
||||
}
|
||||
p.Path = path
|
||||
p.Root = root
|
||||
for _, key := range meta.Undecoded() {
|
||||
p.Undecoded = append(p.Undecoded, key.String())
|
||||
}
|
||||
sort.Strings(p.Undecoded)
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// FindProject walks up from start looking for a project manifest, so that a
|
||||
// command works from any subdirectory of a repository.
|
||||
func FindProject(start string) (string, error) {
|
||||
return findUp(start, ProjectName)
|
||||
}
|
||||
|
||||
// Names lists the components in an order stable between runs.
|
||||
func (p *Project) Names() []string {
|
||||
names := make([]string, 0, len(p.Components))
|
||||
for name := range p.Components {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
// Only picks the component a command works on. With a name given it is that
|
||||
// one; without a name it is the single component of the project. A project of
|
||||
// several components does not get one guessed for it — it gets the list.
|
||||
func (p *Project) Only(name string) (string, Component, error) {
|
||||
if name != "" {
|
||||
c, ok := p.Components[name]
|
||||
if !ok {
|
||||
return "", Component{}, fmt.Errorf("the project declares no component %q; it declares: %s", name, joinNames(p.Names()))
|
||||
}
|
||||
return name, c, nil
|
||||
}
|
||||
switch len(p.Components) {
|
||||
case 0:
|
||||
return "", Component{}, fmt.Errorf("%s declares no component, and a copy is assembled for a component", p.Path)
|
||||
case 1:
|
||||
only := p.Names()[0]
|
||||
return only, p.Components[only], nil
|
||||
}
|
||||
return "", Component{}, fmt.Errorf("the project holds several components, and the command names none: pass --for with one of %s", joinNames(p.Names()))
|
||||
}
|
||||
|
||||
// Subscribed reports whether a component takes a topic.
|
||||
func (c Component) Subscribed(topic string) bool {
|
||||
return slices.Contains(c.Topics, topic)
|
||||
}
|
||||
|
||||
func joinNames(names []string) string {
|
||||
return strings.Join(names, ", ")
|
||||
}
|
||||
Reference in New Issue
Block a user