- разбор документа по языку конвенций: шапка, области правил, блоки под метками - проверки формы правила, распространения, ссылок и самого suite.toml - словарь языка живёт в бинаре реестром «версия × естественный язык», темы и префиксы берутся только из манифеста
179 lines
6.8 KiB
Go
179 lines
6.8 KiB
Go
// 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
|
|
}
|