Files
convy/internal/manifest/manifest.go
T
av b2d07ae55d комментарии и сообщения переведены на английский
- комментарии, тексты ошибок, вывод CLI и сообщения тестов теперь на английском
- по-русски остались только литералы словаря ru и содержимое фикстур: это
  данные под проверкой, а не текст инструмента
- согласование числительных в итоге упростилось до английского plural
2026-07-27 10:04:27 +03:00

182 lines
5.4 KiB
Go

// Package manifest reads suite.toml, the manifest of a conventions suite.
//
// The manifest declares three things: the language the suite's rules are
// written in, its live and retired topics, and its live and retired rule
// prefixes together with the paths of their files. The tool knows no topic and
// no prefix in advance — that whole list arrives from here.
package manifest
import (
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"github.com/BurntSushi/toml"
)
// Name is the name of a suite manifest. Which of the two manifests lies next
// to you tells you where you are: suite.toml means a suite, .conventions.toml
// means a project.
const Name = "suite.toml"
// DefaultLanguageCode is the suite's natural language when the manifest says
// nothing about it. The key is optional on purpose: the vocabulary lives in the
// binary, and making every suite declare what is already implied buys nothing.
const DefaultLanguageCode = "ru"
// Language is the [language] section: the version of the conventions language
// and the two documents about it. The full description stays with the author of
// the suite, the short one travels into the copy.
type Language struct {
Version int `toml:"version"`
Lang string `toml:"lang"`
Description string `toml:"description"`
Reading string `toml:"reading"`
}
// Section is a part of the manifest split into a live and a retired half.
// Retired entries are kept rather than deleted: a topic name and a rule prefix
// live on in foreign repositories, and neither may ever be reused.
type Section struct {
Live map[string]string `toml:"live"`
Retired map[string]string `toml:"retired"`
}
// Manifest is a parsed suite.toml.
type Manifest struct {
Language Language `toml:"language"`
Topics Section `toml:"topics"`
Prefixes Section `toml:"prefixes"`
// Path is where the manifest was read from.
Path string `toml:"-"`
// Undecoded lists keys the tool does not know. A typo in the manifest
// would otherwise pass in silence, and it costs a subscription or a
// whole file.
Undecoded []string `toml:"-"`
}
// Load reads the suite manifest from directory root.
func Load(root string) (*Manifest, error) {
path := filepath.Join(root, Name)
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading the suite manifest: %w", err)
}
var m Manifest
meta, err := toml.Decode(string(data), &m)
if err != nil {
return nil, fmt.Errorf("parsing %s: %w", path, err)
}
m.Path = path
for _, key := range meta.Undecoded() {
m.Undecoded = append(m.Undecoded, key.String())
}
sort.Strings(m.Undecoded)
if m.Language.Lang == "" {
m.Language.Lang = DefaultLanguageCode
}
return &m, nil
}
// Find walks up from start looking for a directory that holds a suite
// manifest, so that `convy suite check` works from any subdirectory of a suite.
func Find(start string) (string, error) {
dir, err := filepath.Abs(start)
if err != nil {
return "", err
}
for {
if _, err := os.Stat(filepath.Join(dir, Name)); err == nil {
return dir, nil
}
parent := filepath.Dir(dir)
if parent == dir {
return "", ErrNotFound
}
dir = parent
}
}
// ErrNotFound means there is no suite manifest here or above.
var ErrNotFound = errors.New("suite manifest not found")
// LivePrefixes lists the live prefixes in an order stable between runs: the
// output of a check must not depend on map iteration.
func (m *Manifest) LivePrefixes() []string {
return sortedKeys(m.Prefixes.Live)
}
// LiveTopics lists the live topics in a stable order.
func (m *Manifest) LiveTopics() []string {
return sortedKeys(m.Topics.Live)
}
// PrefixOf returns the prefix declared for a file, if there is one. Paths are
// compared in slash form, the way the manifest writes them.
func (m *Manifest) PrefixOf(path string) (string, bool) {
want := filepath.ToSlash(path)
for prefix, declared := range m.Prefixes.Live {
if filepath.ToSlash(declared) == want {
return prefix, true
}
}
return "", false
}
// PathOf returns the path declared for a live prefix.
func (m *Manifest) PathOf(prefix string) (string, bool) {
path, ok := m.Prefixes.Live[prefix]
return path, ok
}
// TopicLive reports whether the topic is declared among the live ones.
func (m *Manifest) TopicLive(topic string) bool {
_, ok := m.Topics.Live[topic]
return ok
}
// TopicRetired reports whether the topic is listed among the retired ones.
func (m *Manifest) TopicRetired(topic string) bool {
_, ok := m.Topics.Retired[topic]
return ok
}
// PrefixRetired reports whether the prefix is listed among the retired ones.
func (m *Manifest) PrefixRetired(prefix string) bool {
_, ok := m.Prefixes.Retired[prefix]
return ok
}
// ValidPrefix checks the shape of a prefix: four uppercase Latin letters. The
// letter X in first position is reserved for consuming repositories, and the
// suite never takes it.
func ValidPrefix(prefix string) error {
if len(prefix) != 4 {
return fmt.Errorf("prefix %q is not four letters", prefix)
}
for _, r := range prefix {
if r < 'A' || r > 'Z' {
return fmt.Errorf("prefix %q holds a character that is not an uppercase Latin letter", prefix)
}
}
if strings.HasPrefix(prefix, "X") {
return fmt.Errorf("prefix %q starts with X, a letter reserved for the local rules of consumers", prefix)
}
return nil
}
func sortedKeys(m map[string]string) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}