Add inline templates to the CLI and library #4

Merged
lilleman merged 23 commits from cli-inline-template into main 2026-09-03 21:20:26 +02:00
4 changed files with 65 additions and 15 deletions
Showing only changes of commit 7c7bee5cd9 - Show all commits
+33 -12
View File
@@ -301,21 +301,34 @@ func TestRunShippedDataByDefault(t *testing.T) {
}
}
func TestIsTemplate(t *testing.T) {
for arg, want := range map[string]bool{
"sv_SE.person": false,
"person.last": false,
"[abc]": false, // a [ that is not valid JSON is not a template
"[abc].field": false,
"name: {x}": true, // a { token: a path can never carry a brace
`{"format":"x"}`: true,
`["a","b"]`: true, // a JSON array carries no brace
`[1, 2]`: true,
func TestClassify(t *testing.T) {
for arg, want := range map[string]argKind{
"sv_SE.person": argPath,
"person.last": argPath,
`"abc`: argPath, // a quote opening no JSON string is part of a name
"name: {x}": argTemplate, // a { token: a path can never carry a brace
`{"format":"x"}`: argTemplate,
`["a","b"]`: argTemplate, // a JSON array carries no brace
`[1, 2]`: argTemplate,
`"hello"`: argTemplate, // a JSON string, the spelling a format-only object names
} {
if got := isTemplate(arg); got != want {
t.Errorf("isTemplate(%q) = %v, want %v", arg, got, want)
got, err := classify(arg)
if err != nil || got != want {
t.Errorf("classify(%q) = %v, %v; want %v", arg, got, err, want)
}
}
for _, arg := range []string{"[abc]", "[abc].field", "x[1]", "a]b"} {
if _, err := classify(arg); err == nil {
t.Errorf("classify(%q) = no error; want the bracket rejected", arg)
}
}
}
func TestRunShapeMisuseBeforeLoad(t *testing.T) {
code, _, errb := runOut("--no-shipped-data", "[abc]")
if code != 2 || !strings.Contains(errb, "[abc]") || strings.Contains(errb, "--data-path") {
t.Fatalf("shape misuse with no data = %d, %q; want the shape error before any load", code, errb)
}
}
func TestRunInlineTemplate(t *testing.T) {
@@ -327,6 +340,10 @@ func TestRunInlineTemplate(t *testing.T) {
if code != 0 || (out != "name: bosse\n" && out != "name: lina\n") {
t.Fatalf("inline JSON template = %d, %q, want one name, stderr %q", code, out, errb)
}
code, out, errb = runOut("--seed", "1", `"name: {/sv_SE.person.last}"`)
if code != 0 || !strings.HasPrefix(out, "name: ") || strings.Contains(out, "{") {
t.Fatalf("inline JSON string = %d, %q, stderr %q", code, out, errb)
}
code, out, errb = runOut("--seed", "1", "-n", "2", `{digits(1)}`)
if code != 0 || len(strings.Split(strings.TrimRight(out, "\n"), "\n")) != 2 {
t.Fatalf("inline template with --repeat = %d, %q, stderr %q", code, out, errb)
@@ -339,11 +356,15 @@ func TestRunTemplateMisuse(t *testing.T) {
"[red,green]", // a near-miss JSON array (unquoted strings)
`{"format":"x"}`, // an object holding only a format
"{/no.such.path}", // a reference into nothing
"x[1]", // a bracket no path may hold
} {
code, out, errb := runOut("--seed", "1", arg)
if code != 2 || out != "" || !strings.Contains(errb, "try 'fejkdata --help'") {
t.Errorf("run(%q) = %d, %q, %q; want misuse naming --help", arg, code, out, errb)
}
if strings.Contains(errb, "fejkdata: fejkdata:") {
t.Errorf("run(%q) doubled the program prefix: %q", arg, errb)
}
}
}
+18 -1
View File
@@ -94,7 +94,7 @@ func TestFakeTemplateErrors(t *testing.T) {
{`"{x}"`, `no field "x"`},
{`"{digits(0)}"`, "must be positive"},
{`name: {/no.such.path}`, "no entry"},
{`name: {..nope}`, "no folder above"},
{`name: {..nope}`, "write {/nope}"},
{`{"format":"x"}`, "is a string"},
{`{/misc.country} {/misc.country.alpha2}`, "renders a level"},
} {
@@ -105,6 +105,23 @@ func TestFakeTemplateErrors(t *testing.T) {
}
}
func TestFakeTemplateJSONString(t *testing.T) {
f := shipped(t)
got := tmpl(t, f, `"name: {/sv_SE.person.last}"`)
if !strings.HasPrefix(got, "name: ") || strings.Contains(got, "{") {
t.Fatalf("FakeTemplate(JSON string) = %q, want a rendered last name after the prefix", got)
}
}
func TestPaddedJSONIsRejected(t *testing.T) {
f := shipped(t)
in := `{"format":"{x}","x":["a","b"]}`
_, err := f.NewTemplate(" " + in + " ")
if err == nil || !strings.Contains(err.Error(), "write "+in) {
t.Fatalf("NewTemplate(padded JSON) = %v, want an error naming the unpadded spelling", err)
}
}
func TestNewTemplateReusable(t *testing.T) {
f := shipped(t)
tmpl, err := f.NewTemplate(`{digits(2)}`)
+4 -2
View File
@@ -275,8 +275,6 @@ func TestNewErrors(t *testing.T) {
map[string]string{"a|b": `"1"`},
`category "a|b" contains "|"`,
},
// A bracket is not a token-grammar character, but the CLI reads an argument
// starting with [ as a JSON array, so a name carrying one would be misread.
"field name with a bracket": {
map[string]string{"a": `{"format":"{x}","x":"1","b[c":"2"}`},
`field "b[c" contains "["`,
@@ -285,6 +283,10 @@ func TestNewErrors(t *testing.T) {
map[string]string{"[abc]": `"1"`},
`category "[abc]" contains "["`,
},
"field name with a quote": {
map[string]string{"a": `{"format":"{x}","x":"1","b\"c":"2"}`},
`field "b\"c" contains "\""`,
},
// An empty name is not a path segment, so List never offered it — while a
// bare {}, a trailing dot in Fake("a.") and a {/a.} reference all reached
// it. The engine accepted spellings it would never advertise.
+10
View File
@@ -47,3 +47,13 @@ func TestInertObjectIsRejected(t *testing.T) {
}
}
}
func TestInlineFolderSigilsAreRejected(t *testing.T) {
f := shipped(t)
for _, input := range []string{"{.sv_SE.person.last}", "{..sv_SE.person.last}"} {
_, err := f.NewTemplate(input)
if err == nil || !strings.Contains(err.Error(), "write {/sv_SE.person.last}") {
t.Errorf("NewTemplate(%q) = %v, want an error naming the root spelling", input, err)
}
}
}