Answer architecture round 1: stdin drops one newline, \n or \r\n, and reaches the library unchanged, inputError names the input's fault, two decisions and item 110 stop naming a CLI argument, the gate exemption moves to the floor entry, and item 156 asks about the JSON string

This commit is contained in:
2026-10-07 16:53:41 +02:00
committed by Lillem4n
parent b9c1a28a61
commit 83b4b0bdf0
8 changed files with 95 additions and 118 deletions
+30 -41
View File
@@ -9,7 +9,6 @@ package main
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
@@ -36,8 +35,8 @@ A reference names a category or a dotted path into one ({/sv_SE.person.last}), a
table's row by key or name ({/misc.territory[SE]}), and .. steps up from a row to
the row it links to ({/geo.SE.locality..municipality.name}).
One newline ending stdin is dropped, so echo and echo -n render alike; end it with
two to print one. Whitespace around a JSON template is dropped too.
One newline ending stdin, \n or \r\n, is dropped, so echo and echo -n render
alike; end stdin with two to print one.
With --format json, ndjson, csv or sql the template must name a record — a
template whose fields are its columns, or one reference alone, {/sv_SE.person} —
@@ -60,13 +59,13 @@ A short flag's value attaches or follows (-n3, -n 3); short flags bundle (-hn 3)
`
type invocation struct {
args []string
dirs []string
format string
formatSet bool
help bool
list bool
noShipped bool
args []string
repeat int
repeatSet bool
seed uint64
@@ -267,7 +266,6 @@ func (in invocation) checkFlags() error {
return nil
}
// pipeHint is how a template reaches fejkdata, for the errors of a run that has none.
const pipeHint = "echo -n '{/sv_SE.person}' | fejkdata"
// check rejects a flag combination or an argument that cannot run, before any data is
@@ -333,7 +331,7 @@ func textDraw(f *fejkdata.Generator, src input) (func() (string, error), error)
}
t, err := f.NewTemplate(src.template)
if err != nil {
return nil, templateError{err}
return nil, inputError{err}
}
return func() (string, error) { return t.Fake(), nil }, nil
}
@@ -345,7 +343,7 @@ func (in invocation) recordStream(f *fejkdata.Generator, src input) (func() (*fe
if src.path == "" {
t, err := f.NewRecordTemplate(src.template)
if err != nil {
return nil, "", templateError{err}
return nil, "", inputError{err}
}
record = func() (*fejkdata.Record, error) { return t.Fake(), nil }
}
@@ -393,58 +391,51 @@ func (in invocation) writeRecords(f *fejkdata.Generator, src input, w io.Writer)
// defaultTable names the INSERT target when --table is absent: the path's last
// segment, or "records" for a template that reads no path alone.
func defaultTable(arg string) string {
if arg == "" {
func defaultTable(path string) string {
if path == "" {
return "records"
}
var names strings.Builder // the path with its [selectors] cut out
for depth, i := 0, 0; i < len(arg); i++ {
for depth, i := 0, 0; i < len(path); i++ {
switch {
case arg[i] == '[':
case path[i] == '[':
depth++
case arg[i] == ']':
case path[i] == ']':
depth--
case depth == 0:
names.WriteByte(arg[i])
names.WriteByte(path[i])
}
}
segments := strings.Split(names.String(), ".")
return segments[len(segments)-1]
}
// templateError marks a failure that is the input's own fault — no template, or one
// that does not compile. run reports it as misuse (exit 2, with a pointer to --help),
// unlike an unknown path, which is a runtime error (exit 1).
type templateError struct{ error }
// inputError marks a failure that is the input's own fault — no template, or one that
// does not compile. run reports it as misuse (exit 2, with a pointer to --help), unlike
// an unknown path, which is a runtime error (exit 1).
type inputError struct{ error }
func (e templateError) Unwrap() error { return e.error }
func (e inputError) Unwrap() error { return e.error }
// input is what stdin names: the path a lone reference reads, or else the template as
// the library takes it.
// input is what stdin names: the path a lone reference reads, or else the template.
type input struct {
path string
template string
}
// readInput reads stdin less one newline ending it. A JSON object, array or string
// sheds the whitespace around it; any other text becomes a JSON string, so the library
// reads it as a format string even where it is JSON of another kind, such as 42.
func readInput(raw string) (input, error) {
text := strings.TrimSuffix(raw, "\n")
// parseInput reads stdin's text less one newline ending it.
func parseInput(raw string) (input, error) {
text, found := strings.CutSuffix(raw, "\n")
if found {
text = strings.TrimSuffix(text, "\r")
}
if text == "" {
return input{}, fmt.Errorf("stdin holds no template: %s", pipeHint)
}
if path, lone := loneReference(text); lone {
return input{path: path}, nil
}
if trimmed := strings.TrimSpace(text); trimmed != "" && strings.ContainsRune(`{["`, rune(trimmed[0])) && json.Valid([]byte(trimmed)) {
return input{template: trimmed}, nil
}
quoted, err := json.Marshal(text)
if err != nil {
return input{}, err
}
return input{template: string(quoted)}, nil
return input{template: text}, nil
}
// loneReference is the path that text, one reference from the root and nothing else,
@@ -491,7 +482,7 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
}
var src input
if !in.list {
if src, err = source(stdin); err != nil {
if src, err = readStdin(stdin); err != nil {
return fail(stderr, err)
}
}
@@ -515,25 +506,23 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
return 0
}
// source reads the input from stdin.
func source(stdin io.Reader) (input, error) {
func readStdin(stdin io.Reader) (input, error) {
if stdin == nil {
return input{}, templateError{fmt.Errorf("the template comes from stdin, and nothing is piped in: %s", pipeHint)}
return input{}, inputError{fmt.Errorf("the template comes from stdin, and nothing is piped in: %s", pipeHint)}
}
raw, err := io.ReadAll(stdin)
if err != nil {
return input{}, err
}
src, err := readInput(string(raw))
src, err := parseInput(string(raw))
if err != nil {
return input{}, templateError{err}
return input{}, inputError{err}
}
return src, nil
}
// fail reports err and returns its exit code: 2 for the input's own fault, else 1.
func fail(stderr io.Writer, err error) int {
var te templateError
var te inputError
if errors.As(err, &te) {
return misuse(stderr, te.error)
}
+23 -35
View File
@@ -242,8 +242,8 @@ func TestRunMisuse(t *testing.T) {
}
}
// TestRunTakesNoArgument pins the way from the old spelling, a template or a path as
// an argument, to the one that runs: the template on stdin.
// TestRunTakesNoArgument pins the error for a path or a template given as an argument:
// it shows the template piped to stdin.
func TestRunTakesNoArgument(t *testing.T) {
for _, args := range [][]string{{"sv_SE.person"}, {"{/sv_SE.person}"}, {"name:", "{/sv_SE.person.last}"}} {
code, out, errb := runOut("", args...)
@@ -267,47 +267,32 @@ func TestRunTerminalStdinIsMisuse(t *testing.T) {
func TestRunDropsOneTrailingNewline(t *testing.T) {
for stdin, want := range map[string]string{
"hihi": "hihi\n",
"hihi\n": "hihi\n",
"hihi\n\n": "hihi\n\n",
"a\nb\n": "a\nb\n",
" hihi \n": " hihi \n",
"hihi\r\n": "hihi\r\n",
"{{x}}\n": "{x}\n",
"x: {/sv_SE.person.last}\n\n": "",
"hihi": "hihi\n",
"hihi\n": "hihi\n",
"hihi\r\n": "hihi\n",
"hihi\n\n": "hihi\n\n",
"hihi\r\n\r\n": "hihi\r\n\n",
"a\nb\n": "a\nb\n",
" hihi \n": " hihi \n",
"{{x}}\n": "{x}\n",
"\"a\"\n": "a\n",
} {
code, out, errb := runOut(stdin, "--seed", "1")
if want == "" {
if code != 0 || !strings.HasPrefix(out, "x: ") || !strings.HasSuffix(out, "\n\n") || strings.Count(out, "\n") != 2 {
t.Errorf("run(%q) = %d, %q, %q; want the template's own newline, then the line's", stdin, code, out, errb)
}
continue
}
if code != 0 || out != want {
t.Errorf("run(%q) = %d, %q, %q; want %q", stdin, code, out, errb, want)
}
}
}
func TestRunTextPrintsAsWritten(t *testing.T) {
for _, stdin := range []string{"sv_SE.person", "42", "null", "true", "[Skåne län]", "x[1]y", `say "hi"`} {
code, out, errb := runOut(stdin)
if code != 0 || out != stdin+"\n" {
t.Errorf("run(%q) = %d, %q, %q; want the text as written", stdin, code, out, errb)
}
code, out, errb := runOut("x: {/sv_SE.person.last}\n\n", "--seed", "1")
if code != 0 || !strings.HasPrefix(out, "x: ") || !strings.HasSuffix(out, "\n\n") || strings.Count(out, "\n") != 2 {
t.Errorf("a template ending in a newline = %d, %q, %q; want its own newline, then the line's", code, out, errb)
}
}
func TestRunTrimsWhitespaceAroundJSON(t *testing.T) {
for _, stdin := range []string{
"{\"format\":\"{x}\",\"x\":[\"a\",\"b\"]}\n",
" {\"format\":\"{x}\",\"x\":[\"a\",\"b\"]}\n\n",
"\t[\"a\",\"b\"]\r\n",
" \"a\" \n",
} {
code, out, errb := runOut(stdin, "--seed", "1")
if code != 0 || (out != "a\n" && out != "b\n") {
t.Errorf("run(%q) = %d, %q, %q; want a or b", stdin, code, out, errb)
func TestRunTextPrintsAsWritten(t *testing.T) {
for _, stdin := range []string{"sv_SE.person", "[Skåne län]", "x[1]y", `say "hi"`} {
code, out, errb := runOut(stdin)
if code != 0 || out != stdin+"\n" {
t.Errorf("run(%q) = %d, %q, %q; want the text as written", stdin, code, out, errb)
}
}
}
@@ -464,6 +449,9 @@ func TestRunTemplateMisuse(t *testing.T) {
`{"format":"x"}`: "is a string",
"name: {/no.such.path}": "no entry",
"a } b": "}}",
" [\"a\",\"b\"]": "may not be padded",
"[\"a\",\"b\"]\n\n": "may not be padded",
"42": "number",
} {
code, out, errb := runOut(stdin, "--seed", "1")
if code != 2 || out != "" || !strings.Contains(errb, "try 'fejkdata --help'") || !strings.Contains(errb, want) {
@@ -579,7 +567,7 @@ func TestRunRecordNDJSON(t *testing.T) {
}
func TestRunRecordCSVRoundTrips(t *testing.T) {
code, out, errb := runOut("{/users}\n", "--seed", "1", "--format", "csv", "--repeat", "3", "--data-path", recordDir(t))
code, out, errb := runOut("{/users}\r\n", "--seed", "1", "--format", "csv", "--repeat", "3", "--data-path", recordDir(t))
if code != 0 {
t.Fatalf("run = %d, stderr=%q", code, errb)
}