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:
+30
-41
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user