Compare commits
6 Commits
c9c756e2c4
...
6b0a2a60a8
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b0a2a60a8 | |||
| 7eb891dd80 | |||
| b6e1927468 | |||
| 93e17c1770 | |||
| 9d63943237 | |||
| bb43345d63 |
+17
-8
@@ -34,15 +34,24 @@ replacement, and each removed path, column or flag.
|
|||||||
`en_US.address` read those records, so `en_US.address.street` no longer carries
|
`en_US.address` read those records, so `en_US.address.street` no longer carries
|
||||||
`name` and `suffix`, and a locale folder loads only beside `geo`.
|
`name` and `suffix`, and a locale folder loads only beside `geo`.
|
||||||
- `{date(from,to,'layout')}` and `{time('layout')}`: a second between two days, or
|
- `{date(from,to,'layout')}` and `{time('layout')}`: a second between two days, or
|
||||||
within one, in a quoted Go layout. `sv_SE.date`, `en_US.date`, `sv_SE.time` and
|
within one, in a single-quoted Go layout, drawn in UTC; `from` may equal `to`.
|
||||||
`en_US.time` render through them, so `date.year`, `date.month`, `date.day` and
|
`sv_SE.date`, `en_US.date`, `sv_SE.time` and `en_US.time` render through them, so
|
||||||
`time.hour` are no longer paths, and `misc.datetime` is an RFC 3339 instant.
|
`date.year`, `date.month`, `date.day`, `time.hour`, `time.minute`, `time.minute.t`,
|
||||||
|
`time.sec` and `time.ampm` are no longer paths — a part of a date is now its own
|
||||||
|
`{date(…,'2006')}`. `misc.datetime` is an RFC 3339 instant.
|
||||||
- `sex`, `first-name` and `last-name` tables in `sv_SE` and `en_US`, weighted by
|
- `sex`, `first-name` and `last-name` tables in `sv_SE` and `en_US`, weighted by
|
||||||
bearers from SCB, the SSA and the Census Bureau; `first-name` links to `sex`, and a
|
bearers from SCB, the SSA and the Census Bureau; `first-name` links to `sex`, and a
|
||||||
name both sexes carry is a row under each. `person` reads them, so its columns are
|
name both sexes carry is a row under each. `person` reads them, so its columns are
|
||||||
`first`, `last`, `prefix` and `sex`, and `person.femalefirst` and
|
`first`, `last`, `prefix` and `sex`; `person.femalefirst` and `person.malefirst` are
|
||||||
`person.malefirst` are no longer paths.
|
no longer paths — draw `sex[f].first-name` and `sex[m].first-name` instead.
|
||||||
|
`en_US.title` links to `sex` as well, so `en_US.person.prefix` draws `Mr` or `Ms`
|
||||||
|
without contradicting the record's `sex`.
|
||||||
- `sv_SE.personnummer` and `sv_SE.samordningsnummer`, Skatteverket's test series
|
- `sv_SE.personnummer` and `sv_SE.samordningsnummer`, Skatteverket's test series
|
||||||
from a `sv_SE.birth-number` table under `sex`, in place of `sv_SE.ssn`; `en_US.ssn`
|
from a `sv_SE.birth-number` table under `sex`, in place of `sv_SE.ssn`, whose
|
||||||
in the ranges the SSA assigns, and `en_US.itin`. `en_US.person.prefix` no longer
|
`ssn.mmdd`, `ssn.mmdd.m` and `ssn.mmdd.d` go with it. `en_US.ssn` now draws the
|
||||||
draws `Mr`, `Mrs`, `Ms` or `Miss`, which could contradict the record's `sex`.
|
ranges the SSA assigns and carries the columns `area`, `group` and `serial`, so
|
||||||
|
`--format csv en_US.ssn` writes a header where it used to fail; `en_US.itin` is new.
|
||||||
|
- An error names a spelling that runs: a layout is named single-quoted and free of
|
||||||
|
its own quotes, a row of a table with no key is named as the path that selects it,
|
||||||
|
`sv_SE.sex[f].first-name[Kim]`, and a category with no columns names the record
|
||||||
|
that gives it one.
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ fejkdata 'geo.SE.locality[Lund].street' # Fjelievägen — a linked tabl
|
|||||||
fejkdata --data-path ./mydata sv_SE.word # layer a directory over the shipped data
|
fejkdata --data-path ./mydata sv_SE.word # layer a directory over the shipped data
|
||||||
fejkdata --no-shipped-data -d ./mydata --list # only your data
|
fejkdata --no-shipped-data -d ./mydata --list # only your data
|
||||||
fejkdata 'name: {/sv_SE.person.last}' # name: <a surname> — an inline template
|
fejkdata 'name: {/sv_SE.person.last}' # name: <a surname> — an inline template
|
||||||
|
fejkdata "{date(1990-01-01,2010-12-31,'2006-01-02')}" # 2003-11-27 — the argument in "…", the layout in '…'
|
||||||
fejkdata '{"format":"name: {x}","x":["bosse","lina"]}' # name: bosse or name: lina
|
fejkdata '{"format":"name: {x}","x":["bosse","lina"]}' # name: bosse or name: lina
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -237,7 +238,16 @@ draws a woman's name, and a name both sexes carry is a row under each, so
|
|||||||
`person` reads one draw of the three, so its `first` and `sex` columns agree, and so
|
`person` reads one draw of the three, so its `first` and `sex` columns agree, and so
|
||||||
does a `personnummer` in the same render: its birth number, `sv_SE.birth-number`
|
does a `personnummer` in the same render: its birth number, `sv_SE.birth-number`
|
||||||
under `sex`, is Skatteverket's test series, 238 for a woman and 239 for a man, which no
|
under `sex`, is Skatteverket's test series, 238 for a woman and 239 for a man, which no
|
||||||
real person is ever given.
|
real person is ever given. `en_US.title` links to `sex` too, so a person's prefix
|
||||||
|
never contradicts it.
|
||||||
|
|
||||||
|
A person of a chosen sex is assembled from the tables — `sex[f].first-name` beside
|
||||||
|
`last-name` — while a shipped `personnummer` agrees with the sex its own render
|
||||||
|
*drew*, not with one a path selects. That test series is also small: a personnummer
|
||||||
|
is one of about 70,000 values, a day in 1930–2025 against the two birth numbers, so a
|
||||||
|
fixture past a few hundred rows repeats one and a `UNIQUE` column needs a category of
|
||||||
|
your own. `sv_SE.date` and `en_US.date` are uniform over 1970-01-01 to 2029-12-31,
|
||||||
|
`misc.datetime` over 2000-01-01 to 2029-12-31.
|
||||||
|
|
||||||
A `geo` folder holds one tree per country under its alpha-2 code: five
|
A `geo` folder holds one tree per country under its alpha-2 code: five
|
||||||
[linked tables](#linked-tables) named alike, and an `address` record over one
|
[linked tables](#linked-tables) named alike, and an `address` record over one
|
||||||
@@ -536,12 +546,14 @@ them a birthdate:
|
|||||||
Renders e.g. `811218-2389`. A layout is Go's: the reference time `Mon Jan 2
|
Renders e.g. `811218-2389`. A layout is Go's: the reference time `Mon Jan 2
|
||||||
15:04:05 MST 2006` spelled as the output should look, quoted, since a layout may
|
15:04:05 MST 2006` spelled as the output should look, quoted, since a layout may
|
||||||
carry the comma that separates arguments, with English names. Every second
|
carry the comma that separates arguments, with English names. Every second
|
||||||
between the two days is reachable, so a layout with a clock draws the time too.
|
between the two days is reachable, so a layout with a clock draws the time too, and
|
||||||
|
`from` may equal `to`, which is that one day. The instant is UTC, so a zone in the
|
||||||
|
layout prints `UTC` or `Z`.
|
||||||
The quotes delimit a layout outside a selector only, so `[O'Fallon]` in an
|
The quotes delimit a layout outside a selector only, so `[O'Fallon]` in an
|
||||||
argument stays a name. Rejected at `New`: a bound that is no calendar date, or not
|
argument stays a name. Rejected at `New`: a bound that is no calendar date, or not
|
||||||
before the other; an unquoted layout, naming the quoted one; a layout naming no field, which is text;
|
before the other; an unquoted layout, naming the single-quoted one; a layout naming no field, which is
|
||||||
for `date` a layout naming no date field, naming `time`; and for `time` a layout
|
text, as is one day in a layout with no clock; for `date` a layout naming no date
|
||||||
naming a date field, naming `date`. `{seq()}` spans `Fake`
|
field, naming `time`; and for `time` a layout naming a date field, naming `date`. `{seq()}` spans `Fake`
|
||||||
calls and `repeat`, resets with a new generator, and is the natural primary key for
|
calls and `repeat`, resets with a new generator, and is the natural primary key for
|
||||||
the SQL example above.
|
the SQL example above.
|
||||||
|
|
||||||
@@ -721,6 +733,15 @@ datatype and nullability, and each table's key, name, weight and parent columns;
|
|||||||
changes it or `data/` adds its `CHANGELOG.md` entry, which CI checks. A removed,
|
changes it or `data/` adds its `CHANGELOG.md` entry, which CI checks. A removed,
|
||||||
renamed or retyped line is a major.
|
renamed or retyped line is a major.
|
||||||
|
|
||||||
|
## Audience
|
||||||
|
|
||||||
|
App developers writing tests and fixtures, in Go and at a shell:
|
||||||
|
|
||||||
|
- a **bulk fixture author**, thousands of rows into CSV or SQL
|
||||||
|
- a **Go test author**, filling a struct with `FakeStruct`
|
||||||
|
- a **hand fixture author**, one value at a shell
|
||||||
|
- a **validator-facing author**, who needs a value a real checker accepts
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
|
|
||||||
1. **Valid by construction** — every value passes the check its real consumer
|
1. **Valid by construction** — every value passes the check its real consumer
|
||||||
@@ -1063,12 +1084,21 @@ renamed or retyped line is a major.
|
|||||||
bigger neighbour ship no address; counting the land outside every place too would
|
bigger neighbour ship no address; counting the land outside every place too would
|
||||||
drop a quarter of the places, whose codes straddle unincorporated land, for a
|
drop a quarter of the places, whose codes straddle unincorporated land, for a
|
||||||
postal city the USPS mostly names the same way.
|
postal city the USPS mostly names the same way.
|
||||||
|
- **`--list` stays a plain list of paths.** It is what a script reads, so every line
|
||||||
|
has to be a path that `Fake` takes; a marker for the tables a `[selector]` follows,
|
||||||
|
or a legend above them, would make the output something to parse before use.
|
||||||
|
`--help` names the selector spelling instead, and the Table section teaches it.
|
||||||
- **A layout is always quoted.** A layout may carry the comma that separates
|
- **A layout is always quoted.** A layout may carry the comma that separates
|
||||||
arguments, `'January 2, 2006'`, and one spelling for every layout beats a rule
|
arguments, `'January 2, 2006'`, and one spelling for every layout beats a rule
|
||||||
about which ones need the quotes, so the bare spelling is refused naming the
|
about which ones need the quotes, so the bare spelling is refused naming the
|
||||||
quoted one. The layout is Go's reference time because the library renders with
|
quoted one. The layout is Go's reference time because the library renders with
|
||||||
it and a Go caller already knows it; its names are English, and a locale's own
|
it and a Go caller already knows it; its names are English, and a locale's own
|
||||||
month and weekday names are data.
|
month and weekday names are data.
|
||||||
|
- **A title is a table under `sex`.** A prefix drawn apart would put `Mr` on a record
|
||||||
|
whose `sex` column says `female`, which is the disagreement the record exists to
|
||||||
|
prevent; the tables this set already has are what a title needs, so `en_US.title`
|
||||||
|
links to `sex` as `first-name` does. Swedish has no everyday sexed honorific, so
|
||||||
|
`sv_SE` keeps an unsexed `dr` and `prof`.
|
||||||
- **No builtin reads the clock, so a date is bounded by days, never by an age.**
|
- **No builtin reads the clock, so a date is bounded by days, never by an age.**
|
||||||
An `age(min,max)` would make a seeded fixture change with the day it runs on,
|
An `age(min,max)` would make a seeded fixture change with the day it runs on,
|
||||||
which is what a seed exists to prevent; a birthdate for someone 20 to 60 is
|
which is what a seed exists to prevent; a birthdate for someone 20 to 60 is
|
||||||
|
|||||||
+35
-8
@@ -472,17 +472,28 @@ func iban(r rng, cc string) string {
|
|||||||
|
|
||||||
const dayLayout = "2006-01-02"
|
const dayLayout = "2006-01-02"
|
||||||
|
|
||||||
// Alike in no field; layoutDay differs from layoutProbe in its date fields only.
|
// Alike in no field; layoutDay differs from layoutProbe in its date fields only,
|
||||||
|
// layoutClock in its clock fields only.
|
||||||
var (
|
var (
|
||||||
layoutProbe = time.Date(2001, 2, 3, 4, 5, 6, 0, time.UTC)
|
layoutProbe = time.Date(2001, 2, 3, 4, 5, 6, 0, time.UTC)
|
||||||
layoutProbe2 = time.Date(2010, 11, 12, 13, 14, 15, 0, time.UTC)
|
layoutProbe2 = time.Date(2010, 11, 12, 13, 14, 15, 0, time.UTC)
|
||||||
layoutDay = time.Date(2010, 11, 12, 4, 5, 6, 0, time.UTC)
|
layoutDay = time.Date(2010, 11, 12, 4, 5, 6, 0, time.UTC)
|
||||||
|
layoutClock = time.Date(2001, 2, 3, 13, 14, 15, 0, time.UTC)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// quotedLayout reports whether an arg carries the single quotes a layout is written in.
|
||||||
|
func quotedLayout(a string) bool {
|
||||||
|
return len(a) >= 2 && a[0] == '\'' && a[len(a)-1] == '\''
|
||||||
|
}
|
||||||
|
|
||||||
// layoutArg is the Go layout a quoted arg holds, refused when unquoted or constant.
|
// layoutArg is the Go layout a quoted arg holds, refused when unquoted or constant.
|
||||||
func layoutArg(a string) (string, error) {
|
func layoutArg(a string) (string, error) {
|
||||||
if len(a) < 2 || a[0] != '\'' || a[len(a)-1] != '\'' {
|
if !quotedLayout(a) {
|
||||||
return "", fmt.Errorf("layout %s is not quoted; write '%s'", a, strings.Trim(a, "'"))
|
bare := strings.Trim(a, `'"`)
|
||||||
|
if strings.HasPrefix(a, `"`) || strings.HasSuffix(a, `"`) {
|
||||||
|
return "", fmt.Errorf("layout %s is double-quoted; write '%s'", a, bare)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("layout %s is not quoted; write '%s', which a shell keeps only inside a double-quoted argument", a, bare)
|
||||||
}
|
}
|
||||||
layout := a[1 : len(a)-1]
|
layout := a[1 : len(a)-1]
|
||||||
if layoutProbe.Format(layout) == layoutProbe2.Format(layout) {
|
if layoutProbe.Format(layout) == layoutProbe2.Format(layout) {
|
||||||
@@ -501,18 +512,31 @@ func layoutOf(a string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// layoutArity checks a call ending in a layout takes n args, naming the quoted
|
// layoutArity checks a call ending in a layout takes n args, naming the quoted
|
||||||
// layout when an unquoted one split into more.
|
// layout where an unquoted one split into more.
|
||||||
func layoutArity(name string, n int, a []string) error {
|
func layoutArity(name string, n int, a []string) error {
|
||||||
if len(a) == n {
|
if len(a) == n {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
hint := ""
|
hint := ""
|
||||||
if len(a) > n {
|
if len(a) > n && !split(a[n-1:]) {
|
||||||
hint = fmt.Sprintf("; a layout holding a comma is quoted: '%s'", strings.Join(a[n-1:], ", "))
|
hint = fmt.Sprintf("; a layout holding a comma is quoted: '%s'", strings.Join(a[n-1:], ", "))
|
||||||
}
|
}
|
||||||
return fmt.Errorf("%s takes %d args, got %d%s", name, n, len(a), hint)
|
return fmt.Errorf("%s takes %d argument%s, got %d%s", name, n, plural(n), len(a), hint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// split reports whether the surplus args already hold a quoted layout, which no
|
||||||
|
// comma split apart.
|
||||||
|
func split(a []string) bool {
|
||||||
|
for _, arg := range a {
|
||||||
|
if quotedLayout(arg) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func plural(n int) string { return map[bool]string{true: "s"}[n != 1] }
|
||||||
|
|
||||||
func dateArgs(_ map[string]node, a []string) error {
|
func dateArgs(_ map[string]node, a []string) error {
|
||||||
if err := layoutArity("date", 3, a); err != nil {
|
if err := layoutArity("date", 3, a); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -525,8 +549,8 @@ func dateArgs(_ map[string]node, a []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("date(from,to,layout): to %q is not a YYYY-MM-DD date", a[1])
|
return fmt.Errorf("date(from,to,layout): to %q is not a YYYY-MM-DD date", a[1])
|
||||||
}
|
}
|
||||||
if !from.Before(to) {
|
if to.Before(from) {
|
||||||
return fmt.Errorf("date(from,to,layout): from %s is not before to %s", a[0], a[1])
|
return fmt.Errorf("date(from,to,layout): from %s is after to %s", a[0], a[1])
|
||||||
}
|
}
|
||||||
layout, err := layoutArg(a[2])
|
layout, err := layoutArg(a[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -535,6 +559,9 @@ func dateArgs(_ map[string]node, a []string) error {
|
|||||||
if layoutProbe.Format(layout) == layoutDay.Format(layout) {
|
if layoutProbe.Format(layout) == layoutDay.Format(layout) {
|
||||||
return fmt.Errorf("date(from,to,layout): '%s' names no date field; write time('%s')", layout, layout)
|
return fmt.Errorf("date(from,to,layout): '%s' names no date field; write time('%s')", layout, layout)
|
||||||
}
|
}
|
||||||
|
if from.Equal(to) && layoutProbe.Format(layout) == layoutClock.Format(layout) {
|
||||||
|
return fmt.Errorf("date(%s,%s,'%s') is the constant %q; write it as text", a[0], a[1], layout, from.Format(layout))
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+49
-3
@@ -298,13 +298,13 @@ func TestBuiltinDateAndTime(t *testing.T) {
|
|||||||
func TestBuiltinDateArgs(t *testing.T) {
|
func TestBuiltinDateArgs(t *testing.T) {
|
||||||
for tmpl, want := range map[string]string{
|
for tmpl, want := range map[string]string{
|
||||||
`"{date(1990-13-01,1990-12-31,'2006-01-02')}"`: "1990-13-01",
|
`"{date(1990-13-01,1990-12-31,'2006-01-02')}"`: "1990-13-01",
|
||||||
`"{date(1990-12-31,1990-01-01,'2006-01-02')}"`: "before",
|
`"{date(1990-12-31,1990-01-01,'2006-01-02')}"`: "is after",
|
||||||
`"{date(1990-01-01,1990-01-01,'2006-01-02')}"`: "before",
|
`"{date(1990-01-01,1990-01-01,'2006-01-02')}"`: "write it as text",
|
||||||
`"{date(1990-01-01,1990-12-31,2006-01-02)}"`: "'2006-01-02'",
|
`"{date(1990-01-01,1990-12-31,2006-01-02)}"`: "'2006-01-02'",
|
||||||
`"{date(1990-01-01,1990-12-31,'January 2, 2006)}"`: "'",
|
`"{date(1990-01-01,1990-12-31,'January 2, 2006)}"`: "'",
|
||||||
`"{date(1990-01-01,1990-12-31,'x')}"`: "text",
|
`"{date(1990-01-01,1990-12-31,'x')}"`: "text",
|
||||||
`"{date(1990-01-01,1990-12-31,'')}"`: "text",
|
`"{date(1990-01-01,1990-12-31,'')}"`: "text",
|
||||||
`"{date(1990-01-01,1990-12-31)}"`: "3 args",
|
`"{date(1990-01-01,1990-12-31)}"`: "3 arguments",
|
||||||
`"{date(1990-01-01,1990-12-31,January 2, 2006)}"`: "'January 2, 2006'",
|
`"{date(1990-01-01,1990-12-31,January 2, 2006)}"`: "'January 2, 2006'",
|
||||||
`"{time(3:04 PM, Mon)}"`: "'3:04 PM, Mon'",
|
`"{time(3:04 PM, Mon)}"`: "'3:04 PM, Mon'",
|
||||||
`"{time(15:04)}"`: "'15:04'",
|
`"{time(15:04)}"`: "'15:04'",
|
||||||
@@ -318,3 +318,49 @@ func TestBuiltinDateArgs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBuiltinLayoutErrorsNameARunnableSpelling pins the two layout errors a user
|
||||||
|
// can follow: a double-quoted layout is named single-quoted, without its own
|
||||||
|
// quotes carried into the suggestion, and a bare one says a shell ate them.
|
||||||
|
func TestBuiltinLayoutErrorsNameARunnableSpelling(t *testing.T) {
|
||||||
|
_, err := compile(parse(t, `"{date(1990-01-01,1990-12-31,\"2006-01-02\")}"`))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "write '2006-01-02'") {
|
||||||
|
t.Fatalf("a double-quoted layout = %v, want it named single-quoted", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), `'"`) {
|
||||||
|
t.Errorf("%v names a layout that renders its own quotes", err)
|
||||||
|
}
|
||||||
|
_, err = compile(parse(t, `"{time(15:04)}"`))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "write '15:04'") || !strings.Contains(err.Error(), "shell") {
|
||||||
|
t.Fatalf("a bare layout = %v, want it named quoted and the shell explained", err)
|
||||||
|
}
|
||||||
|
// The comma hint belongs to a layout that split, not to a call given extra args.
|
||||||
|
_, err = compile(parse(t, `"{time(0,12,'15:04')}"`))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "takes 1 argument, got 3") {
|
||||||
|
t.Fatalf("time with three args = %v, want the count named in the singular", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "holding a comma") {
|
||||||
|
t.Errorf("%v offers the comma hint though the layout is already quoted", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBuiltinDateSpansOneDay pins that from == to is a day: with a clock layout it
|
||||||
|
// draws every second of it, and without one it could only emit one value.
|
||||||
|
func TestBuiltinDateSpansOneDay(t *testing.T) {
|
||||||
|
f := engine(1)
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for i := 0; i < 500; i++ {
|
||||||
|
got := mustRender(t, f, `"{date(2026-01-01,2026-01-01,'2006-01-02 15:04:05')}"`)
|
||||||
|
if !strings.HasPrefix(got, "2026-01-01 ") {
|
||||||
|
t.Fatalf("date over one day = %q, out of range", got)
|
||||||
|
}
|
||||||
|
seen[got] = true
|
||||||
|
}
|
||||||
|
if len(seen) < 400 {
|
||||||
|
t.Fatalf("date over one day drew %d distinct seconds in 500, want the whole day", len(seen))
|
||||||
|
}
|
||||||
|
_, err := compile(parse(t, `"{date(2026-01-01,2026-01-01,'2006-01-02')}"`))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "write it as text") {
|
||||||
|
t.Fatalf("one day in a date-only layout = %v, want it named a constant", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
"format": "{prefix}{first} {last}",
|
"format": "{prefix}{first} {last}",
|
||||||
"first": "{.first-name.name}",
|
"first": "{.first-name.name}",
|
||||||
"last": "{.last-name.name}",
|
"last": "{.last-name.name}",
|
||||||
"prefix": ["", { "format": "{title} ", "title": ["Dr", "Mx", "Prof"], "weight": 0.1 }],
|
"prefix": ["", { "format": "{.title.name} ", "weight": 0.1 }],
|
||||||
"sex": "{.sex.name}"
|
"sex": "{.sex.name}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{ "format": "{name}", "rows": "title.tsv", "name": "name", "parent": "sex", "weight": "share" }
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
name sex share
|
||||||
|
Dr f 6
|
||||||
|
Dr m 6
|
||||||
|
Miss f 4
|
||||||
|
Mr m 60
|
||||||
|
Mrs f 22
|
||||||
|
Ms f 30
|
||||||
|
Mx f 1
|
||||||
|
Mx m 1
|
||||||
|
Prof f 3
|
||||||
|
Prof m 3
|
||||||
|
@@ -2,6 +2,6 @@
|
|||||||
"format": "{prefix}{first} {last}",
|
"format": "{prefix}{first} {last}",
|
||||||
"first": "{.first-name.name}",
|
"first": "{.first-name.name}",
|
||||||
"last": "{.last-name.name}",
|
"last": "{.last-name.name}",
|
||||||
"prefix": ["", { "format": "{string} ", "string": ["dr", "prof"], "weight": 0.05 }],
|
"prefix": ["", { "format": "{title} ", "title": ["dr", "prof"], "weight": 0.05 }],
|
||||||
"sex": "{.sex.name}"
|
"sex": "{.sex.name}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -392,3 +392,25 @@ func TestShippedStreetNumberFormats(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestShippedUSTitleAgreesWithSex pins that one person's title and sex agree, and
|
||||||
|
// that the everyday titles are reachable.
|
||||||
|
func TestShippedUSTitleAgreesWithSex(t *testing.T) {
|
||||||
|
f := newGenerator(t, "data", WithSeed(4))
|
||||||
|
female := map[string]bool{"Miss": true, "Mrs": true, "Ms": true}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for i := 0; i < 3000; i++ {
|
||||||
|
got := fakeTemplate(t, f, `{/en_US.person.prefix}|{/en_US.person.sex}`)
|
||||||
|
title, sex, _ := strings.Cut(got, "|")
|
||||||
|
title = strings.TrimSpace(title)
|
||||||
|
seen[title] = true
|
||||||
|
if title == "Mr" && sex != "male" || female[title] && sex != "female" {
|
||||||
|
t.Fatalf("%q: the title contradicts the sex", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, want := range []string{"", "Mr", "Ms"} {
|
||||||
|
if !seen[want] {
|
||||||
|
t.Errorf("en_US.person.prefix never drew %q in 3000 draws", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -140,6 +140,10 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
|
|||||||
return nil, fmt.Errorf("fejkdata: %s descends into %q, a field; only a category-level template is a record", path, tail[0])
|
return nil, fmt.Errorf("fejkdata: %s descends into %q, a field; only a category-level template is a record", path, tail[0])
|
||||||
}
|
}
|
||||||
shape := f.recordShapeOf(n)
|
shape := f.recordShapeOf(n)
|
||||||
|
if errors.Is(shape.err, errNoColumns) {
|
||||||
|
column := path[strings.LastIndex(path, ".")+1:]
|
||||||
|
return nil, fmt.Errorf("fejkdata: %s %w; render it as a column of one: '{\"format\":\"\",\"%s\":\"{/%s}\"}'", path, shape.err, column, path)
|
||||||
|
}
|
||||||
if shape.err != nil {
|
if shape.err != nil {
|
||||||
return nil, fmt.Errorf("fejkdata: %s %w", path, shape.err)
|
return nil, fmt.Errorf("fejkdata: %s %w", path, shape.err)
|
||||||
}
|
}
|
||||||
@@ -227,6 +231,10 @@ func (f *Generator) FakeRecordTemplate(input string) (*Record, error) {
|
|||||||
return t.Fake(), nil
|
return t.Fake(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// errNoColumns is the one record fence a path can answer, so its entry point names
|
||||||
|
// the record to write instead.
|
||||||
|
var errNoColumns = errors.New("has no fields, so no columns")
|
||||||
|
|
||||||
// recordOf is the fence both record entry points pass. The columns come back with
|
// recordOf is the fence both record entry points pass. The columns come back with
|
||||||
// the template, fixed for every draw the caller goes on to make.
|
// the template, fixed for every draw the caller goes on to make.
|
||||||
func recordOf(n node) (*template, []Column, error) {
|
func recordOf(n node) (*template, []Column, error) {
|
||||||
@@ -239,7 +247,7 @@ func recordOf(n node) (*template, []Column, error) {
|
|||||||
}
|
}
|
||||||
names := recordColumns(t)
|
names := recordColumns(t)
|
||||||
if len(names) == 0 {
|
if len(names) == 0 {
|
||||||
return nil, nil, errors.New("has no fields, so no columns")
|
return nil, nil, errNoColumns
|
||||||
}
|
}
|
||||||
if !t.record {
|
if !t.record {
|
||||||
return nil, nil, fmt.Errorf("carries repeat %d, which composes its format into one string; a record projects columns instead — drop the repeat and render the record again for more rows", t.repeat)
|
return nil, nil, fmt.Errorf("carries repeat %d, which composes its format into one string; a record projects columns instead — drop the repeat and render the record again for more rows", t.repeat)
|
||||||
|
|||||||
@@ -547,3 +547,18 @@ func TestRecordTemplateRejectsATopLevelRepeat(t *testing.T) {
|
|||||||
t.Errorf("NewTemplate on the same input = %v, want the string view to still compile", err)
|
t.Errorf("NewTemplate on the same input = %v, want the string view to still compile", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRecordOfAFieldlessCategoryNamesTheWrapper pins the way out of the first
|
||||||
|
// command a fixture author types: a category of one value has no columns, and the
|
||||||
|
// error names the record that gives it one.
|
||||||
|
func TestRecordOfAFieldlessCategoryNamesTheWrapper(t *testing.T) {
|
||||||
|
f := newGenerator(t, "data", WithSeed(1))
|
||||||
|
want := `'{"format":"","personnummer":"{/sv_SE.personnummer}"}'`
|
||||||
|
_, err := f.FakeRecord("sv_SE.personnummer")
|
||||||
|
if err == nil || !strings.Contains(err.Error(), want) {
|
||||||
|
t.Fatalf("FakeRecord(sv_SE.personnummer) = %v, want it to name %s", err, want)
|
||||||
|
}
|
||||||
|
if _, err := f.FakeRecordTemplate(`{"format":"","personnummer":"{/sv_SE.personnummer}"}`); err != nil {
|
||||||
|
t.Fatalf("the named wrapper does not render: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
// cell of the row a render pinned; a cell carrying tokens compiles to a string node.
|
// cell of the row a render pinned; a cell carrying tokens compiles to a string node.
|
||||||
type table struct {
|
type table struct {
|
||||||
category string
|
category string
|
||||||
|
path string // the category's path from the data root, set when the tables are linked
|
||||||
file string
|
file string
|
||||||
format *template // fields are the column nodes
|
format *template // fields are the column nodes
|
||||||
columns []string
|
columns []string
|
||||||
@@ -350,6 +351,7 @@ func linkTables(root map[string]node) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case *table:
|
case *table:
|
||||||
|
n.path = path
|
||||||
if n.parent < 0 {
|
if n.parent < 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -508,7 +510,7 @@ func (t *table) under(r int, a *table, pr int) bool {
|
|||||||
// naming several rows resolves inside the ancestors pinned in d.
|
// naming several rows resolves inside the ancestors pinned in d.
|
||||||
func (t *table) find(sel string, d *draws) (int, error) {
|
func (t *table) find(sel string, d *draws) (int, error) {
|
||||||
if t.key < 0 && t.name < 0 {
|
if t.key < 0 && t.name < 0 {
|
||||||
return 0, fmt.Errorf("%s has no key or name column to select a row by", t.category)
|
return 0, fmt.Errorf("%s has no key or name column to select a row by", t.spelledPath())
|
||||||
}
|
}
|
||||||
if r, ok := t.byKey[sel]; ok {
|
if r, ok := t.byKey[sel]; ok {
|
||||||
return r, nil
|
return r, nil
|
||||||
@@ -521,7 +523,7 @@ func (t *table) find(sel string, d *draws) (int, error) {
|
|||||||
case 1:
|
case 1:
|
||||||
return rows[0], nil
|
return rows[0], nil
|
||||||
case 0:
|
case 0:
|
||||||
return 0, fmt.Errorf("no row of %s has key or name %q", t.category, sel)
|
return 0, fmt.Errorf("no row of %s has key or name %q", t.spelledPath(), sel)
|
||||||
}
|
}
|
||||||
keys := make([]string, len(rows))
|
keys := make([]string, len(rows))
|
||||||
for i, r := range rows {
|
for i, r := range rows {
|
||||||
@@ -531,14 +533,24 @@ func (t *table) find(sel string, d *draws) (int, error) {
|
|||||||
keys[i] = t.cell(r, t.key)
|
keys[i] = t.cell(r, t.key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
listed := strings.Join(keys, ", ")
|
||||||
if t.key < 0 {
|
if t.key < 0 {
|
||||||
return 0, fmt.Errorf("%q names %d rows of %s; select it inside its %s, one of %v", sel, len(rows), t.category, t.parentT.category, keys)
|
return 0, fmt.Errorf("%q names %d rows of %s; select it inside its %s, one of %s", sel, len(rows), t.spelledPath(), t.parentT.spelledPath(), listed)
|
||||||
}
|
}
|
||||||
inside := ""
|
inside := ""
|
||||||
if t.parentT != nil {
|
if t.parentT != nil {
|
||||||
inside = fmt.Sprintf(", or select it inside its %s", t.parentT.category)
|
inside = fmt.Sprintf(", or select it inside its %s", t.parentT.category)
|
||||||
}
|
}
|
||||||
return 0, fmt.Errorf("%q names %d rows of %s; select one by key, one of %v%s", sel, len(rows), t.category, keys, inside)
|
return 0, fmt.Errorf("%q names %d rows of %s; select one by key, one of %s%s", sel, len(rows), t.spelledPath(), listed, inside)
|
||||||
|
}
|
||||||
|
|
||||||
|
// spelledPath is the path a selector on t is written at, the category's own name
|
||||||
|
// until the tables are linked.
|
||||||
|
func (t *table) spelledPath() string {
|
||||||
|
if t.path == "" {
|
||||||
|
return t.category
|
||||||
|
}
|
||||||
|
return t.path
|
||||||
}
|
}
|
||||||
|
|
||||||
// selectorSpelling is how a path writes a selected row, for messages: by key, or
|
// selectorSpelling is how a path writes a selected row, for messages: by key, or
|
||||||
@@ -546,7 +558,7 @@ func (t *table) find(sel string, d *draws) (int, error) {
|
|||||||
func (t *table) selectorSpelling(r int) string {
|
func (t *table) selectorSpelling(r int) string {
|
||||||
switch {
|
switch {
|
||||||
case t.key >= 0:
|
case t.key >= 0:
|
||||||
return t.category + "[" + t.cell(r, t.key) + "]"
|
return t.spelledPath() + "[" + t.cell(r, t.key) + "]"
|
||||||
case t.name >= 0:
|
case t.name >= 0:
|
||||||
return t.parentT.selectorSpelling(t.parentRow(r)) + "." + t.category + "[" + t.cell(r, t.name) + "]"
|
return t.parentT.selectorSpelling(t.parentRow(r)) + "." + t.category + "[" + t.cell(r, t.name) + "]"
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-2
@@ -172,7 +172,7 @@ func TestTableSelectsARowByKeyOrName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for path, want := range map[string]string{
|
for path, want := range map[string]string{
|
||||||
"region[99]": `"99"`,
|
"region[99]": `"99"`,
|
||||||
"locality[Sandby]": "L7 L8",
|
"locality[Sandby]": "L7, L8",
|
||||||
"region[12].code[1]": "not a table",
|
"region[12].code[1]": "not a table",
|
||||||
"region[]": "empty",
|
"region[]": "empty",
|
||||||
"region[12": "]",
|
"region[12": "]",
|
||||||
@@ -334,7 +334,7 @@ func TestTableSelectorInAReference(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for name, c := range map[string]struct{ json, want string }{
|
for name, c := range map[string]struct{ json, want string }{
|
||||||
"unknown row": {`"{/region[99].name}"`, `"99"`},
|
"unknown row": {`"{/region[99].name}"`, `"99"`},
|
||||||
"ambiguous name": {`"{/locality[Sandby].name}"`, "L7 L8"},
|
"ambiguous name": {`"{/locality[Sandby].name}"`, "L7, L8"},
|
||||||
"not inside": {`"{/region[12].municipality[0180].name}"`, "not inside"},
|
"not inside": {`"{/region[12].municipality[0180].name}"`, "not inside"},
|
||||||
"selector on template": {`"{/x[1].a}"`, "not a table"},
|
"selector on template": {`"{/x[1].a}"`, "not a table"},
|
||||||
} {
|
} {
|
||||||
@@ -744,3 +744,32 @@ func TestNamedTableWithoutAKeyResolvesInsideItsParent(t *testing.T) {
|
|||||||
t.Fatalf("a name selected inside a pinned parent = %q", got)
|
t.Fatalf("a name selected inside a pinned parent = %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAmbiguousNameNamesARunnablePath pins what a shell user reads: each row is
|
||||||
|
// named as the path they can type, from the root, and the rows are listed plainly.
|
||||||
|
func TestAmbiguousNameNamesARunnablePath(t *testing.T) {
|
||||||
|
files := map[string]string{}
|
||||||
|
for name, body := range siblings() {
|
||||||
|
files["se/"+name] = body
|
||||||
|
}
|
||||||
|
files["se/street.json"] = `{"format":"{name}","rows":"street.tsv","name":"name","parent":"locality","weight":"segments"}`
|
||||||
|
f := newGenerator(t, writeFiles(t, files), WithSeed(1))
|
||||||
|
_, err := f.Fake("se.street[Sandbyvägen]")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("se.street[Sandbyvägen] resolved, though two rows carry that name")
|
||||||
|
}
|
||||||
|
for _, want := range []string{"se.locality[L7].street[Sandbyvägen]", "se.locality[L8].street[Sandbyvägen]"} {
|
||||||
|
if !strings.Contains(err.Error(), want) {
|
||||||
|
t.Errorf("%v does not name %s", err, want)
|
||||||
|
}
|
||||||
|
if got := fake(t, f, want); got != "Sandbyvägen" {
|
||||||
|
t.Errorf("Fake(%q) = %q, want the named path to run", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "[se.locality") {
|
||||||
|
t.Errorf("%v lists the rows as a Go slice; separate them with commas", err)
|
||||||
|
}
|
||||||
|
if _, err := f.Fake("se.locality[Sandby]"); err == nil || !strings.Contains(err.Error(), "one of L7, L8") {
|
||||||
|
t.Fatalf("se.locality[Sandby] = %v, want its keys listed plainly", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -125,7 +125,7 @@ func checkFunc(body string, fields map[string]node) error {
|
|||||||
return fmt.Errorf("token {%s}: unknown function %q", body, name)
|
return fmt.Errorf("token {%s}: unknown function %q", body, name)
|
||||||
}
|
}
|
||||||
if b.arity >= 0 && len(args) != b.arity {
|
if b.arity >= 0 && len(args) != b.arity {
|
||||||
return fmt.Errorf("token {%s}: %s takes %d args, got %d", body, name, b.arity, len(args))
|
return fmt.Errorf("token {%s}: %s takes %d argument%s, got %d", body, name, b.arity, plural(b.arity), len(args))
|
||||||
}
|
}
|
||||||
if b.check != nil {
|
if b.check != nil {
|
||||||
if err := b.check(fields, args); err != nil {
|
if err := b.check(fields, args); err != nil {
|
||||||
|
|||||||
Vendored
+9
-1
@@ -24,7 +24,7 @@ en_US.itin.serial string
|
|||||||
en_US.last-name format "{name}" key name weight count
|
en_US.last-name format "{name}" key name weight count
|
||||||
en_US.last-name.count string
|
en_US.last-name.count string
|
||||||
en_US.last-name.name string
|
en_US.last-name.name string
|
||||||
en_US.person format "{prefix}{first} {last}" reads en_US.first-name en_US.last-name en_US.sex
|
en_US.person format "{prefix}{first} {last}" reads en_US.first-name en_US.last-name en_US.sex en_US.title
|
||||||
en_US.person.first string
|
en_US.person.first string
|
||||||
en_US.person.last string
|
en_US.person.last string
|
||||||
en_US.person.prefix string
|
en_US.person.prefix string
|
||||||
@@ -48,11 +48,19 @@ en_US.sex.first-name.count
|
|||||||
en_US.sex.first-name.name
|
en_US.sex.first-name.name
|
||||||
en_US.sex.first-name.sex
|
en_US.sex.first-name.sex
|
||||||
en_US.sex.name string
|
en_US.sex.name string
|
||||||
|
en_US.sex.title
|
||||||
|
en_US.sex.title.name
|
||||||
|
en_US.sex.title.sex
|
||||||
|
en_US.sex.title.share
|
||||||
en_US.ssn format "{area}-{group}-{serial}"
|
en_US.ssn format "{area}-{group}-{serial}"
|
||||||
en_US.ssn.area string
|
en_US.ssn.area string
|
||||||
en_US.ssn.group string
|
en_US.ssn.group string
|
||||||
en_US.ssn.serial string
|
en_US.ssn.serial string
|
||||||
en_US.time format "{time('3:04 PM')}"
|
en_US.time format "{time('3:04 PM')}"
|
||||||
|
en_US.title format "{name}" name name weight share parent sex
|
||||||
|
en_US.title.name string
|
||||||
|
en_US.title.sex string
|
||||||
|
en_US.title.share string
|
||||||
en_US.url format "https://{host}{path}"
|
en_US.url format "https://{host}{path}"
|
||||||
en_US.url.host string
|
en_US.url.host string
|
||||||
en_US.url.path string
|
en_US.url.path string
|
||||||
|
|||||||
@@ -164,9 +164,13 @@ address, phone, national id, company and date names each.
|
|||||||
1. Table node, key and name selection, parent links, consistent draws, the
|
1. Table node, key and name selection, parent links, consistent draws, the
|
||||||
choice-of-rows fence, `DATA-LICENSES.md`, `data-import/` — done.
|
choice-of-rows fence, `DATA-LICENSES.md`, `data-import/` — done.
|
||||||
2. `geo/SE` and `geo/US`, and `address` in both locales on top of them — done.
|
2. `geo/SE` and `geo/US`, and `address` in both locales on top of them — done.
|
||||||
3. Weighted person names and valid ids in both locales; `date()` — done. Middle
|
3. Weighted person names and valid ids in both locales; `date()` — done. Left for
|
||||||
names wait for a draw group that shares its family's pins, so a second name
|
later: middle names, and drawing a shipped `personnummer` inside a *selected*
|
||||||
is drawn under the same sex.
|
sex, both of which want a draw group that shares its family's pins — until then
|
||||||
|
the conflict error can name a rewrite that returns a different value when the
|
||||||
|
read it conflicts with sits inside another category. Give the national ids one
|
||||||
|
record shape in step 6, and report a struct column's draw conflict with the path
|
||||||
|
spelling a tag takes rather than the reference spelling.
|
||||||
4. `misc` conversions and the new `misc` tables.
|
4. `misc` conversions and the new `misc` tables.
|
||||||
5. The remaining locale categories: company, phone, finance, vehicle, words.
|
5. The remaining locale categories: company, phone, finance, vehicle, words.
|
||||||
6. Records with building-block columns across the shipped set; shape re-pin.
|
6. Records with building-block columns across the shipped set; shape re-pin.
|
||||||
|
|||||||
Reference in New Issue
Block a user