suite rule и suite list
- suite rule дописывает правило: номер берётся следующим за наибольшим, блоки раскладываются в порядке норма, ПОЧЕМУ, ПРИМЕРЫ, --after ставит правило рядом с тем, которое оно уточняет - ступень называется категорией (requirement, prohibition, ...), а не словом языка, поэтому вызывающему не нужно знать, на каком языке записан набор - suite list показывает темы со слоями, а с осью — что возьмёт компонент; отбор слоёв вынесен в suite.Assemble, откуда его возьмут проектные команды - слои темы теперь всегда возвращаются базовым вперёд
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user