// Package manifest читает suite.toml — манифест набора конвенций. // // Манифест объявляет три вещи: язык, которым записаны правила набора, живые и // выбывшие темы, живые и выбывшие префиксы правил вместе с путями к файлам. // Инструмент не знает ни одной темы и ни одного префикса заранее — весь этот // список приходит отсюда. package manifest import ( "errors" "fmt" "os" "path/filepath" "sort" "strings" "github.com/BurntSushi/toml" ) // Name — имя манифеста набора. По тому, какой из двух манифестов лежит рядом, // определяется контекст: suite.toml — набор, .conventions.toml — проект. const Name = "suite.toml" // DefaultLanguageCode — естественный язык набора, когда манифест о нём молчит. // Ключ необязателен намеренно: словарь живёт в бинаре, и заставлять каждый // набор объявлять то, что и так подразумевается, незачем. const DefaultLanguageCode = "ru" // Language — секция [language]: версия языка конвенций и два документа о нём. // Полное описание остаётся у автора набора, короткое едет в копию. type Language struct { Version int `toml:"version"` Lang string `toml:"lang"` Description string `toml:"description"` Reading string `toml:"reading"` } // Section — раздел манифеста, разбитый на живую и выбывшую части. Выбывшее // хранится, а не удаляется: имя темы и префикс правила живут в чужих // репозиториях, и переиспользовать их нельзя никогда. type Section struct { Live map[string]string `toml:"live"` Retired map[string]string `toml:"retired"` } // Manifest — разобранный suite.toml. type Manifest struct { Language Language `toml:"language"` Topics Section `toml:"topics"` Prefixes Section `toml:"prefixes"` // Path — путь, по которому манифест прочитан. Path string `toml:"-"` // Undecoded — ключи, которых инструмент не знает. Опечатка в манифесте // иначе прошла бы молча, а стоит она подписки или целого файла. Undecoded []string `toml:"-"` } // Load читает манифест набора из директории root. func Load(root string) (*Manifest, error) { path := filepath.Join(root, Name) data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("чтение манифеста набора: %w", err) } var m Manifest meta, err := toml.Decode(string(data), &m) if err != nil { return nil, fmt.Errorf("разбор %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 поднимается от start вверх до корня, ища директорию с манифестом // набора. Так `convy suite check` работает из любой поддиректории набора. 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 означает, что рядом и выше нет манифеста набора. var ErrNotFound = errors.New("манифест набора не найден") // LivePrefixes перечисляет живые префиксы в порядке, устойчивом между // запусками: вывод проверки не должен зависеть от обхода карты. func (m *Manifest) LivePrefixes() []string { return sortedKeys(m.Prefixes.Live) } // LiveTopics перечисляет живые темы в устойчивом порядке. func (m *Manifest) LiveTopics() []string { return sortedKeys(m.Topics.Live) } // PrefixOf возвращает префикс, объявленный за файлом, если такой есть. // Путь сверяется в форме со слэшами — так он записан в манифесте. 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 возвращает путь, объявленный за живым префиксом. func (m *Manifest) PathOf(prefix string) (string, bool) { path, ok := m.Prefixes.Live[prefix] return path, ok } // TopicLive отвечает, объявлена ли тема среди живых. func (m *Manifest) TopicLive(topic string) bool { _, ok := m.Topics.Live[topic] return ok } // TopicRetired отвечает, значится ли тема среди выбывших. func (m *Manifest) TopicRetired(topic string) bool { _, ok := m.Topics.Retired[topic] return ok } // PrefixRetired отвечает, значится ли префикс среди выбывших. func (m *Manifest) PrefixRetired(prefix string) bool { _, ok := m.Prefixes.Retired[prefix] return ok } // ValidPrefix проверяет форму префикса: четыре заглавные латинские буквы. // Буква X в начале зарезервирована за репозиториями-потребителями, и набор // её не занимает никогда. func ValidPrefix(prefix string) error { if len(prefix) != 4 { return fmt.Errorf("префикс %q — не четыре буквы", prefix) } for _, r := range prefix { if r < 'A' || r > 'Z' { return fmt.Errorf("префикс %q содержит не заглавную латинскую букву", prefix) } } if strings.HasPrefix(prefix, "X") { return fmt.Errorf("префикс %q начинается на X — буква зарезервирована за локальными правилами потребителей", 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 }