// Package suite assembles a conventions suite in memory: its manifest, the // vocabulary of its language, and the documents the language employs. // // What is subject to checking is everything the language employs: the // convention files and the document the suite governs itself by. The list of // those files comes from the manifest, from the section of live prefixes where // every prefix carries a path. A file the language merely quotes — the // description of the language itself — is not listed in the suite and gets no // checks. package suite import ( "errors" "fmt" "io/fs" "os" "path/filepath" "sort" "strings" "git.vakhrushev.me/av/convy/internal/doc" "git.vakhrushev.me/av/convy/internal/lang" "git.vakhrushev.me/av/convy/internal/manifest" ) // Suite is a loaded suite. type Suite struct { Root string Manifest *manifest.Manifest Vocab lang.Vocabulary // Docs holds the documents of the suite in the order of live prefixes. Docs []*doc.Document // ByPrefix maps a manifest prefix to its document. ByPrefix map[string]*doc.Document // Missing lists the prefixes whose file the manifest declares while the // file system holds none. Missing map[string]string // Unregistered lists files found in the suite carrying front matter that // the manifest does not declare. Such a file is neither checked nor // assembled: for the suite it does not exist, however its author sees it. Unregistered []string // Broken lists files whose front matter could not be parsed. Broken []error } // Load reads a suite from directory root. func Load(root string) (*Suite, error) { m, err := manifest.Load(root) if err != nil { return nil, err } vocab, err := lang.Lookup(m.Language.Version, m.Language.Lang) if err != nil { return nil, fmt.Errorf("%s: %w", m.Path, err) } s := &Suite{ Root: root, Manifest: m, Vocab: vocab, ByPrefix: make(map[string]*doc.Document), Missing: make(map[string]string), } for _, prefix := range m.LivePrefixes() { rel := filepath.ToSlash(m.Prefixes.Live[prefix]) name := filepath.Join(root, filepath.FromSlash(rel)) d, err := doc.Load(rel, name) if err != nil { if errors.Is(err, fs.ErrNotExist) { s.Missing[prefix] = rel continue } s.Broken = append(s.Broken, err) continue } d.Blocks(vocab) s.Docs = append(s.Docs, d) s.ByPrefix[prefix] = d } if err := s.findUnregistered(); err != nil { return nil, err } return s, nil } // findUnregistered walks the suite looking for files written in the conventions // language yet absent from the manifest. The marker is the prefix key in the // front matter rather than the location of the file: the suite rearranges its // taxonomy, while the front matter asserts. func (s *Suite) findUnregistered() error { declared := make(map[string]bool) for _, path := range s.Manifest.Prefixes.Live { declared[filepath.ToSlash(path)] = true } for _, path := range s.Manifest.Prefixes.Retired { declared[filepath.ToSlash(path)] = true } err := filepath.WalkDir(s.Root, func(name string, entry fs.DirEntry, err error) error { if err != nil { return err } if entry.IsDir() { if strings.HasPrefix(entry.Name(), ".") && name != s.Root { return fs.SkipDir } return nil } if filepath.Ext(entry.Name()) != ".md" { return nil } rel, err := filepath.Rel(s.Root, name) if err != nil { return err } rel = filepath.ToSlash(rel) if declared[rel] { return nil } d, err := doc.Load(rel, name) if err != nil { // The file carries no front matter, or it is broken — for the // suite this is not a document of the language but plain // markdown lying nearby. return nil } if d.Front.Prefix != "" { s.Unregistered = append(s.Unregistered, rel) } return nil }) if err != nil { return fmt.Errorf("walking the suite: %w", err) } sort.Strings(s.Unregistered) return nil } // Conventions picks out the convention documents — the ones that carry a topic // and therefore travel to a consumer. A document without a topic, the one the // suite governs itself by, gets no spread checks. func (s *Suite) Conventions() []*doc.Document { var out []*doc.Document for _, d := range s.Docs { if d.Front.Topic != "" { out = append(out, d) } } return out } // Layers lists the layers of a topic — the documents that declared that name in // their front matter. func (s *Suite) Layers(topic string) []*doc.Document { var out []*doc.Document for _, d := range s.Docs { if d.Front.Topic == topic { out = append(out, d) } } return out } // Prefix returns the prefix the manifest declares for a document. func (s *Suite) Prefix(d *doc.Document) string { prefix, _ := s.Manifest.PrefixOf(d.Path) return prefix } // Exists reports whether the suite holds a file at a path from its root. func (s *Suite) Exists(rel string) bool { _, err := os.Stat(filepath.Join(s.Root, filepath.FromSlash(rel))) return err == nil }