- убраны комментарии из suite.toml и .conventions.toml: файл, который машина переписывает, комментарий через круг не проносит; объяснения ушли в README рядом, который suite init теперь заводит - удалена текстовая правка манифеста целиком — 520 строк ручного лексера TOML вместе со всем классом ошибок порчи данных - запись идёт из структур энкодером; ключ, которого инструмент не знает, запись останавливает, а не теряется молча - convy sync сверяет манифест и подводит под него раскладку файлов: чего не хватает — собирает, что осиротело — удаляет, копию с локальной частью не трогает никогда
141 lines
4.6 KiB
Go
141 lines
4.6 KiB
Go
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,omitempty"`
|
|
Stack []string `toml:"stack,omitempty"`
|
|
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
|
|
}
|
|
|
|
// Save writes the project manifest back to where it was read from.
|
|
func (p *Project) Save() error {
|
|
return save(p.Path, p, p.Undecoded)
|
|
}
|
|
|
|
// Subscribe adds a topic to a component, keeping the list sorted so that the
|
|
// file does not churn on the order things were added in.
|
|
func (p *Project) Subscribe(name, topic string) {
|
|
c := p.Components[name]
|
|
c.Topics = append(c.Topics, topic)
|
|
sort.Strings(c.Topics)
|
|
p.Components[name] = c
|
|
}
|
|
|
|
// Unsubscribe drops a topic from a component. Nothing is kept behind: a
|
|
// subscription is a choice of the project, not a name anyone else may reuse.
|
|
func (p *Project) Unsubscribe(name, topic string) {
|
|
c := p.Components[name]
|
|
c.Topics = slices.DeleteFunc(c.Topics, func(t string) bool { return t == topic })
|
|
p.Components[name] = c
|
|
}
|
|
|
|
// 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, ", ")
|
|
}
|