- suite rule дописывает правило: номер берётся следующим за наибольшим, блоки раскладываются в порядке норма, ПОЧЕМУ, ПРИМЕРЫ, --after ставит правило рядом с тем, которое оно уточняет - ступень называется категорией (requirement, prohibition, ...), а не словом языка, поэтому вызывающему не нужно знать, на каком языке записан набор - suite list показывает темы со слоями, а с осью — что возьмёт компонент; отбор слоёв вынесен в suite.Assemble, откуда его возьмут проектные команды - слои темы теперь всегда возвращаются базовым вперёд
225 lines
7.5 KiB
Go
225 lines
7.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.vakhrushev.me/av/convy/internal/doc"
|
|
"git.vakhrushev.me/av/convy/internal/lang"
|
|
"git.vakhrushev.me/av/convy/internal/suite"
|
|
)
|
|
|
|
// Adding a rule is where the tool stops merely judging the form and starts
|
|
// holding it. Everything the check verifies afterwards is known here already:
|
|
// the next free number, the prefix of the file, the words of the modality and
|
|
// the marks, the order of the blocks. Leaving the author to reproduce all of
|
|
// that by hand and then reporting what they got wrong is the worse half of the
|
|
// deal.
|
|
|
|
func runSuiteRule(env Env, args []string) ExitCode {
|
|
fs := flag.NewFlagSet("convy suite rule", flag.ContinueOnError)
|
|
fs.SetOutput(env.Err)
|
|
root := fs.String("root", "", "root of the suite; by default it is looked up upwards")
|
|
prefix := fs.String("prefix", "", "prefix of the convention the rule joins")
|
|
title := fs.String("title", "", "heading of the rule, what it is about")
|
|
modality := fs.String("modality", "", "step of the scale: "+strings.Join(lang.LevelNames(), ", "))
|
|
norm := fs.String("norm", "", "the norm itself, one statement")
|
|
why := fs.String("why", "", "what breaks if it is done otherwise")
|
|
examples := fs.String("examples", "", "illustration of the norm; optional")
|
|
after := fs.String("after", "", "identifier of the rule to place this one after; the last by default")
|
|
if err := fs.Parse(args); err != nil {
|
|
return Usage
|
|
}
|
|
|
|
dir, code := suiteRoot(env, *root)
|
|
if code != OK {
|
|
return code
|
|
}
|
|
s, err := suite.Load(dir)
|
|
if err != nil {
|
|
fmt.Fprintln(env.Err, err)
|
|
return Usage
|
|
}
|
|
|
|
given := map[string]string{
|
|
"prefix": *prefix, "title": *title, "modality": *modality,
|
|
"norm": *norm, "why": *why, "examples": *examples, "after": *after,
|
|
}
|
|
if len(args) == 0 {
|
|
if !env.Interactive {
|
|
fmt.Fprintln(env.Err, "convy suite rule without arguments asks questions, and there is no terminal to ask on; pass --prefix, --title, --modality, --norm and --why")
|
|
return Usage
|
|
}
|
|
if err := askAll(env, ruleFields(s), given); err != nil {
|
|
return Usage
|
|
}
|
|
} else if err := resolve(ruleFields(s), given); err != nil {
|
|
fmt.Fprintln(env.Err, err)
|
|
return Usage
|
|
}
|
|
|
|
return writeRule(env, s, given)
|
|
}
|
|
|
|
func ruleFields(s *suite.Suite) []Field {
|
|
return []Field{{
|
|
Flag: "prefix",
|
|
Ask: "Prefix of the convention",
|
|
Hint: "Which file the rule joins. The number is taken from that file: the next one after its highest, never a number that was used before.",
|
|
Check: func(v string) error {
|
|
if _, ok := s.ByPrefix[v]; !ok {
|
|
return fmt.Errorf("the suite declares no live prefix %s", v)
|
|
}
|
|
return nil
|
|
},
|
|
}, {
|
|
Flag: "title",
|
|
Ask: "Heading of the rule",
|
|
Hint: "What the rule is about, in one line. It is read on its own in a list, so it says the substance rather than the topic.",
|
|
}, {
|
|
Flag: "modality",
|
|
Ask: "Step of the scale",
|
|
Hint: "How binding it is. The highest step asks two things at once: a named harm from breaking it, and a verdict two reviewers reach alike. Neither one — the rule belongs a step lower.",
|
|
Options: lang.LevelNames(),
|
|
Default: "recommendation",
|
|
Check: func(v string) error {
|
|
if _, ok := lang.LevelByName(v); ok {
|
|
return nil
|
|
}
|
|
if _, ok := s.Vocab.Modal(v); ok {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("one of: %s — or the word the suite uses for the step",
|
|
strings.Join(lang.LevelNames(), ", "))
|
|
},
|
|
}, {
|
|
Flag: "norm",
|
|
Ask: "The norm",
|
|
Hint: "One statement of what is required. If it does not fit in one, these are two rules: half a compound norm cannot be addressed on its own.",
|
|
}, {
|
|
Flag: "why",
|
|
Ask: "The rationale",
|
|
Hint: "What breaks if it is done otherwise — not the norm said again. A cause that cannot be put into words means this is a habit rather than a rule.",
|
|
}, {
|
|
Flag: "examples",
|
|
Ask: "Examples",
|
|
Hint: "Code showing the norm at work, usually bad against good. Worth it where showing is cheaper than saying; a rule about choosing a boundary has nothing to illustrate.",
|
|
Optional: true,
|
|
}, {
|
|
Flag: "after",
|
|
Ask: "Place after",
|
|
Hint: "Identifier of the rule this one follows. Order in a file goes by reading rather than by number, so a rule elaborating another belongs next to it. Empty puts it last.",
|
|
Optional: true,
|
|
Check: func(v string) error {
|
|
if !ruleIDRe.MatchString(v) {
|
|
return fmt.Errorf("a rule is named by its identifier, PREFIX-N")
|
|
}
|
|
return nil
|
|
},
|
|
}}
|
|
}
|
|
|
|
func writeRule(env Env, s *suite.Suite, given map[string]string) ExitCode {
|
|
prefix := given["prefix"]
|
|
target := s.ByPrefix[prefix]
|
|
|
|
level, ok := lang.LevelByName(given["modality"])
|
|
if !ok {
|
|
if l, isWord := s.Vocab.Modal(given["modality"]); isWord {
|
|
level = l
|
|
} else {
|
|
fmt.Fprintf(env.Err, "unknown step %q; the steps are: %s\n",
|
|
given["modality"], strings.Join(lang.LevelNames(), ", "))
|
|
return Usage
|
|
}
|
|
}
|
|
|
|
own := ownRules(target, prefix)
|
|
if len(own) == 0 {
|
|
fmt.Fprintf(env.Err, "%s holds no rule yet, and a number is taken from the highest one; write the first rule as ### %s-1 by hand\n", target.Path, prefix)
|
|
return Usage
|
|
}
|
|
num := 0
|
|
for _, r := range own {
|
|
num = max(num, r.Num)
|
|
}
|
|
num++
|
|
|
|
// A number is never reused, so a rule retired long ago still owns its own.
|
|
// Taking the next after the highest is the only choice that cannot collide
|
|
// with a reference living in a foreign repository.
|
|
place := own[len(own)-1]
|
|
if given["after"] != "" {
|
|
m := ruleIDRe.FindStringSubmatch(given["after"])
|
|
if m[1] != prefix {
|
|
fmt.Fprintf(env.Err, "--after names %s, which belongs to another file than %s\n", given["after"], prefix)
|
|
return Usage
|
|
}
|
|
found := false
|
|
for _, r := range own {
|
|
if strconv.Itoa(r.Num) == m[2] {
|
|
place, found = r, true
|
|
}
|
|
}
|
|
if !found {
|
|
fmt.Fprintf(env.Err, "%s holds no rule %s\n", target.Path, given["after"])
|
|
return Usage
|
|
}
|
|
}
|
|
|
|
name := filepath.Join(s.Root, filepath.FromSlash(target.Path))
|
|
body, err := os.ReadFile(name)
|
|
if err != nil {
|
|
fmt.Fprintln(env.Err, err)
|
|
return Failed
|
|
}
|
|
lines := strings.Split(string(body), "\n")
|
|
|
|
at := place.End
|
|
for at > place.Line && strings.TrimSpace(lines[at-1]) == "" {
|
|
at--
|
|
}
|
|
|
|
block := renderRule(s, prefix, num, level, given)
|
|
out := make([]string, 0, len(lines)+len(block))
|
|
out = append(out, lines[:at]...)
|
|
out = append(out, block...)
|
|
out = append(out, lines[at:]...)
|
|
if err := os.WriteFile(name, []byte(strings.Join(out, "\n")), 0o644); err != nil {
|
|
fmt.Fprintln(env.Err, err)
|
|
return Failed
|
|
}
|
|
|
|
fmt.Fprintf(env.Out, "\n%s-%d added to %s after %s\n", prefix, num, target.Path, place.ID())
|
|
return OK
|
|
}
|
|
|
|
// renderRule lays out the blocks in the order the language fixes: the norm,
|
|
// then the reason, then the illustration.
|
|
func renderRule(s *suite.Suite, prefix string, num int, level lang.Level, given map[string]string) []string {
|
|
out := []string{"", fmt.Sprintf("### %s-%d. %s", prefix, num, given["title"]), ""}
|
|
out = append(out, wrap(fmt.Sprintf("**%s.** %s", s.Vocab.Word(level), given["norm"]), 78)...)
|
|
out = append(out, "")
|
|
out = append(out, wrap(fmt.Sprintf("**%s.** %s", s.Vocab.MarkWord(lang.Rationale), given["why"]), 78)...)
|
|
if given["examples"] != "" {
|
|
out = append(out, "")
|
|
out = append(out, wrap(fmt.Sprintf("**%s.** %s", s.Vocab.MarkWord(lang.Examples), given["examples"]), 78)...)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ownRules picks the rules a file numbers itself, in the order they stand.
|
|
func ownRules(d *doc.Document, prefix string) []doc.Rule {
|
|
var out []doc.Rule
|
|
for _, r := range d.Rules {
|
|
if r.Prefix == prefix {
|
|
out = append(out, r)
|
|
}
|
|
}
|
|
return out
|
|
}
|