suite init и suite add: заведение набора и конвенции в двух режимах
- suite init создаёт директорию и манифест со скелетом таблиц; suite add пишет файл конвенции и вправляет запись в suite.toml, сохраняя комментарии - без аргументов команды спрашивают поля с подсказками, с флагами берут всё сразу и не спрашивают ничего; без терминала пустой вызов отказывает - строка о версии языка генерируется из словаря набора, поэтому созданный файл проходит suite check без правок - починено разрешение extends: короткая форма бралась по суффиксу и могла указать на сам файл; теперь неоднозначность либо избегается при записи, либо сообщается ошибкой
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.vakhrushev.me/av/convy/internal/lang"
|
||||
"git.vakhrushev.me/av/convy/internal/manifest"
|
||||
)
|
||||
|
||||
// manifestSkeleton is what a suite starts as. The tables stand empty but
|
||||
// present: `convy suite add` splices entries into them, and a table that is not
|
||||
// there is a table an edit cannot find.
|
||||
//
|
||||
// The two documents about the language are left commented out on purpose. The
|
||||
// suite is expected to carry them, but the tool cannot author them, and a
|
||||
// manifest pointing at a file that does not exist is a manifest that fails its
|
||||
// own check on the first run.
|
||||
const manifestSkeleton = `# The manifest of a conventions suite.
|
||||
#
|
||||
# Two manifests exist in the model, each named after what it describes:
|
||||
# suite.toml here, in the suite, describes the suite itself; .conventions.toml
|
||||
# in a project describes what that project subscribed to. Which of the two lies
|
||||
# next to you tells you where you are.
|
||||
|
||||
[language]
|
||||
version = %d
|
||||
lang = "%s"
|
||||
# description = "LANGUAGE.md" # the full account of the language, stays with the author
|
||||
# reading = "READING.md" # the short guide for a reader, travels into every copy
|
||||
|
||||
# ─── Topics ─────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# A topic is a set of rules about one focus of development, and the unit of
|
||||
# subscription. The value is the one line about what the topic is for; the
|
||||
# table of conventions in a consumer's README is built out of it.
|
||||
|
||||
[topics.live]
|
||||
|
||||
# Retired names land here together with a reason and a date, so that they can
|
||||
# never be handed to another topic: the name lives on in foreign repositories.
|
||||
|
||||
[topics.retired]
|
||||
|
||||
# ─── Rule prefixes ──────────────────────────────────────────────────────────
|
||||
#
|
||||
# A prefix is four uppercase Latin letters, unique across the suite, chosen for
|
||||
# a file rather than derived by a formula. The letter X is reserved for the
|
||||
# local rules of consumers and is never taken here. Paths are given from the
|
||||
# root of the repository.
|
||||
|
||||
[prefixes.live]
|
||||
|
||||
# Prefixes of deleted and split files land here, likewise never to be reissued.
|
||||
|
||||
[prefixes.retired]
|
||||
`
|
||||
|
||||
func runSuiteInit(env Env, args []string) ExitCode {
|
||||
fs := flag.NewFlagSet("convy suite init", flag.ContinueOnError)
|
||||
fs.SetOutput(env.Err)
|
||||
path := fs.String("path", "", "directory of the suite; it is created if absent")
|
||||
code := fs.String("lang", "", "code of the natural language the suite is written in")
|
||||
version := fs.Int("language-version", 1, "version of the conventions language")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return Usage
|
||||
}
|
||||
|
||||
fields := []Field{{
|
||||
Flag: "path",
|
||||
Ask: "Directory of the suite",
|
||||
Hint: "Where the suite will live. The directory is created if it is not there yet.",
|
||||
Default: ".",
|
||||
}, {
|
||||
Flag: "lang",
|
||||
Ask: "Natural language of the suite",
|
||||
Hint: "The language the rules are written in. It settles the key words: ДОЛЖЕН and ПОЧЕМУ for ru, MUST and WHY for en.",
|
||||
Default: manifest.DefaultLanguageCode,
|
||||
Check: func(v string) error {
|
||||
_, err := lang.Lookup(*version, v)
|
||||
return err
|
||||
},
|
||||
}}
|
||||
|
||||
given := map[string]string{"path": *path, "lang": *code}
|
||||
if len(args) == 0 {
|
||||
if !env.Interactive {
|
||||
fmt.Fprintln(env.Err, "convy suite init without arguments asks questions, and there is no terminal to ask on; pass --path and --lang")
|
||||
return Usage
|
||||
}
|
||||
if err := askAll(env, fields, given); err != nil {
|
||||
return Usage
|
||||
}
|
||||
} else if err := resolve(fields, given); err != nil {
|
||||
fmt.Fprintln(env.Err, err)
|
||||
return Usage
|
||||
}
|
||||
|
||||
root := given["path"]
|
||||
if _, err := os.Stat(filepath.Join(root, manifest.Name)); err == nil {
|
||||
fmt.Fprintf(env.Err, "%s already holds %s: this is a suite already\n", root, manifest.Name)
|
||||
return Usage
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(root, "conventions"), 0o755); err != nil {
|
||||
fmt.Fprintln(env.Err, err)
|
||||
return Usage
|
||||
}
|
||||
|
||||
body := fmt.Sprintf(manifestSkeleton, *version, given["lang"])
|
||||
name := filepath.Join(root, manifest.Name)
|
||||
if err := os.WriteFile(name, []byte(body), 0o644); err != nil {
|
||||
fmt.Fprintln(env.Err, err)
|
||||
return Usage
|
||||
}
|
||||
|
||||
fmt.Fprintf(env.Out, "\ncreated %s\ncreated %s\n", name, filepath.Join(root, "conventions"))
|
||||
fmt.Fprintf(env.Out, "\nthe suite speaks %s, conventions language version %d\n", given["lang"], *version)
|
||||
fmt.Fprint(env.Out, `
|
||||
next:
|
||||
write the two documents about the language and name them in [language]
|
||||
convy suite add add the first convention
|
||||
convy suite check verify the suite holds together
|
||||
`)
|
||||
return OK
|
||||
}
|
||||
|
||||
// askAll walks the fields in interactive mode, keeping answers already given on
|
||||
// the command line.
|
||||
func askAll(env Env, fields []Field, given map[string]string) error {
|
||||
d := newDialogue(env)
|
||||
for _, f := range fields {
|
||||
if given[f.Flag] != "" {
|
||||
continue
|
||||
}
|
||||
answer, err := d.ask(f)
|
||||
if err != nil {
|
||||
fmt.Fprintln(env.Err, "\ninterrupted, nothing was written")
|
||||
return err
|
||||
}
|
||||
given[f.Flag] = answer
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user