Compare commits
10 Commits
f210c7a764
...
8cd603a2b1
| Author | SHA1 | Date | |
|---|---|---|---|
| 8cd603a2b1 | |||
| 00a6160aa4 | |||
| 035e51d3e1 | |||
| 31c2f12098 | |||
| 8fa33694b9 | |||
| f0305f191f | |||
| 054f99c94f | |||
| de8c1d13e9 | |||
| ff4b243148 | |||
| 99d8aa0fb7 |
@@ -1,3 +1,4 @@
|
|||||||
.claude
|
.claude
|
||||||
*.out
|
*.out
|
||||||
|
__pycache__/
|
||||||
todo.md
|
todo.md
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Rules
|
||||||
|
|
||||||
|
- Go runs only through `docker compose run --rm <test|vet|fmt|build>`; the merge gate is `docker build .`.
|
||||||
|
- Tests first, in their own commit; the implementation follows in the next. A re-pin of seeded output is its own commit.
|
||||||
|
- One-line commit messages: no ticket prefix, no repo name, no authorship trailers.
|
||||||
|
- Hard tabs. No comment by default; delete a restatement, a rationale, history, or a file preamble.
|
||||||
|
- One spelling per result: reject the other at `New`, and let the error name the spelling to use.
|
||||||
|
- A standing choice a reader would relitigate goes under Decisions in the README, not in a comment.
|
||||||
|
- A README example is a `json` block that loads and renders as a category; `readme_test.go` runs every one.
|
||||||
@@ -1,69 +1,65 @@
|
|||||||
# fejkdata
|
# fejkdata
|
||||||
|
|
||||||
|
A Go library and CLI for generating locale-aware fake data from JSON templates.
|
||||||
Forked from [github.com/Timewave-AB/fakes](https://github.com/Timewave-AB/fakes).
|
Forked from [github.com/Timewave-AB/fakes](https://github.com/Timewave-AB/fakes).
|
||||||
|
|
||||||
A Go library and CLI for generating fake data, built for
|
## Goals
|
||||||
**internationalization**. It exists because the existing Go fake libraries
|
|
||||||
lacked the locale coverage and format control we needed.
|
|
||||||
|
|
||||||
- **Standards first** — formats, names and structures follow international and
|
1. **Valid by construction** — every value passes the check its real consumer
|
||||||
local standards first and foremost.
|
applies; facts that belong together come from one draw, within a value and
|
||||||
- **Locale-aware** — names, addresses, postal codes and phone numbers follow
|
across categories.
|
||||||
per-locale data and formats. The shipped data is organised by full locale tag
|
2. **Text means what it says** — a format renders as written; only `{…}` varies,
|
||||||
(`sv_SE`), but the engine treats folders as plain namespaces — name yours
|
random characters included (`{digits(3)}`). One spelling per result; the wrong
|
||||||
anything.
|
one is a load error naming the right one.
|
||||||
- **Data lives in JSON** — all source data is recursive JSON on disk, read when
|
3. **Every mistake is a load error** — `New` rejects; `Fake` on a loaded generator
|
||||||
you create a generator and then served from memory. Add or change data without
|
fails only for an unknown path.
|
||||||
touching the library. Behavior belongs in data too: the engine grows a
|
4. **Zero to a value in one command** — `go install`, then `fejkdata sv_SE.person`:
|
||||||
built-in function only for what data can't express (a checksum, a time-based
|
no checkout, no flag. Flags are GNU-form (`--seed 42`, `-n 3`) in any position;
|
||||||
id), never for what character classes and choices already do.
|
the first custom template needs no escape and no option.
|
||||||
- **Composable** — templates nest without limit: weighted choices, character
|
5. **Data lives in JSON** — a builtin only for what data can't express.
|
||||||
classes and sub-templates combine to model any format.
|
6. **Reproducible** — seed in, same stream out; no builtin reads a clock.
|
||||||
- **Reproducible** — seed a generator and it emits the same sequence every time,
|
7. **Zero dependencies** — standard library only.
|
||||||
for a given version of the data: changing how a value is composed shifts the
|
8. **Docs index the grammar** — every syntax feature is a heading; every example
|
||||||
stream for that value and for everything drawn after it in the same generator.
|
runs under test and shows its output; a rule is stated once.
|
||||||
Every built-in draws only from that seed — no wall-clock, no `crypto/rand` —
|
|
||||||
so determinism holds end to end.
|
|
||||||
- **Zero dependencies** — standard library only.
|
|
||||||
|
|
||||||
## CLI
|
## CLI
|
||||||
|
|
||||||
Install the `fejkdata` command and give it a path — it prints one value to stdout.
|
|
||||||
The shipped data set is built in; each dot segment descends one level: folders,
|
|
||||||
then the category (a JSON file), then fields inside it.
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go install gitea.larvit.se/larvit/fejkdata/cmd/fejkdata@latest
|
go install gitea.larvit.se/larvit/fejkdata/cmd/fejkdata@latest
|
||||||
|
|
||||||
fejkdata sv_SE.person # Sara Eriksson
|
fejkdata sv_SE.person # Sara Eriksson
|
||||||
fejkdata sv_SE.person.last # Eriksson (dotted path into a category)
|
fejkdata sv_SE.person.last # Eriksson
|
||||||
fejkdata --seed 42 sv_SE.address
|
fejkdata --seed 42 sv_SE.address # the same address every run
|
||||||
fejkdata --repeat 3 sv_SE.person # three values, one per line
|
|
||||||
fejkdata -n 3 --separator ', ' sv_SE.word # nät, barn, sol
|
fejkdata -n 3 --separator ', ' sv_SE.word # nät, barn, sol
|
||||||
fejkdata --list # every path the data offers
|
fejkdata --list # every path the data offers
|
||||||
fejkdata --data-path ./mydata sv_SE.word # layer a dir over the shipped data; last wins a clash
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
Flags are GNU-style: `--name value` or `--name=value`, short aliases `-d`, `-n`,
|
A path names a category, or a field inside one: each dot segment descends one
|
||||||
`-s`, `-h`, in any position; `--` ends the flags. `--data-path` is repeatable
|
level — folders, then the category (a JSON file), then fields.
|
||||||
(last wins a name clash) and `--no-shipped-data` leaves the built-in set out.
|
|
||||||
`--repeat N` renders the path N times — each an independent draw — joined by
|
|
||||||
`--separator` (default a newline, so values land one per line). `--list` prints
|
|
||||||
every path you can ask for; `--version` prints the build version.
|
|
||||||
|
|
||||||
Without installing, run it from a checkout with `go run ./cmd/fejkdata …`. Exit
|
| Flag | |
|
||||||
codes: `0` success (including `--list`, `--version`, `--help`), `1` runtime error
|
|------|--|
|
||||||
(missing dir, unknown path), `2` misuse.
|
| `-d`, `--data-path D` | a directory to layer over the shipped data; repeatable, the last wins a name clash |
|
||||||
|
| `--no-shipped-data` | load only the `--data-path` directories |
|
||||||
|
| `-s`, `--seed N` | reproducible output |
|
||||||
|
| `-n`, `--repeat N` | render the path N times, each an independent draw |
|
||||||
|
| `--separator S` | between repeated values (default a newline) |
|
||||||
|
| `--list` | print every path, then exit |
|
||||||
|
| `--version`, `-h`, `--help` | print, then exit |
|
||||||
|
|
||||||
### Generating a file from a custom template
|
`--name value` and `--name=value` both work (see [Decisions](#decisions)); flags
|
||||||
|
go anywhere, `--` ends them. Exit codes: `0` success, `1` runtime error (missing
|
||||||
|
dir, unknown path), `2` misuse. From a checkout: `go run ./cmd/fejkdata …`.
|
||||||
|
|
||||||
A category is just a JSON file in a data directory, so you can drop in your
|
### Your own data
|
||||||
own and render it — no code change. Save this as `mydata/sql.json`:
|
|
||||||
|
A category is a JSON file in a directory. Save this as `mydata/sql.json`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"format": "INSERT INTO users V#ALUES({sql-username});",
|
"format": "INSERT INTO users VALUES({sql-username});",
|
||||||
"sql-username": {
|
"sql-username": {
|
||||||
"format": "'{username}'",
|
"format": "'{username}'",
|
||||||
"repeat": 3,
|
"repeat": 3,
|
||||||
@@ -73,410 +69,283 @@ own and render it — no code change. Save this as `mydata/sql.json`:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`sql-username` renders `'{username}'` `repeat` times and joins the results with
|
|
||||||
the `),(` separator; the outer `V#ALUES(…)` wraps that into one valid row list.
|
|
||||||
(`#A` escapes the literal `A`, which a format string would otherwise read as a
|
|
||||||
letter token — see [Data format](#data-format).)
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
fejkdata --seed 1 --data-path ./mydata sql
|
fejkdata --seed 1 --data-path ./mydata sql
|
||||||
# INSERT INTO users VALUES('zoom'),('wahoo'),('blip');
|
# INSERT INTO users VALUES('zoom'),('wahoo'),('blip');
|
||||||
```
|
|
||||||
|
|
||||||
Raise the template's `repeat` for more rows per statement; use the CLI's
|
|
||||||
`--repeat` for more statements — together they build a whole seed file:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
fejkdata --repeat 100 --data-path ./mydata sql > seed.sql
|
fejkdata --repeat 100 --data-path ./mydata sql > seed.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
## Library
|
## Library
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go get gitea.larvit.se/larvit/fejkdata # requires Go 1.22+ (for math/rand/v2)
|
go get gitea.larvit.se/larvit/fejkdata # Go 1.22+
|
||||||
```
|
```
|
||||||
|
|
||||||
`New` loads the shipped data set, plus any `WithDataPath` directories layered over
|
|
||||||
it; generate values by path with `Fake`. Each dot segment descends one level:
|
|
||||||
folders, then the category (a JSON file), then fields inside it.
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
f, err := fejkdata.New(fejkdata.WithSeed(42))
|
||||||
|
if err != nil {
|
||||||
import (
|
log.Fatal(err)
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"gitea.larvit.se/larvit/fejkdata"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
f, err := fejkdata.New()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, path := range []string{"sv_SE.person", "sv_SE.address", "sv_SE.phone", "sv_SE.address.locality"} {
|
|
||||||
v, err := f.Fake(path)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
fmt.Printf("%-24s %s\n", path, v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
v, err := f.Fake("sv_SE.address") // "Kungsvägen 68\n379 17 Stockholm"
|
||||||
|
paths := f.List() // every path Fake accepts, sorted
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
| Option | |
|
||||||
sv_SE.person Sara Eriksson
|
|--------|--|
|
||||||
sv_SE.address Kungsvägen 68
|
| `WithSeed(n)` | same seed, same data: identical sequence |
|
||||||
379 17 Stockholm
|
| `WithDataPath(dir)` | layer a directory; repeat to layer several, the last wins a clash |
|
||||||
sv_SE.phone 072-402 91 67
|
| `WithDataFS(fsys)` | layer an `fs.FS`, such as your own `embed.FS` |
|
||||||
sv_SE.address.locality Linköping
|
| `WithoutShippedData()` | load only what you give |
|
||||||
```
|
|
||||||
|
|
||||||
Options: `WithSeed(n)` for a reproducible sequence — same seed + data yields an
|
|
||||||
identical sequence, handy for stable tests; `WithDataPath(dir)` layers a directory
|
|
||||||
(repeat it to layer several, last wins a clash); `WithDataFS(fsys)` layers an
|
|
||||||
`fs.FS`, such as your own `embed.FS`; `WithoutShippedData()` loads only what you
|
|
||||||
give.
|
|
||||||
|
|
||||||
```go
|
|
||||||
a, _ := fejkdata.New(fejkdata.WithSeed(42))
|
|
||||||
b, _ := fejkdata.New(fejkdata.WithSeed(42))
|
|
||||||
av, _ := a.Fake("sv_SE.person")
|
|
||||||
bv, _ := b.Fake("sv_SE.person")
|
|
||||||
av == bv // true
|
|
||||||
```
|
|
||||||
|
|
||||||
`f.List()` returns the sorted paths the loaded data offers — the categories, their
|
|
||||||
dotted fields and folder segments (what the CLI's `--list` prints). Every path it
|
|
||||||
lists renders.
|
|
||||||
|
|
||||||
A `*Generator` is safe for concurrent use; a seeded sequence is reproducible only
|
A `*Generator` is safe for concurrent use; a seeded sequence is reproducible only
|
||||||
when drawn from one goroutine.
|
when drawn from one goroutine. Changing how a value is composed shifts the seeded
|
||||||
|
stream for that value and everything drawn after it.
|
||||||
|
|
||||||
## Data
|
## Data
|
||||||
|
|
||||||
The shipped set under [`data/`](data) — one folder per locale (`en_US`, `sv_SE`)
|
The shipped set under [`data/`](data) — one folder per locale (`en_US`, `sv_SE`)
|
||||||
plus a locale-neutral `misc` folder — is embedded in the library and the CLI, so
|
plus a locale-neutral `misc` folder — is embedded, so the CLI and the library
|
||||||
both work with no data on disk. Layer your own directories over it; a category or
|
work with no data on disk. A directory is a namespace: each JSON file is a
|
||||||
folder name must not use `.`, `|`, `(` or `}` (see [Data format](#data-format)),
|
category named after the file, each subdirectory a dot-path segment, so
|
||||||
and dot-prefixed entries are skipped, so a data directory can also be a checkout.
|
`mydata/sv_SE/person.json` is `sv_SE.person` and replaces the shipped one.
|
||||||
|
Sources merge in order; matching folders combine, any other clash is won by the
|
||||||
|
last loaded. Names may not use `.`, `|`, `(`, `{` or `}`; dot-prefixed entries
|
||||||
|
are skipped, so a data directory can also be a checkout.
|
||||||
|
|
||||||
A directory is just a namespace. Each JSON file is a category named after the
|
Each locale carries `address`, `color`, `company`, `date`, `email`, `ip`,
|
||||||
file; each subdirectory is a dot-path segment — folders nest exactly like JSON
|
|
||||||
objects do. So `mydata/sv_SE/person.json` is `Fake("sv_SE.person")`.
|
|
||||||
|
|
||||||
Sources merge in order: matching folders combine by their children, and any other
|
|
||||||
clash is won by the last one loaded. That lets you override a shipped category
|
|
||||||
without copying the rest:
|
|
||||||
|
|
||||||
```go
|
|
||||||
fejkdata.New(fejkdata.WithDataPath("./mydata")) // mydata/sv_SE/person.json replaces sv_SE.person
|
|
||||||
```
|
|
||||||
|
|
||||||
Each shipped locale carries these categories, formatted per locale (e.g. `date`
|
|
||||||
is `MM/DD/YYYY` in `en_US`, `YYYY-MM-DD` in `sv_SE`; `ssn` is a US SSN vs a
|
|
||||||
Swedish personnummer): `address`, `color`, `company`, `date`, `email`, `ip`,
|
|
||||||
`person`, `phone`, `price`, `sentence`, `ssn`, `time`, `url`, `username`,
|
`person`, `phone`, `price`, `sentence`, `ssn`, `time`, `url`, `username`,
|
||||||
`version`, `word`.
|
`version` and `word`, formatted per locale. `misc` carries `car`, `coordinate`,
|
||||||
|
`country` (ISO 3166), `creditcard` (Luhn-valid), `currency` (ISO 4217), `emoji`,
|
||||||
`data/misc` carries locale-neutral categories — universal data that isn't tied to
|
`httpstatus`, `language` (ISO 639), `mac`, `mimetype`, `objectid`, `timezone`
|
||||||
a language or region:
|
(IANA), `useragent` and `uuid` (v4). Many carry sub-fields — `misc.currency.symbol`,
|
||||||
|
`misc.country.alpha2`, `misc.httpstatus.code` — which `--list` shows.
|
||||||
- ids & networking: `uuid` (a proper random v4), `mac`, `objectid` (24 hex chars,
|
|
||||||
MongoDB ObjectID-shaped — the leading bytes are random, not a real timestamp),
|
|
||||||
`creditcard` (per-network numbers ending in a valid `{luhn()}` digit)
|
|
||||||
- reference codes: `currency` (ISO 4217), `country` (ISO 3166), `language` (ISO
|
|
||||||
639), `timezone` (IANA)
|
|
||||||
- web & systems: `mimetype`, `httpstatus`, `useragent`
|
|
||||||
- misc: `coordinate` (a lat/long point), `emoji`, `car`
|
|
||||||
|
|
||||||
Many carry dotted sub-fields — `currency.symbol`, `country.alpha2`,
|
|
||||||
`mimetype.ext`, `httpstatus.code`, `car.maker`. A time-ordered v7 UUID can't be
|
|
||||||
expressed as data, so it's the `{uuid()}` builtin instead (see **Functions**).
|
|
||||||
|
|
||||||
## Data format
|
## Data format
|
||||||
|
|
||||||
Each JSON file in a data directory is a **category** named after the file
|
Every value is a **node**, nestable without limit:
|
||||||
(`address.json` → `address`), rendered by `Fake("address")`. Drop in a new file
|
|
||||||
or folder — no code change, no recompile.
|
|
||||||
|
|
||||||
Every value is a **node**, one of three shapes, nestable without limit:
|
| Node | JSON | Renders |
|
||||||
|
|
||||||
| Node | JSON | Meaning |
|
|
||||||
|------|------|---------|
|
|------|------|---------|
|
||||||
| literal | `"Malmö"` | emitted verbatim — never formatted |
|
| string | `"Malmö"` | its text, with any `{…}` tokens expanded |
|
||||||
| choice | `["a", "b", …]` | one element, picked at random |
|
| choice | `["a", "b", …]` | one item, picked at random |
|
||||||
| template | `{"format": "…", …}` | a format string plus the named sub-nodes it references |
|
| template | `{"format": "…", …}` | its format, with `{name}` tokens rendering the named fields |
|
||||||
|
|
||||||
**Weight.** A template node may carry a `weight` (default `1`) to skew its odds
|
### Format string
|
||||||
within a choice:
|
|
||||||
|
Every character is literal except a `{…}` token:
|
||||||
|
|
||||||
|
| Token | Renders |
|
||||||
|
|-------|---------|
|
||||||
|
| `{name}` | the sibling field `name` |
|
||||||
|
| `{name.field}` | `field` of one draw of `name` ([Correlated fields](#correlated-fields)) |
|
||||||
|
| `{a\|b}` | one of the named fields, even odds |
|
||||||
|
| `{fn(args)}` | a builtin ([Functions](#functions)) |
|
||||||
|
| `{..path}` | a node reached from the data root ([References](#references)) |
|
||||||
|
| `{{`, `}}` | a literal `{` or `}` |
|
||||||
|
|
||||||
|
```json
|
||||||
|
"100 Main St, Apt {int(1,9)}{upper(1)} — tel {digits(3)}-{digits(4)}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Renders e.g. `100 Main St, Apt 4B — tel 555-0199`. Random characters come from
|
||||||
|
the sample functions, so a phone pattern is `070-{digits(3)} {digits(2)} {digits(2)}`.
|
||||||
|
A lone `}` is a load error naming `}}`; the arms of `{a|b}` must differ, a
|
||||||
|
repeated arm being a second spelling of [weight](#weight).
|
||||||
|
|
||||||
|
### Weight
|
||||||
|
|
||||||
|
An item of a choice may carry a `weight` (default `1`) to skew its odds:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
{ "format": "#070-000 00 00", "weight": 10 },
|
{ "format": "070-{digits(3)} {digits(2)} {digits(2)}", "weight": 10 },
|
||||||
{ "format": "#01-000 00 00" },
|
"08-{digits(3)} {digits(2)} {digits(2)}",
|
||||||
{ "format": "#010-000 00 00" }
|
{ "format": "010-{digits(3)} {digits(2)} {digits(2)}", "weight": 2 }
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
Only template (object) nodes carry `weight` — a bare string or nested array in a
|
Renders `070-412 38 91` ten times as often as `08-…`. A string item is weighted by
|
||||||
choice always counts as `1`. Weights are checked when you create the generator: a
|
writing it as `{ "format": "AB", "weight": 3 }`. Rejected at load: a weight that
|
||||||
negative, non-numeric, or all-zero set is rejected at `New`, so a typo fails
|
is negative, non-numeric, `1` (the default) or outside a choice, a set summing to
|
||||||
fast instead of silently skewing output.
|
zero, and a repeated item — `["a", "a", "b"]` is `[{ "format": "a", "weight": 2 }, "b"]`,
|
||||||
|
and the error says so.
|
||||||
|
|
||||||
**Repeat.** A template node may carry a `repeat` (default `1`) to render its
|
### Repeat
|
||||||
`format` that many times — each render an independent pick — joined by
|
|
||||||
`separator` (default `""`):
|
A template may carry `repeat` (an integer above `1`) to render its format that
|
||||||
|
many times — each an independent draw — joined by `separator` (default `""`):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "format": "{word}", "repeat": 3, "separator": " ", "word": ["foo", "bar", "baz"] }
|
{ "format": "{word}", "repeat": 3, "separator": " ", "word": ["foo", "bar", "baz"] }
|
||||||
```
|
```
|
||||||
|
|
||||||
This yields e.g. `bar foo baz`. `repeat` must be a positive integer and
|
Renders e.g. `bar foo baz`. A `separator` without a `repeat` is rejected.
|
||||||
`separator` a string, both checked at `New`.
|
|
||||||
|
|
||||||
`format`, `weight`, `repeat` and `separator` are the only options; **any other key
|
### Options and fields
|
||||||
is a field**. So write `seperator` and you get a field by that name while the
|
|
||||||
option stays unset. `New` rejects an option that cannot take effect — a
|
|
||||||
`separator` without a `repeat` above 1, a `weight` outside a choice — and a name
|
|
||||||
using a character the grammars reserve: `.` separates the segments of a path, `|`
|
|
||||||
the arms of a token, `(` opens a function call and `}` ends the token, so a name
|
|
||||||
carrying one is a name no format could ever spell. An empty name goes the same
|
|
||||||
way — it is no path segment at all, so `List` never offers it. That holds for a
|
|
||||||
category, a folder and a field alike.
|
|
||||||
|
|
||||||
**Functions.** A `{name()}` token calls a built-in function instead of rendering
|
`format`, `weight`, `repeat` and `separator` are the only options; **any other
|
||||||
a field. `{luhn()}` appends a Luhn check digit over the digits emitted **so far**
|
key is a field** (see [Decisions](#decisions)). An object that does nothing a
|
||||||
in the current format (non-digits skipped but kept); unknown functions or wrong
|
string can't — only a `format` — is rejected naming the string, as is a one-item
|
||||||
argument counts are rejected at `New`. This is what makes a generated Swedish
|
choice naming its item.
|
||||||
personnummer valid — its last digit is a Luhn checksum over the nine before it:
|
|
||||||
|
|
||||||
```json
|
### Functions
|
||||||
{ "format": "00{mmdd}-000{luhn()}", "mmdd": [ … ] }
|
|
||||||
```
|
|
||||||
|
|
||||||
renders e.g. `811218-987`, then `{luhn()}` appends `6` → `811218-9876`. Place it
|
A `{name(args)}` token calls a builtin. Arguments are checked at `New`: a bad
|
||||||
after its payload (it reads what is to its left). The buffer it reads is
|
count, range, country or expression fails fast, and a length, count or decimal
|
||||||
per-expansion, so nesting keeps fixed parts out of the sum — e.g. a 12-digit
|
place beyond a sane maximum is rejected, so a fat-fingered `hex(2000000000)`
|
||||||
form prefixes the century outside the checksummed core:
|
never tries to allocate gigabytes. Every builtin draws only from the seed — a
|
||||||
|
time-based id takes its timestamp from the rng, not the clock — so seeded output
|
||||||
|
stays reproducible.
|
||||||
|
|
||||||
|
| Function | Kind | Renders |
|
||||||
|
|----------|------|---------|
|
||||||
|
| `{luhn()}` | derivation | Luhn check digit over the digits emitted so far in this expansion |
|
||||||
|
| `{mod11()}` | derivation | weighted mod-11 check char (weights 2–7 from the right); `X` for 10 |
|
||||||
|
| `{ean()}` | derivation | EAN-13 / UPC-A / ISBN-13 / GTIN check digit |
|
||||||
|
| `{digits(n)}` | sample | `n` digits 0–9 |
|
||||||
|
| `{upper(n)}`, `{lower(n)}` | sample | `n` letters A–Z, a–z |
|
||||||
|
| `{int(min,max)}` | sample | uniform integer in `[min, max]` |
|
||||||
|
| `{float(min,max,dp)}` | sample | number in `[min, max]` with `dp` decimals |
|
||||||
|
| `{hex(n)}` | sample | `n` lowercase hex digits |
|
||||||
|
| `{base64(n)}` | sample | `n` random bytes, base64 |
|
||||||
|
| `{uuid()}` | sample | UUID v7 (`misc.uuid` ships v4 as data) |
|
||||||
|
| `{ulid()}` | sample | ULID, 26 Crockford base32 chars |
|
||||||
|
| `{nanoid(n)}` | sample | URL-safe Nano ID, `n` chars |
|
||||||
|
| `{iban(CC)}` | sample | length- and mod-97-valid IBAN for BE, DE, DK, ES, FI, NO or SE |
|
||||||
|
| `{seq()}`, `{seq(name)}` | counter | next integer from 1 in this generator; `name` selects an independent counter |
|
||||||
|
| `{calc(expr)}`, `{calc(expr,dp)}` | computation | an arithmetic expression over sibling fields ([Computation](#computation)) |
|
||||||
|
| `{lowercase(x)}`, `{uppercase(x)}`, `{ascii(x)}` | transform | a field's value rewritten ([Transforms](#transforms)) |
|
||||||
|
|
||||||
|
A derivation reads what is to its left, so place it after its payload; the
|
||||||
|
buffer is per expansion, so a nested template keeps fixed parts out of the sum. A
|
||||||
|
Swedish personnummer is a Luhn checksum over the nine digits before it:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "format": "{century}{core}", "century": ["19", "20"],
|
{ "format": "{century}{core}", "century": ["19", "20"],
|
||||||
"core": { "format": "00{mmdd}-000{luhn()}", "mmdd": [ … ] } }
|
"core": { "format": "{digits(2)}{mmdd}-{digits(3)}{luhn()}", "mmdd": ["0115", "0704", "1218"] } }
|
||||||
```
|
```
|
||||||
|
|
||||||
A function must be deterministic (no wall-clock), so a seeded generator stays
|
Renders e.g. `19811218-9876`. `{seq()}` spans `Fake` calls and `repeat`, resets
|
||||||
reproducible. A time-based id (UUID v7, ULID) therefore draws its timestamp from
|
with a new generator, and is the natural primary key for the SQL example above.
|
||||||
the rng, not the clock — the result is a valid, reproducible value, not a real
|
|
||||||
point in time.
|
|
||||||
|
|
||||||
There are four kinds. **Derivations** read the digits emitted so far, so put them
|
### Computation
|
||||||
after their payload; **samples** read only the rng, so they stand alone; one
|
|
||||||
**session counter** (`seq`) advances state held on the generator; and one
|
|
||||||
**computation** (`calc`) evaluates arithmetic over sibling fields. Arguments are
|
|
||||||
validated at `New` (a bad count, range, country, or expression fails fast); a
|
|
||||||
length, count or decimal place beyond a sane maximum is rejected there too, so a
|
|
||||||
fat-fingered `hex(2000000000)` can't try to allocate gigabytes at render.
|
|
||||||
|
|
||||||
| Function | Kind | Emits |
|
`{calc(expr)}` evaluates `+ - * /`, parentheses and unary minus over number
|
||||||
|----------|------|-------|
|
literals and sibling field names, each rendered then read as a number; a second
|
||||||
| `{luhn()}` | derivation | Luhn check digit (mod-10) over preceding digits |
|
argument rounds to that many decimals. A hyphen is always subtraction, so a
|
||||||
| `{mod11()}` | derivation | weighted mod-11 check char (weights 2–7 from the right); `X` when it would be 10 |
|
hyphenated field can't be an operand.
|
||||||
| `{ean()}` | derivation | EAN-13 / UPC-A / ISBN-13 / GTIN check digit |
|
|
||||||
| `{uuid()}` | sample | UUID v7 (v4 ships as data — see [Data](#data)) |
|
|
||||||
| `{ulid()}` | sample | ULID, 26-char Crockford base32 |
|
|
||||||
| `{nanoid(n)}` | sample | URL-safe Nano ID, `n` chars |
|
|
||||||
| `{hex(n)}` | sample | `n` lowercase hex digits |
|
|
||||||
| `{base64(n)}` | sample | `n` random bytes, base64 |
|
|
||||||
| `{int(min,max)}` | sample | uniform integer in `[min, max]` |
|
|
||||||
| `{float(min,max,dp)}` | sample | number in `[min, max]` with `dp` decimals |
|
|
||||||
| `{iban(CC)}` | sample | a length- and mod-97-valid IBAN for country `CC` (BE, DE, DK, ES, FI, NO, SE) |
|
|
||||||
| `{seq()}`, `{seq(name)}` | session counter | next integer (from 1) in this generator's sequence; `name` selects an independent counter |
|
|
||||||
| `{calc(expr)}`, `{calc(expr,dp)}` | computation | value of an arithmetic expression over number literals and sibling fields; `dp` rounds |
|
|
||||||
|
|
||||||
`{ean()}` is also the ISBN-13 check (an ISBN-13 *is* an EAN-13 — build the 978/979
|
|
||||||
prefix in data and call `{ean()}`). `{iban()}` is a sample, not a derivation:
|
|
||||||
an IBAN's check digits sit *before* the account number, which a left-to-right
|
|
||||||
reader can't reach, so it emits the whole value (a generic numeric BBAN — valid
|
|
||||||
length and checksum, not real bank routing).
|
|
||||||
|
|
||||||
`{seq()}`'s counter lives on the generator, so it spans `Fake` calls (and `repeat`)
|
|
||||||
and resets when you build a new generator — `seq` is reproducible by being ordered,
|
|
||||||
not random. It's the natural fit for a primary-key column in the SQL example above.
|
|
||||||
|
|
||||||
**Computation.** `{calc(expr)}` evaluates an arithmetic expression — `+ - * /`,
|
|
||||||
parentheses and unary minus, the usual precedence — and emits the result. Operands
|
|
||||||
are number literals and **sibling field names**, each rendered then read as a
|
|
||||||
number; an optional second arg rounds to that many decimals (`{calc(net * qty, 2)}`),
|
|
||||||
otherwise the value prints in minimal form. A hyphen is always subtraction, so a
|
|
||||||
hyphenated field name can't be an operand. The expression is checked at `New`
|
|
||||||
(parse, and that every name is a real field):
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "format": "{net} x {qty} = {calc(net * qty, 2)}", "net": ["19.99"], "qty": ["3"] }
|
{ "format": "{net} x {qty} = {calc(net * qty, 2)}", "net": ["19.99", "5.00"], "qty": ["3", "7"] }
|
||||||
```
|
```
|
||||||
|
|
||||||
renders `19.99 x 3 = 59.97`. A field a `calc` reads is drawn **once per
|
Renders e.g. `19.99 x 3 = 59.97`: an operand is drawn once per expansion, so the
|
||||||
expansion** and held, so the operand shown is the operand computed — give `net`
|
operand shown is the operand computed. A non-numeric operand yields `NaN` and a
|
||||||
three prices and the line still multiplies the one it printed. The hold covers
|
division by zero `Inf`; both print rather than fail.
|
||||||
every reading of that name in the format, so `{w} {w} {calc(w)}` is one value
|
|
||||||
three times; a name no `calc` reads is unaffected, and `{word} {word}` still draws
|
|
||||||
twice. A field that doesn't render to a number yields `NaN`, and a division by
|
|
||||||
zero yields `Inf`; both print rather than failing the render.
|
|
||||||
|
|
||||||
**References.** A `{..path}` token renders a node from the **data root** instead
|
### Transforms
|
||||||
of a sibling field — the dot path is the one `Fake` takes, resolved across every
|
|
||||||
loaded directory. One category can borrow another, even across folders or layered
|
`{lowercase(x)}`, `{uppercase(x)}` and `{ascii(x)}` rewrite the value of `x` — a
|
||||||
data dirs:
|
field, a path or a `..path` — and nest. `ascii` folds Latin letters (`Åsa Öberg`
|
||||||
|
→ `Asa Oberg`) and drops any other non-ASCII rune. Like a calc operand, `x` is
|
||||||
|
drawn once per expansion, so an email built from a name matches the name beside it:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "format": "Hej, {..en_US.person}!" }
|
{ "format": "{p.first} {p.last} <{lowercase(ascii(p.first))}.{lowercase(ascii(p.last))}@example.com>",
|
||||||
```
|
"p": [
|
||||||
|
{ "format": "{first} {last}", "first": "Åsa", "last": "Öberg" },
|
||||||
renders e.g. `Hej, Pat Smith!`. References are bound when you create the generator, so
|
{ "format": "{first} {last}", "first": "Bo", "last": "Ek" }
|
||||||
a path that is unknown, names a folder, or steps through a multi-variant choice
|
|
||||||
fails at `New`. A reference that leads back to its own value (directly, mutually,
|
|
||||||
or through a chain) is a cycle that would never finish rendering, so it too is
|
|
||||||
rejected at `New`.
|
|
||||||
|
|
||||||
**Correlated fields.** A `{name.field}` token addresses a **path** into a sibling,
|
|
||||||
and a sibling addressed that way is drawn **once per expansion** — so several
|
|
||||||
tokens read one row. That is how two facts that belong together, such as a
|
|
||||||
locality and the postal code that really covers it, stay together:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "format": "{street} {number}\n{place.postal-code} {place.locality}",
|
|
||||||
"place": [
|
|
||||||
{ "format": "{locality}", "locality": "Stockholm", "postal-code": { "format": "#100 00" }, "weight": 975 },
|
|
||||||
{ "format": "{locality}", "locality": "Tranås", "postal-code": { "format": "#5#7#3 00" }, "weight": 18 }
|
|
||||||
] }
|
] }
|
||||||
```
|
```
|
||||||
|
|
||||||
renders e.g. `Kungsgatan 35` / `176 99 Stockholm`, never a Stockholm postal code
|
Renders `Åsa Öberg <asa.oberg@example.com>` or `Bo Ek <bo.ek@example.com>`, never
|
||||||
beside Tranås. Each row carries its own `weight`, so how often a place appears is data
|
a mix.
|
||||||
too. The rule holds both ways: two tails of one head come from the same row, and
|
|
||||||
one path read twice reads one value (`{p.first} … {p.first}@…` gives one name).
|
|
||||||
|
|
||||||
**One draw, one spelling.** The hold above is what a dotted token reads, and a
|
### References
|
||||||
`{calc()}` operand reads its sibling the same way (see **Computation**). A format
|
|
||||||
may not both *render* a level and *read a path into* it — `{p}` beside
|
|
||||||
`{p.first}`, or `{place}` beside `{place.locality}`.
|
|
||||||
Rendering a level expands it afresh while a path reads the level's held draw, so
|
|
||||||
the two would disagree; naming the fields you want is the one spelling that
|
|
||||||
always agrees, and the other is a load error. This covers every way a level can
|
|
||||||
be rendered: a token, a `{calc()}` operand, and a `{..path}` reference — wherever
|
|
||||||
the reference sits, including in a field the format renders.
|
|
||||||
|
|
||||||
A `{calc()}` operand is held on its own terms too, so the same fence guards it:
|
`{..path}` renders a node from the **data root** — the path `Fake` takes, across
|
||||||
`{..cat.net} x 2 = {calc(net * 2, 2)}` names one field two ways and is a load
|
every loaded source — so one category borrows another, even across folders or
|
||||||
error, with no path token anywhere. A calc renders its operand whole, so the hold
|
layered directories:
|
||||||
pins what that render settled, following the operand's plain `{field}` tokens —
|
|
||||||
`{net.v}` and `{..cat.net.v}` are rejected alike. It stops at a `{..path}`, where
|
```json
|
||||||
the operand's own value ends and a shared source begins: two names drawing from
|
"Hej, {..en_US.person}!"
|
||||||
one referenced category are two draws, as `{word} {word}` is, so two dice over
|
```
|
||||||
one `{..die}` are fine.
|
|
||||||
|
Renders e.g. `Hej, Pat Smith!`. A reference into a category is held like a
|
||||||
|
[correlated](#correlated-fields) path — `{..sv_SE.person.first} {..sv_SE.person.last}`
|
||||||
|
name one person, `{lowercase(..sv_SE.person.first)}` reads that same draw — while
|
||||||
|
a bare `{..misc.uuid} {..misc.uuid}` is two draws. Rejected at `New`: a path that
|
||||||
|
is unknown, names a folder, or reads a field not every variant of a choice
|
||||||
|
carries, and a reference that leads back to its own value, directly, mutually or
|
||||||
|
through a chain.
|
||||||
|
|
||||||
|
### Correlated fields
|
||||||
|
|
||||||
|
`{name.field}` reads a path into a sibling, and a sibling read that way is drawn
|
||||||
|
**once per expansion**, so several tokens read one row — a locality and the
|
||||||
|
postal code that really covers it:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "format": "{street} {int(1,99)}\n{place.postal-code} {place.locality}",
|
||||||
|
"street": ["Kungsgatan", "Storgatan"],
|
||||||
|
"place": [
|
||||||
|
{ "format": "{locality}", "locality": "Stockholm", "postal-code": "1{digits(2)} {digits(2)}", "weight": 975 },
|
||||||
|
{ "format": "{locality}", "locality": "Tranås", "postal-code": "573 {digits(2)}", "weight": 18 }
|
||||||
|
] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Renders e.g. `Kungsgatan 35` / `176 99 Stockholm`, never a Stockholm code beside
|
||||||
|
Tranås; each row's `weight` says how often it appears. A path is held at every
|
||||||
|
level it passes through (`{p.geo.town.name} {p.geo.town.zip}` share the town),
|
||||||
|
one path read twice reads one value, and a field no path addresses is drawn each
|
||||||
|
time (`{word} {word}` differs). The hold lasts one expansion: each `repeat`
|
||||||
|
iteration and each nested template draws again. Every variant of a choice on the
|
||||||
|
path must carry the rest of it, so a row missing a field is named at load:
|
||||||
|
|
||||||
|
```text
|
||||||
|
token {place.postal-code}: field "place": not every variant of this 2-way choice carries "postal-code"; all carry [locality]
|
||||||
|
```
|
||||||
|
|
||||||
|
The sub-fields stay addressable — `Fake("address.place.locality")` renders, and
|
||||||
|
`List` advertises it. A path may not read into a level carrying a `repeat`.
|
||||||
|
|
||||||
|
### One draw, one spelling
|
||||||
|
|
||||||
|
A format may not both **render** a level and **read a path into** it — `{p}`
|
||||||
|
beside `{p.first}`, `{..cat.net}` beside `{calc(net * 2)}`, or `{q}` beside
|
||||||
|
`{p.first}` where `q` renders `{..cat.p.last}` — because the render draws afresh
|
||||||
|
while the path reads the held draw, and the two would disagree. Wherever the
|
||||||
|
second route sits, it is a load error naming the spelling to use:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
token {p} renders a level that {p.first} reads a path into; name the fields you want instead
|
token {p} renders a level that {p.first} reads a path into; name the fields you want instead
|
||||||
```
|
```
|
||||||
|
|
||||||
For the same reason a path may not read into a level carrying a `repeat`: it
|
|
||||||
reads one draw, so the repeat could never apply.
|
|
||||||
|
|
||||||
A path is held at **every level it passes through**, not just the first, so the
|
|
||||||
facts can nest as deeply as they belong:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "format": "{p.geo.town.name} {p.geo.town.zip}, {p.geo.region}", "p": [ … ] }
|
|
||||||
```
|
|
||||||
|
|
||||||
renders `Kiruna 98100, Norrbotten` — the town, the zip that covers it and the
|
|
||||||
region it sits in all come from one draw. Two paths part company exactly where
|
|
||||||
they diverge: `{p.a.v}` and `{p.b.v}` share the row and nothing below it.
|
|
||||||
|
|
||||||
The binding lasts for one expansion, so each `repeat` iteration draws again and a
|
|
||||||
nested template keeps its own. A field no dotted token addresses is unaffected:
|
|
||||||
`{word} {word}` still draws twice.
|
|
||||||
|
|
||||||
`New` checks a path the way `Fake` resolves one: every variant of a multi-variant
|
|
||||||
choice must carry the whole path, so a row missing a field is named at load:
|
|
||||||
|
|
||||||
```text
|
|
||||||
token {place.postal-code}: field "place": not every variant of this 2-way choice
|
|
||||||
carries "postal-code"; all carry [locality]
|
|
||||||
```
|
|
||||||
|
|
||||||
The sub-fields stay addressable on their own — `Fake("address.place.locality")`
|
|
||||||
renders, and `List` advertises it.
|
|
||||||
|
|
||||||
**Format string.** Every character is literal except:
|
|
||||||
|
|
||||||
| Token | Expands to |
|
|
||||||
|-------|-----------|
|
|
||||||
| `0` | digit 0–9 |
|
|
||||||
| `1` | digit 1–9 |
|
|
||||||
| `A` | letter A–Z |
|
|
||||||
| `a` | letter a–z |
|
|
||||||
| `#` | escape — the next char is literal (`#0` → `0`, `##` → `#`) |
|
|
||||||
| `{name}` | render the sibling field `name` |
|
|
||||||
| `{name.field}` | render `field` of one draw of the sibling `name` (see **Correlated fields**) |
|
|
||||||
| `{name()}` | call a built-in function (see **Functions**) |
|
|
||||||
| `{..path}` | render the node at a dot path from the data root (see **References**) |
|
|
||||||
|
|
||||||
`{a|b}` renders one of the sibling fields `a` or `b`, chosen at random; an arm
|
|
||||||
may be a `{..path}` reference too (`{name|..en_US.person}`). The arms are picked
|
|
||||||
evenly and must differ — `{a|a|b}` would skew the odds, which is what `weight` is
|
|
||||||
for, so a repeated arm is a load error.
|
|
||||||
|
|
||||||
Inside a `format`, `0 1 A a` are **always** character classes — so a fixed `0`,
|
|
||||||
`1`, `A` or `a` must be escaped (`#1`, `#A`) or it becomes random. A format of
|
|
||||||
`100 Main St` renders e.g. `506 Mdin St` — the `1`, `0`, `0` and `a` were random,
|
|
||||||
the `M`, `in` and `St` were not. A half-fixed string is the trap: `555-0000`
|
|
||||||
keeps `555-` and randomises the last four digits. For a value with no
|
|
||||||
tokens at all, use a bare string node (`"100 Main St"`), emitted verbatim.
|
|
||||||
|
|
||||||
**Putting it together** (`person.json`):
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"format": "{prefix}{femalefirst|malefirst} {last}",
|
|
||||||
"femalefirst": ["Anna", "Astrid", "Elin"],
|
|
||||||
"malefirst": ["Anders", "Erik", "Gustav"],
|
|
||||||
"last": [
|
|
||||||
{ "format": "{first}sson", "first": ["Ander", "Erik", "Karl"] },
|
|
||||||
["Berg", "von Flemming"]
|
|
||||||
],
|
|
||||||
"prefix": [
|
|
||||||
"",
|
|
||||||
{ "format": "{string} ", "string": ["dr", "prof"], "weight": 0.05 }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
This yields e.g. `Anna Eriksson`, `Erik Berg`, or rarely `dr Astrid von Flemming`.
|
|
||||||
Any field is reachable by dotted path — `Fake("person.last")` renders just a
|
|
||||||
surname; choices along the path are resolved at random. A path may continue
|
|
||||||
*through* a choice only where every variant carries the rest of it — every
|
|
||||||
`currency` variant carries `symbol`, so `currency.symbol` resolves — which keeps a
|
|
||||||
path from rendering on one call and failing on the next.
|
|
||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
|
|
||||||
Each file is parsed, validated and weight-indexed once, in `New`. After that a
|
Each file is parsed, validated and weight-indexed once, in `New`. A `Fake` call
|
||||||
`Fake` call costs about what its output costs — it scans the chosen format and
|
then costs about what its output costs: an unweighted pick is O(1) whatever the
|
||||||
renders nested tokens, independent of how large your lists are:
|
list's length, a weighted one O(log n), and long formats, deep nesting and many
|
||||||
|
tokens add cost in proportion to the output.
|
||||||
|
|
||||||
- Picking from a list is **O(1)** whatever its length — a 10-name list and a
|
## Decisions
|
||||||
100 000-name list cost the same.
|
|
||||||
- Giving entries a `weight` makes that list's pick **O(log n)** instead (a
|
- **Options and fields share one namespace.** `format`, `weight`, `repeat` and
|
||||||
search over cumulative weights). Still tiny, but an unweighted list is the
|
`separator` are reserved; every other key is a field. Nesting fields under a
|
||||||
cheapest — only add `weight` where you actually want skew.
|
key, or prefixing options, would tax every template to guard against a
|
||||||
- Long `format` strings, deep nesting and many `{tokens}` add cost in
|
misspelt option.
|
||||||
proportion to the output produced.
|
- **`{a|b}` stays beside nested choices.** `[[…], […]]` picks the same way, but
|
||||||
|
its arms are anonymous; `{femalefirst|malefirst}` keeps `person.femalefirst`
|
||||||
|
addressable.
|
||||||
|
- **`--name value` and `--name=value` both work.** GNU getopt_long convention,
|
||||||
|
which every shell user expects. A single-dash long flag is rejected naming the
|
||||||
|
double-dash spelling.
|
||||||
|
- **The shipped data is embedded, not discovered.** A directory a machine happens
|
||||||
|
to have would make `--seed 42` machine-dependent. Data still lives in `data/`
|
||||||
|
as JSON; `--data-path` layers over it.
|
||||||
|
- **Samples say what they emit, transforms what they do.** `{upper(2)}` is two
|
||||||
|
letters, `{uppercase(x)}` is `x` upper-cased; one name for both would turn on
|
||||||
|
whether the argument looks like a number.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -484,7 +353,7 @@ Everything runs in Docker — **no local tooling beyond Docker is needed**.
|
|||||||
Source is bind-mounted; build caches persist in the `gocache` volume.
|
Source is bind-mounted; build caches persist in the `gocache` volume.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker compose run --rm test # run tests
|
docker compose run --rm test # go test -race
|
||||||
docker compose run --rm cover # tests with coverage
|
docker compose run --rm cover # tests with coverage
|
||||||
docker compose run --rm bench # benchmarks
|
docker compose run --rm bench # benchmarks
|
||||||
docker compose run --rm build # compile the library
|
docker compose run --rm build # compile the library
|
||||||
@@ -506,34 +375,25 @@ gate — vet, format check and tests — so run it locally before pushing:
|
|||||||
```sh
|
```sh
|
||||||
docker build . # latest
|
docker build . # latest
|
||||||
docker build --build-arg GO_VERSION=1.22.12 . # lowest supported
|
docker build --build-arg GO_VERSION=1.22.12 . # lowest supported
|
||||||
```
|
GO_VERSION=1.22.12 docker compose run --rm test # the same tests, without the image build
|
||||||
|
|
||||||
Tests run against the latest Go by default. Set `GO_VERSION` to check the lowest
|
|
||||||
supported version too:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
GO_VERSION=1.22.12 docker compose run --rm test # lowest supported
|
|
||||||
docker compose run --rm test # latest
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
fejkdata.go Generator, New, options, the embedded data set, List
|
fejkdata.go Generator, New, options, the embedded data set, List
|
||||||
node.go the node model and JSON -> node compilation
|
node.go the node model and JSON -> node compilation
|
||||||
render.go Fake and the recursive renderer (choices, format strings, paths, bound draws)
|
render.go Fake and the recursive renderer (choices, format strings, paths, held draws)
|
||||||
template.go the {token} grammar: scanning, function and path tokens, validation
|
template.go the {token} grammar: scanning, arms, operands, validation
|
||||||
reference.go {..path} binding across the tree, the render graph, and the walks over it
|
reference.go {..path} binding across the tree, the render graph, and the walks over it
|
||||||
builtins.go the {name()} function registry and its implementations
|
builtins.go the {name()} function registry and its implementations
|
||||||
calc.go the {calc()} arithmetic evaluator: parser, eval, validation
|
calc.go the {calc()} arithmetic evaluator: parser, eval, validation
|
||||||
data.go data loading: fs.FS folders/files -> namespace tree, multi-source merge
|
data.go data loading: fs.FS folders/files -> namespace tree, multi-source merge
|
||||||
cmd/fejkdata/ the `fejkdata` CLI (New + Fake/List over stdout)
|
cmd/fejkdata/ the fejkdata CLI
|
||||||
data/ shipped data (JSON), embedded at build: locale folders + a misc folder
|
data/ shipped data (JSON), embedded at build: locale folders + a misc folder
|
||||||
|
format-migration/ converters from the pre-release grammar; delete before the first tag
|
||||||
```
|
```
|
||||||
|
|
||||||
To add a category, drop a JSON file into a data directory; to add a locale, add
|
|
||||||
a subdirectory of JSON files.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT — see [LICENSE](LICENSE).
|
MIT — see [LICENSE](LICENSE).
|
||||||
|
|||||||
+5
-10
@@ -43,12 +43,12 @@ func tmpData(b *testing.B, name, body string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkCalc(b *testing.B) {
|
func BenchmarkCalc(b *testing.B) {
|
||||||
dir := tmpData(b, "inv", `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99"],"qty":["3"]}`)
|
dir := tmpData(b, "inv", `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":"19.99","qty":"3"}`)
|
||||||
benchPath(b, dir, "inv")
|
benchPath(b, dir, "inv")
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkLongLiteral(b *testing.B) {
|
func BenchmarkLongLiteral(b *testing.B) {
|
||||||
dir := tmpData(b, "sql", `{"format":"INSERT INTO customers (id, name, city) V#ALUES (#1#2#3, '{word}', '{word}');","word":["alpha","beta","gamma","delta"]}`)
|
dir := tmpData(b, "sql", `{"format":"INSERT INTO customers (id, name, city) VALUES (123, '{word}', '{word}');","word":["alpha","beta","gamma","delta"]}`)
|
||||||
benchPath(b, dir, "sql")
|
benchPath(b, dir, "sql")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,24 +61,19 @@ func BenchmarkRepeat(b *testing.B) {
|
|||||||
// what a correlated pair costs against BenchmarkUnbound, the same output drawn from
|
// what a correlated pair costs against BenchmarkUnbound, the same output drawn from
|
||||||
// two independent fields.
|
// two independent fields.
|
||||||
func BenchmarkBound(b *testing.B) {
|
func BenchmarkBound(b *testing.B) {
|
||||||
dir := tmpData(b, "addr", `{"format":"{place.postal-code} {place.locality}","place":[
|
dir := tmpData(b, "addr", `{"format":"{place.postal-code} {place.locality}","place":[{"format":"{locality}","locality":"Stockholm","postal-code":"1{digits(2)} {digits(2)}"},{"format":"{locality}","locality":"Tranås","postal-code":"573 {digits(2)}"}]}`)
|
||||||
{"format":"{locality}","locality":"Stockholm","postal-code":{"format":"#100 00"}},
|
|
||||||
{"format":"{locality}","locality":"Tranås","postal-code":{"format":"#5#7#3 00"}}]}`)
|
|
||||||
benchPath(b, dir, "addr")
|
benchPath(b, dir, "addr")
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkUnbound(b *testing.B) {
|
func BenchmarkUnbound(b *testing.B) {
|
||||||
dir := tmpData(b, "addr", `{"format":"{postal-code} {locality}",
|
dir := tmpData(b, "addr", `{"format":"{postal-code} {locality}","postal-code":["1{digits(2)} {digits(2)}","573 {digits(2)}"],"locality":["Stockholm","Tranås"]}`)
|
||||||
"postal-code":[{"format":"#100 00"},{"format":"#5#7#3 00"}],
|
|
||||||
"locality":["Stockholm","Tranås"]}`)
|
|
||||||
benchPath(b, dir, "addr")
|
benchPath(b, dir, "addr")
|
||||||
}
|
}
|
||||||
|
|
||||||
// BenchmarkBoundDeep reads two paths through an intermediate level, the shape the
|
// BenchmarkBoundDeep reads two paths through an intermediate level, the shape the
|
||||||
// depth question is about.
|
// depth question is about.
|
||||||
func BenchmarkBoundDeep(b *testing.B) {
|
func BenchmarkBoundDeep(b *testing.B) {
|
||||||
dir := tmpData(b, "addr", `{"format":"{p.addr.city} {p.addr.zip}","p":[
|
dir := tmpData(b, "addr", `{"format":"{p.addr.city} {p.addr.zip}","p":{"format":"{addr}","addr":{"format":"{city}","city":["Stockholm","Tranås"],"zip":"1{digits(2)} {digits(2)}"}}}`)
|
||||||
{"format":"{addr}","addr":{"format":"{city}","city":["Stockholm","Tranås"],"zip":{"format":"#100 00"}}}]}`)
|
|
||||||
benchPath(b, dir, "addr")
|
benchPath(b, dir, "addr")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+45
-59
@@ -15,10 +15,7 @@ import (
|
|||||||
|
|
||||||
// swedishPlaces is a two-variant sibling whose variants pair a locality with the
|
// swedishPlaces is a two-variant sibling whose variants pair a locality with the
|
||||||
// postal-code prefix that really belongs to it.
|
// postal-code prefix that really belongs to it.
|
||||||
const swedishPlaces = `{"format":"%s","place":[
|
const swedishPlaces = `{"format":"%s","place":[{"format":"{locality}","locality":"Stockholm","postal-code":"1{digits(2)} {digits(2)}"},{"format":"{locality}","locality":"Tranås","postal-code":"573 {digits(2)}"}]}`
|
||||||
{"format":"{locality}","locality":"Stockholm","postal-code":{"format":"#100 00"}},
|
|
||||||
{"format":"{locality}","locality":"Tranås","postal-code":{"format":"#5#7#3 00"}}
|
|
||||||
]}`
|
|
||||||
|
|
||||||
// agree reports whether a rendered "postcode locality" pair is a real pairing.
|
// agree reports whether a rendered "postcode locality" pair is a real pairing.
|
||||||
var agree = regexp.MustCompile(`^(1[0-9]{2} [0-9]{2} Stockholm|573 [0-9]{2} Tranås)$`)
|
var agree = regexp.MustCompile(`^(1[0-9]{2} [0-9]{2} Stockholm|573 [0-9]{2} Tranås)$`)
|
||||||
@@ -48,10 +45,10 @@ func TestRenderingALevelAndReadingIntoItIsRejected(t *testing.T) {
|
|||||||
// other rendered — so the pair is a load error rather than a shape whose two
|
// other rendered — so the pair is a load error rather than a shape whose two
|
||||||
// halves can disagree.
|
// halves can disagree.
|
||||||
rejected := map[string]string{
|
rejected := map[string]string{
|
||||||
"bare head beside a path": `{"format":"{p} + {p.first}","p":[{"format":"{first}","first":["Anna","Bo"]}]}`,
|
"bare head beside a path": `{"format":"{p} + {p.first}","p":{"format":"{first}","first":["Anna","Bo"]}}`,
|
||||||
"prefix path beside a path": `{"format":"{p.addr}|{p.addr.city}","p":[{"format":"{addr}","addr":[` +
|
"prefix path beside a path": `{"format":"{p.addr}|{p.addr.city}","p":{"format":"{addr}","addr":[` +
|
||||||
`{"format":"{city}","city":["Kiruna","Boden"]},{"format":"{city}","city":["Malmö","Lund"]}]}]}`,
|
`{"format":"{city}","city":["Kiruna","Boden"]},{"format":"{city}","city":["Malmö","Lund"]}]}}`,
|
||||||
"either order": `{"format":"{p.first} + {p}","p":[{"format":"{first}","first":["Anna","Bo"]}]}`,
|
"either order": `{"format":"{p.first} + {p}","p":{"format":"{first}","first":["Anna","Bo"]}}`,
|
||||||
}
|
}
|
||||||
for name, file := range rejected {
|
for name, file := range rejected {
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file})))
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file})))
|
||||||
@@ -65,7 +62,7 @@ func TestRenderingALevelAndReadingIntoItIsRejected(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// The same path twice is one spelling, so it stays legal.
|
// The same path twice is one spelling, so it stays legal.
|
||||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"cat": `{"format":"{p.first} + {p.first}","p":[{"format":"{first}","first":["Anna","Bo"]}]}`,
|
"cat": `{"format":"{p.first} + {p.first}","p":{"format":"{first}","first":["Anna","Bo"]}}`,
|
||||||
}))); err != nil {
|
}))); err != nil {
|
||||||
t.Errorf("New = %v, want one path read twice accepted", err)
|
t.Errorf("New = %v, want one path read twice accepted", err)
|
||||||
}
|
}
|
||||||
@@ -84,7 +81,7 @@ func TestCalcOperandNamingABoundLevelIsRejected(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// Arithmetic over fields no path names is untouched.
|
// Arithmetic over fields no path names is untouched.
|
||||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"cat": `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99"],"qty":["3"]}`,
|
"cat": `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":"19.99","qty":"3"}`,
|
||||||
}))); err != nil {
|
}))); err != nil {
|
||||||
t.Errorf("New = %v, want plain arithmetic accepted", err)
|
t.Errorf("New = %v, want plain arithmetic accepted", err)
|
||||||
}
|
}
|
||||||
@@ -95,13 +92,13 @@ func TestReferenceNamingABoundLevelIsRejected(t *testing.T) {
|
|||||||
// afresh beside the path that reads its held draw — the same overlap by
|
// afresh beside the path that reads its held draw — the same overlap by
|
||||||
// another spelling.
|
// another spelling.
|
||||||
rejected := map[string]string{
|
rejected := map[string]string{
|
||||||
"reference names the head": `{"format":"{p.first}|{..cat.p}","p":[{"format":"{first}","first":["Anna","Bo"]}]}`,
|
"reference names the head": `{"format":"{p.first}|{..cat.p}","p":{"format":"{first}","first":["Anna","Bo"]}}`,
|
||||||
"reference names the leaf": `{"format":"{p.addr}|{..cat.p.addr}","p":{"format":"x","addr":["A","B","C","D"]}}`,
|
"reference names the leaf": `{"format":"{p.addr}|{..cat.p.addr}","p":{"format":"x","addr":["A","B","C","D"]}}`,
|
||||||
// The reference need not sit in the format that binds: any field it renders
|
// The reference need not sit in the format that binds: any field it renders
|
||||||
// reaches the level just the same, however deep.
|
// reaches the level just the same, however deep.
|
||||||
"reference from a sibling field": `{"format":"{p.first}|{inner}","p":[` +
|
"reference from a sibling field": `{"format":"{p.first}|{inner}","p":[` +
|
||||||
`{"format":"{first}-{last}","first":"A","last":"1"},{"format":"{first}-{last}","first":"B","last":"2"}],` +
|
`{"format":"{first}-{last}","first":"A","last":"1"},{"format":"{first}-{last}","first":"B","last":"2"}],` +
|
||||||
`"inner":{"format":"{..cat.p}"}}`,
|
`"inner":"{..cat.p}"}`,
|
||||||
}
|
}
|
||||||
for name, file := range rejected {
|
for name, file := range rejected {
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file})))
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file})))
|
||||||
@@ -111,7 +108,7 @@ func TestReferenceNamingABoundLevelIsRejected(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// A reference to anything this format does not bind is untouched.
|
// A reference to anything this format does not bind is untouched.
|
||||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"cat": `{"format":"{p.first} {..surname}","p":[{"format":"{first}","first":["Anna","Bo"]}]}`,
|
"cat": `{"format":"{p.first} {..surname}","p":{"format":"{first}","first":["Anna","Bo"]}}`,
|
||||||
"surname": `["Eriksson","Lindqvist"]`,
|
"surname": `["Eriksson","Lindqvist"]`,
|
||||||
}))); err != nil {
|
}))); err != nil {
|
||||||
t.Errorf("New = %v, want a reference outside the bound level accepted", err)
|
t.Errorf("New = %v, want a reference outside the bound level accepted", err)
|
||||||
@@ -123,7 +120,7 @@ func TestCycleReachedOnlyByAPathTokenIsRejected(t *testing.T) {
|
|||||||
// cycle walk has to follow it there. A head whose own format names nothing
|
// cycle walk has to follow it there. A head whose own format names nothing
|
||||||
// would otherwise hide the cycle until render, where it is fatal.
|
// would otherwise hide the cycle until render, where it is fatal.
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"a": `{"format":"{p.x}","p":{"format":"static","x":{"format":"{..a}"}}}`,
|
"a": `{"format":"{p.x}","p":{"format":"static","x":"{..a}"}}`,
|
||||||
})))
|
})))
|
||||||
if err == nil || !strings.Contains(err.Error(), "reference cycle") {
|
if err == nil || !strings.Contains(err.Error(), "reference cycle") {
|
||||||
t.Fatalf("New = %v, want the cycle through {p.x} rejected", err)
|
t.Fatalf("New = %v, want the cycle through {p.x} rejected", err)
|
||||||
@@ -134,7 +131,7 @@ func TestALevelRenderedOnlyByAPathTokenIsHeld(t *testing.T) {
|
|||||||
// {p.a} renders q, so it is a route to the level {q.x} holds — even though p's
|
// {p.a} renders q, so it is a route to the level {q.x} holds — even though p's
|
||||||
// own format names nothing.
|
// own format names nothing.
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"thing": `{"format":"{p.a} {q.x}","p":{"format":"static","a":{"format":"{..thing.q}"}},` +
|
"thing": `{"format":"{p.a} {q.x}","p":{"format":"static","a":"{..thing.q}"},` +
|
||||||
`"q":{"format":"{x}","x":["1","2"]}}`,
|
`"q":{"format":"{x}","x":["1","2"]}}`,
|
||||||
})))
|
})))
|
||||||
if err == nil || !strings.Contains(err.Error(), "reads a path into") {
|
if err == nil || !strings.Contains(err.Error(), "reads a path into") {
|
||||||
@@ -161,7 +158,7 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
"a reference one level down": {
|
"a reference one level down": {
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"cat": `{"format":"{calc(net * 2, 2)} {q}","net":["10.00","20.00"],` +
|
"cat": `{"format":"{calc(net * 2, 2)} {q}","net":["10.00","20.00"],` +
|
||||||
`"q":{"format":"{..cat.net}"}}`,
|
`"q":"{..cat.net}"}`,
|
||||||
},
|
},
|
||||||
`{q} renders "net"`,
|
`{q} renders "net"`,
|
||||||
},
|
},
|
||||||
@@ -169,8 +166,8 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
// so wrapping the operand in one changes nothing.
|
// so wrapping the operand in one changes nothing.
|
||||||
"a reference to an operand wrapped in a choice": {
|
"a reference to an operand wrapped in a choice": {
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"cat": `{"format":"{calc(n * 2, 2)} {q}","n":[{"format":"{v}","v":["1","2"]}],` +
|
"cat": `{"format":"{calc(n * 2, 2)} {q}","n":{"format":"{v}","v":["1","2"]},` +
|
||||||
`"q":{"format":"{..cat.n}"}}`,
|
`"q":"{..cat.n}"}`,
|
||||||
},
|
},
|
||||||
`{q} renders "n"`,
|
`{q} renders "n"`,
|
||||||
},
|
},
|
||||||
@@ -179,7 +176,7 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
"an operand reaching another operand": {
|
"an operand reaching another operand": {
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"cat": `{"format":"{calc(a + b, 0)}","a":{"format":"{x}","x":["1","2"]},` +
|
"cat": `{"format":"{calc(a + b, 0)}","a":{"format":"{x}","x":["1","2"]},` +
|
||||||
`"b":{"format":"{..cat.a}"}}`,
|
`"b":"{..cat.a}"}`,
|
||||||
},
|
},
|
||||||
`calc operand "b" renders "a"`,
|
`calc operand "b" renders "a"`,
|
||||||
},
|
},
|
||||||
@@ -196,7 +193,7 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
"a reference into the operand one level down": {
|
"a reference into the operand one level down": {
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"cat": `{"format":"{calc(net * 2, 2)}|{q}","net":{"format":"{v}","v":["10.00","20.00"]},` +
|
"cat": `{"format":"{calc(net * 2, 2)}|{q}","net":{"format":"{v}","v":["10.00","20.00"]},` +
|
||||||
`"q":{"format":"{..cat.net.v}"}}`,
|
`"q":"{..cat.net.v}"}`,
|
||||||
},
|
},
|
||||||
`{q} renders "net"`,
|
`{q} renders "net"`,
|
||||||
},
|
},
|
||||||
@@ -206,7 +203,7 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
"a violation on the second of two held heads": {
|
"a violation on the second of two held heads": {
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"cat": `{"format":"{calc(a + b, 0)} {w}","a":{"format":"{x}","x":["1","2"]},` +
|
"cat": `{"format":"{calc(a + b, 0)} {w}","a":{"format":"{x}","x":["1","2"]},` +
|
||||||
`"b":{"format":"{y}","y":["3","4"]},"w":{"format":"{..cat.b}"}}`,
|
`"b":{"format":"{y}","y":["3","4"]},"w":"{..cat.b}"}`,
|
||||||
},
|
},
|
||||||
`{w} renders "b"`,
|
`{w} renders "b"`,
|
||||||
},
|
},
|
||||||
@@ -233,20 +230,20 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
"a reference to a sibling the operand never renders": {
|
"a reference to a sibling the operand never renders": {
|
||||||
"cat": `{"format":"{calc(net * 2, 2)} {unit}",` +
|
"cat": `{"format":"{calc(net * 2, 2)} {unit}",` +
|
||||||
`"net":{"format":"{v}","v":["1","2"],"spare":["kg","lb"]},` +
|
`"net":{"format":"{v}","v":["1","2"],"spare":["kg","lb"]},` +
|
||||||
`"unit":{"format":"{..cat.net.spare}"}}`,
|
`"unit":"{..cat.net.spare}"}`,
|
||||||
},
|
},
|
||||||
// Two operands drawing from one source are two names, so two draws: each is
|
// Two operands drawing from one source are two names, so two draws: each is
|
||||||
// held under its own name and shown once, and neither can disagree.
|
// held under its own name and shown once, and neither can disagree.
|
||||||
"two operands sharing one source": {
|
"two operands sharing one source": {
|
||||||
"die": `["1","2","3","4","5","6"]`,
|
"die": `["1","2","3","4","5","6"]`,
|
||||||
"cat": `{"format":"{d1} + {d2} = {calc(d1 + d2, 0)}","d1":{"format":"{..die}"},` +
|
"cat": `{"format":"{d1} + {d2} = {calc(d1 + d2, 0)}","d1":"{..die}",` +
|
||||||
`"d2":{"format":"{..die}"}}`,
|
`"d2":"{..die}"}`,
|
||||||
},
|
},
|
||||||
// Likewise a reference drawing from what the operand draws from: {..common}
|
// Likewise a reference drawing from what the operand draws from: {..common}
|
||||||
// and net are two names, not two spellings of one field.
|
// and net are two names, not two spellings of one field.
|
||||||
"a reference to what an operand renders through": {
|
"a reference to what an operand renders through": {
|
||||||
"common": `["1","2"]`,
|
"common": `["1","2"]`,
|
||||||
"cat": `{"format":"{calc(net * 2, 2)} {..common}","net":{"format":"{..common}"}}`,
|
"cat": `{"format":"{calc(net * 2, 2)} {..common}","net":"{..common}"}`,
|
||||||
},
|
},
|
||||||
// A fixed string cannot disagree with itself, so it needs no fence.
|
// A fixed string cannot disagree with itself, so it needs no fence.
|
||||||
"a literal operand named twice": {
|
"a literal operand named twice": {
|
||||||
@@ -272,11 +269,11 @@ func TestAPathReachesEveryVariantItMightDraw(t *testing.T) {
|
|||||||
// to a held level disagrees silently.
|
// to a held level disagrees silently.
|
||||||
rejected := map[string]struct{ file, want string }{
|
rejected := map[string]struct{ file, want string }{
|
||||||
"a cycle in a later variant": {
|
"a cycle in a later variant": {
|
||||||
`{"format":"{p.x}","p":[{"format":"h","x":"safe"},{"format":"h","x":{"format":"{..cat}"}}]}`,
|
`{"format":"{p.x}","p":[{"format":"h","x":"safe"},{"format":"h","x":"{..cat}"}]}`,
|
||||||
"reference cycle",
|
"reference cycle",
|
||||||
},
|
},
|
||||||
"a second route in a later variant": {
|
"a second route in a later variant": {
|
||||||
`{"format":"{p.x} {q.y}","p":[{"format":"h","x":"safe"},{"format":"h","x":{"format":"{..cat.q}"}}],` +
|
`{"format":"{p.x} {q.y}","p":[{"format":"h","x":"safe"},{"format":"h","x":"{..cat.q}"}],` +
|
||||||
`"q":{"format":"{y}","y":["1","2"]}}`,
|
`"q":{"format":"{y}","y":["1","2"]}}`,
|
||||||
"reads a path into",
|
"reads a path into",
|
||||||
},
|
},
|
||||||
@@ -309,10 +306,10 @@ func TestALevelAPathNeverRendersIsAccepted(t *testing.T) {
|
|||||||
func TestADeepDiamondChainLoads(t *testing.T) {
|
func TestADeepDiamondChainLoads(t *testing.T) {
|
||||||
// A diamond chain is 2^n routes through n nodes. The search past a bound level
|
// A diamond chain is 2^n routes through n nodes. The search past a bound level
|
||||||
// must walk each node once, not once per route, or a deep chain never loads.
|
// must walk each node once, not once per route, or a deep chain never loads.
|
||||||
files := map[string]string{"l0": `["x"]`}
|
files := map[string]string{"l0": `"x"`}
|
||||||
for i := 1; i <= 30; i++ {
|
for i := 1; i <= 30; i++ {
|
||||||
files[fmt.Sprintf("l%d", i)] = fmt.Sprintf(
|
files[fmt.Sprintf("l%d", i)] = fmt.Sprintf(
|
||||||
`{"format":"{a}{b}","a":{"format":"{..l%d}"},"b":{"format":"{..l%d}"}}`, i-1, i-1)
|
`{"format":"{a}{b}","a":"{..l%d}","b":"{..l%d}"}`, i-1, i-1)
|
||||||
}
|
}
|
||||||
files["thing"] = `{"format":"{p.first} {..l30}","p":{"format":"x","first":["A","B"]}}`
|
files["thing"] = `{"format":"{p.first} {..l30}","p":{"format":"x","first":["A","B"]}}`
|
||||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err != nil {
|
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err != nil {
|
||||||
@@ -325,9 +322,9 @@ func TestASharedNodeIsWalkedOnce(t *testing.T) {
|
|||||||
// The search past a bound level must take that in its stride rather than walk
|
// The search past a bound level must take that in its stride rather than walk
|
||||||
// the shared node once per route.
|
// the shared node once per route.
|
||||||
f, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
f, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"cat": `{"format":"{p.first}|{q}","p":[{"format":"{first}","first":["Anna","Bo"]}],` +
|
"cat": `{"format":"{p.first}|{q}","p":{"format":"{first}","first":["Anna","Bo"]},` +
|
||||||
`"q":{"format":"{a}{b}","a":{"format":"{..shared}"},"b":{"format":"{..shared}"}}}`,
|
`"q":{"format":"{a}{b}","a":"{..shared}","b":"{..shared}"}}`,
|
||||||
"shared": `["x"]`,
|
"shared": `"x"`,
|
||||||
})), WithSeed(1))
|
})), WithSeed(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("New = %v, want a shared node accepted", err)
|
t.Fatalf("New = %v, want a shared node accepted", err)
|
||||||
@@ -342,7 +339,7 @@ func TestTheEarlierReaderIsNamed(t *testing.T) {
|
|||||||
// first is the one reported, so the error points at the same place a reader
|
// first is the one reported, so the error points at the same place a reader
|
||||||
// looking at the format would start.
|
// looking at the format would start.
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"cat": `{"format":"{calc(p * 1)} {p} {p.first}","p":[{"format":"{first}","first":["1","2"]}]}`,
|
"cat": `{"format":"{calc(p * 1)} {p} {p.first}","p":{"format":"{first}","first":["1","2"]}}`,
|
||||||
})))
|
})))
|
||||||
if err == nil || !strings.Contains(err.Error(), `calc operand "p"`) {
|
if err == nil || !strings.Contains(err.Error(), `calc operand "p"`) {
|
||||||
t.Fatalf("New = %v, want the calc operand named, being written first", err)
|
t.Fatalf("New = %v, want the calc operand named, being written first", err)
|
||||||
@@ -368,7 +365,7 @@ func TestNestedChoiceDrawsOneVariant(t *testing.T) {
|
|||||||
f := engine(21)
|
f := engine(21)
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
seen[mustRender(t, f, `{"format":"{p.x}","p":[[{"format":"{x}","x":"1"}],[{"format":"{x}","x":"2"}]]}`)] = true
|
seen[mustRender(t, f, `{"format":"{p.x}","p":[{"format":"{x}","x":"1"},{"format":"{x}","x":"2"}]}`)] = true
|
||||||
}
|
}
|
||||||
if !seen["1"] || !seen["2"] || len(seen) != 2 {
|
if !seen["1"] || !seen["2"] || len(seen) != 2 {
|
||||||
t.Fatalf("200 draws produced %v, want 1 and 2", seen)
|
t.Fatalf("200 draws produced %v, want 1 and 2", seen)
|
||||||
@@ -379,7 +376,7 @@ func TestRepeatingLevelIsNamedInTheError(t *testing.T) {
|
|||||||
// The level carrying the repeat is the one to fix, so the error names it
|
// The level carrying the repeat is the one to fix, so the error names it
|
||||||
// rather than the head the path started from.
|
// rather than the head the path started from.
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"cat": `{"format":"[{p.a.b}]","p":{"format":"{a}","a":{"format":"{b}","repeat":3,"separator":",","b":["z"]}}}`,
|
"cat": `{"format":"[{p.a.b}]","p":{"format":"{a}","a":{"format":"{b}","repeat":3,"separator":",","b":"z"}}}`,
|
||||||
})))
|
})))
|
||||||
if err == nil || !strings.Contains(err.Error(), `"p.a"`) {
|
if err == nil || !strings.Contains(err.Error(), `"p.a"`) {
|
||||||
t.Fatalf("New = %v, want it to name the level p.a that carries the repeat", err)
|
t.Fatalf("New = %v, want it to name the level p.a that carries the repeat", err)
|
||||||
@@ -391,7 +388,7 @@ func TestPathIntoARepeatingLevelBehindAChoiceIsRejected(t *testing.T) {
|
|||||||
// reach inside one — otherwise the direct spelling is a load error and the same
|
// reach inside one — otherwise the direct spelling is a load error and the same
|
||||||
// mistake behind a choice silently drops the repeat.
|
// mistake behind a choice silently drops the repeat.
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"cat": `{"format":"[{p.a}]","p":[{"format":"{a}","repeat":3,"separator":",","a":["x"]},{"format":"{a}","a":["y"]}]}`,
|
"cat": `{"format":"[{p.a}]","p":[{"format":"{a}","repeat":3,"separator":",","a":"x"},{"format":"{a}","a":"y"}]}`,
|
||||||
})))
|
})))
|
||||||
if err == nil || !strings.Contains(err.Error(), "repeat") {
|
if err == nil || !strings.Contains(err.Error(), "repeat") {
|
||||||
t.Fatalf("New = %v, want the repeat behind a choice rejected", err)
|
t.Fatalf("New = %v, want the repeat behind a choice rejected", err)
|
||||||
@@ -402,7 +399,7 @@ func TestPathIntoARepeatingLevelIsRejected(t *testing.T) {
|
|||||||
// A path reads one level's draw, so it can never apply that level's repeat.
|
// A path reads one level's draw, so it can never apply that level's repeat.
|
||||||
// The engine rejects options that cannot take effect, and this is one.
|
// The engine rejects options that cannot take effect, and this is one.
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"cat": `{"format":"[{p.a}]","p":{"format":"{a}","repeat":3,"separator":",","a":["z"]}}`,
|
"cat": `{"format":"[{p.a}]","p":{"format":"{a}","repeat":3,"separator":",","a":"z"}}`,
|
||||||
})))
|
})))
|
||||||
if err == nil || !strings.Contains(err.Error(), "repeat") {
|
if err == nil || !strings.Contains(err.Error(), "repeat") {
|
||||||
t.Fatalf("New = %v, want a path into a repeating level rejected", err)
|
t.Fatalf("New = %v, want a path into a repeating level rejected", err)
|
||||||
@@ -511,7 +508,7 @@ func TestDottedTokenErrors(t *testing.T) {
|
|||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
"unknown head": {
|
"unknown head": {
|
||||||
`{"format":"{nope.x}","place":[{"format":"{v}","v":"A"}]}`,
|
`{"format":"{nope.x}","place":{"format":"{v}","v":"A"}}`,
|
||||||
`no field "nope"`,
|
`no field "nope"`,
|
||||||
},
|
},
|
||||||
"unknown tail": {
|
"unknown tail": {
|
||||||
@@ -519,7 +516,7 @@ func TestDottedTokenErrors(t *testing.T) {
|
|||||||
`"nope"`,
|
`"nope"`,
|
||||||
},
|
},
|
||||||
"tail only some variants carry": {
|
"tail only some variants carry": {
|
||||||
`{"format":"{place.locality}","place":[{"format":"{locality}","locality":"A"},{"format":"x"}]}`,
|
`{"format":"{place.locality}","place":[{"format":"{locality}","locality":"A"},"x"]}`,
|
||||||
`locality`,
|
`locality`,
|
||||||
},
|
},
|
||||||
"path into a literal": {
|
"path into a literal": {
|
||||||
@@ -527,7 +524,7 @@ func TestDottedTokenErrors(t *testing.T) {
|
|||||||
`"y"`,
|
`"y"`,
|
||||||
},
|
},
|
||||||
"dotted field key": {
|
"dotted field key": {
|
||||||
`{"format":"[{a.b}]","a.b":["V"]}`,
|
`{"format":"[{a.b}]","a.b":"V"}`,
|
||||||
`contains "."`,
|
`contains "."`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -548,8 +545,7 @@ func TestSamePathReadTwiceReadsOneValue(t *testing.T) {
|
|||||||
// what a name shown in a display form and again in an address needs.
|
// what a name shown in a display form and again in an address needs.
|
||||||
f := engine(7)
|
f := engine(7)
|
||||||
for i := 0; i < 300; i++ {
|
for i := 0; i < 300; i++ {
|
||||||
got := mustRender(t, f, `{"format":"{p.first}|{p.first}",
|
got := mustRender(t, f, `{"format":"{p.first}|{p.first}","p":{"format":"{first}","first":["Anna","Astrid","Elin","Karin"]}}`)
|
||||||
"p":[{"format":"{first}","first":["Anna","Astrid","Elin","Karin"]}]}`)
|
|
||||||
parts := strings.Split(got, "|")
|
parts := strings.Split(got, "|")
|
||||||
if parts[0] != parts[1] {
|
if parts[0] != parts[1] {
|
||||||
t.Fatalf("draw %d = %q, want one value read twice", i, got)
|
t.Fatalf("draw %d = %q, want one value read twice", i, got)
|
||||||
@@ -571,9 +567,7 @@ func TestDifferentTailsUnderOneHeadShareTheRow(t *testing.T) {
|
|||||||
|
|
||||||
// deepPlaces nests the correlated facts a level down: the row holds an addr, and
|
// deepPlaces nests the correlated facts a level down: the row holds an addr, and
|
||||||
// the addr holds the city and the zip that belongs to it.
|
// the addr holds the city and the zip that belongs to it.
|
||||||
const deepPlaces = `{"format":"%s","p":[{"format":"{addr}","addr":[
|
const deepPlaces = `{"format":"%s","p":{"format":"{addr}","addr":[{"format":"{city}","city":"Stockholm","zip":"11111"},{"format":"{city}","city":"Kiruna","zip":"98100"}]}}`
|
||||||
{"format":"{city}","city":"Stockholm","zip":"11111"},
|
|
||||||
{"format":"{city}","city":"Kiruna","zip":"98100"}]}]}`
|
|
||||||
|
|
||||||
func TestPathHoldsItsDrawAtEveryLevel(t *testing.T) {
|
func TestPathHoldsItsDrawAtEveryLevel(t *testing.T) {
|
||||||
// A choice below the head is drawn once too, so two paths sharing a prefix
|
// A choice below the head is drawn once too, so two paths sharing a prefix
|
||||||
@@ -595,13 +589,7 @@ func TestPathHoldsItsDrawAtEveryLevel(t *testing.T) {
|
|||||||
func TestPathHoldsItsDrawThreeLevelsDown(t *testing.T) {
|
func TestPathHoldsItsDrawThreeLevelsDown(t *testing.T) {
|
||||||
// The rule does not run out at two: every level a path passes through is held.
|
// The rule does not run out at two: every level a path passes through is held.
|
||||||
f := engine(12)
|
f := engine(12)
|
||||||
tmpl := `{"format":"{p.geo.town.name}/{p.geo.town.zip} {p.geo.region}","p":[{"format":"{geo}","geo":[
|
tmpl := `{"format":"{p.geo.town.name}/{p.geo.town.zip} {p.geo.region}","p":{"format":"{geo}","geo":[{"format":"{town}","region":"Norrbotten","town":[{"format":"{name}","name":"Kiruna","zip":"98100"},{"format":"{name}","name":"Luleå","zip":"97200"}]},{"format":"{town}","region":"Skåne","town":[{"format":"{name}","name":"Malmö","zip":"21100"},{"format":"{name}","name":"Lund","zip":"22100"}]}]}}`
|
||||||
{"format":"{town}","region":"Norrbotten","town":[
|
|
||||||
{"format":"{name}","name":"Kiruna","zip":"98100"},
|
|
||||||
{"format":"{name}","name":"Luleå","zip":"97200"}]},
|
|
||||||
{"format":"{town}","region":"Skåne","town":[
|
|
||||||
{"format":"{name}","name":"Malmö","zip":"21100"},
|
|
||||||
{"format":"{name}","name":"Lund","zip":"22100"}]}]}]}`
|
|
||||||
want := map[string]bool{
|
want := map[string]bool{
|
||||||
"Kiruna/98100 Norrbotten": true, "Luleå/97200 Norrbotten": true,
|
"Kiruna/98100 Norrbotten": true, "Luleå/97200 Norrbotten": true,
|
||||||
"Malmö/21100 Skåne": true, "Lund/22100 Skåne": true,
|
"Malmö/21100 Skåne": true, "Lund/22100 Skåne": true,
|
||||||
@@ -641,9 +629,7 @@ func TestDeepPathsUnderOneHeadStayIndependentWhereTheyDiverge(t *testing.T) {
|
|||||||
// Two paths share only what they actually share: a common prefix is one draw,
|
// Two paths share only what they actually share: a common prefix is one draw,
|
||||||
// and each keeps its own draw past the point they part.
|
// and each keeps its own draw past the point they part.
|
||||||
f := engine(14)
|
f := engine(14)
|
||||||
tmpl := `{"format":"{p.a.v}{p.b.v}","p":[{"format":"x",
|
tmpl := `{"format":"{p.a.v}{p.b.v}","p":{"format":"x","a":[{"format":"{v}","v":"1"},{"format":"{v}","v":"2"}],"b":[{"format":"{v}","v":"1"},{"format":"{v}","v":"2"}]}}`
|
||||||
"a":[{"format":"{v}","v":"1"},{"format":"{v}","v":"2"}],
|
|
||||||
"b":[{"format":"{v}","v":"1"},{"format":"{v}","v":"2"}]}]}`
|
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for i := 0; i < 400; i++ {
|
for i := 0; i < 400; i++ {
|
||||||
seen[mustRender(t, f, tmpl)] = true
|
seen[mustRender(t, f, tmpl)] = true
|
||||||
@@ -660,9 +646,9 @@ func TestEmptyPathSegmentIsRejected(t *testing.T) {
|
|||||||
// nothing is reported as that rather than as a missing field. An empty name is
|
// nothing is reported as that rather than as a missing field. An empty name is
|
||||||
// rejected where it is authored, so no data can make these resolve.
|
// rejected where it is authored, so no data can make these resolve.
|
||||||
rejected := map[string]string{
|
rejected := map[string]string{
|
||||||
"trailing dot": `{"format":"[{a.}]","a":{"format":"x"}}`,
|
"trailing dot": `{"format":"[{a.}]","a":"x"}`,
|
||||||
"leading dot": `{"format":"[{.b}]","a":{"format":"{b}","b":["V"]}}`,
|
"leading dot": `{"format":"[{.b}]","a":{"format":"{b}","b":"V"}}`,
|
||||||
"double dot": `{"format":"[{a..b}]","a":{"format":"x"}}`,
|
"double dot": `{"format":"[{a..b}]","a":"x"}`,
|
||||||
}
|
}
|
||||||
for name, file := range rejected {
|
for name, file := range rejected {
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file})))
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file})))
|
||||||
@@ -681,7 +667,7 @@ func TestCycleThroughAPathTokenIsRejected(t *testing.T) {
|
|||||||
// must be caught at New. Reaching render would be fatal: the recursion never
|
// must be caught at New. Reaching render would be fatal: the recursion never
|
||||||
// terminates, and a stack overflow cannot be recovered.
|
// terminates, and a stack overflow cannot be recovered.
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"a": `{"format":"{p.x}","p":{"format":"{x}","x":{"format":"{..a}"}}}`,
|
"a": `{"format":"{p.x}","p":{"format":"{x}","x":"{..a}"}}`,
|
||||||
})))
|
})))
|
||||||
if err == nil || !strings.Contains(err.Error(), "reference cycle") {
|
if err == nil || !strings.Contains(err.Error(), "reference cycle") {
|
||||||
t.Fatalf("New = %v, want the cycle through {p.x} rejected", err)
|
t.Fatalf("New = %v, want the cycle through {p.x} rejected", err)
|
||||||
|
|||||||
+124
-38
@@ -6,6 +6,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
// maxLen caps sample output lengths (hex, nanoid, base64) and maxDecimals caps
|
// maxLen caps sample output lengths (hex, nanoid, base64) and maxDecimals caps
|
||||||
@@ -16,27 +17,21 @@ const (
|
|||||||
maxDecimals = 1024
|
maxDecimals = 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
// builtins is the registry of {name(args)} functions. Derivations read the
|
// builtins is the registry of {name(args)} functions. Derivations read the digits
|
||||||
// digits emitted so far in the current expansion (luhn, mod11, ean — place them
|
// emitted so far in the current expansion (place them after their payload);
|
||||||
// after their payload); samples read only the rng (uuid, ulid, ...). All
|
// samples read only the rng. A time-based id (uuid v7, ulid) draws its timestamp
|
||||||
// must stay pure over (rng, emitted, args) so seeded output is reproducible — a
|
// from the rng, not the wall clock, so seeded output stays reproducible.
|
||||||
// time-based id (uuid v7, ulid) draws its timestamp from the rng, not the wall
|
|
||||||
// clock. Add a builtin only for what data can't express: a random v4 UUID and a
|
|
||||||
// 24-hex ObjectID both ship as data, so the uuid builtin is v7.
|
|
||||||
var builtins = map[string]builtin{
|
var builtins = map[string]builtin{
|
||||||
"luhn": {arity: 0, prep: derive(func(e string) string { return string(rune('0' + luhnCheck(e))) })},
|
"luhn": {arity: 0, prep: derive(func(e string) string { return string(rune('0' + luhnCheck(e))) })},
|
||||||
"mod11": {arity: 0, prep: derive(mod11Check)},
|
"mod11": {arity: 0, prep: derive(mod11Check)},
|
||||||
"ean": {arity: 0, prep: derive(eanCheck)},
|
"ean": {arity: 0, prep: derive(eanCheck)},
|
||||||
"uuid": {arity: 0, prep: sample(uuidV7)},
|
"uuid": {arity: 0, prep: sample(uuidV7)},
|
||||||
"ulid": {arity: 0, prep: sample(ulid)},
|
"ulid": {arity: 0, prep: sample(ulid)},
|
||||||
"nanoid": {arity: 1, check: posIntArg, prep: func(a []string) callFn {
|
"nanoid": {arity: 1, check: posIntArg, prep: chars(nanoidAlphabet)},
|
||||||
n := atoi(a[0])
|
"hex": {arity: 1, check: posIntArg, prep: chars(hexDigits)},
|
||||||
return func(s *session, _ string, _ []string) string { return nanoid(s, n) }
|
"digits": {arity: 1, check: posIntArg, prep: chars("0123456789")},
|
||||||
}},
|
"upper": {arity: 1, check: posIntArg, prep: chars("ABCDEFGHIJKLMNOPQRSTUVWXYZ")},
|
||||||
"hex": {arity: 1, check: posIntArg, prep: func(a []string) callFn {
|
"lower": {arity: 1, check: posIntArg, prep: chars("abcdefghijklmnopqrstuvwxyz")},
|
||||||
n := atoi(a[0])
|
|
||||||
return func(s *session, _ string, _ []string) string { return randHex(s, n) }
|
|
||||||
}},
|
|
||||||
"base64": {arity: 1, check: posIntArg, prep: func(a []string) callFn {
|
"base64": {arity: 1, check: posIntArg, prep: func(a []string) callFn {
|
||||||
n := atoi(a[0])
|
n := atoi(a[0])
|
||||||
return func(s *session, _ string, _ []string) string {
|
return func(s *session, _ string, _ []string) string {
|
||||||
@@ -57,9 +52,10 @@ var builtins = map[string]builtin{
|
|||||||
cc := a[0]
|
cc := a[0]
|
||||||
return func(s *session, _ string, _ []string) string { return iban(s, cc) }
|
return func(s *session, _ string, _ []string) string { return iban(s, cc) }
|
||||||
}},
|
}},
|
||||||
// calc is the one builtin that names operands (expand reads them for it);
|
"calc": {arity: -1, check: checkCalc, prep: calcPrep, operands: calcOperands},
|
||||||
// every other ignores them. 1 or 2 args: the expression and an optional decimals.
|
"lowercase": {arity: 1, check: transformArg, prep: transformPrep(strings.ToLower), operands: transformOperand},
|
||||||
"calc": {arity: -1, check: checkCalc, prep: calcPrep},
|
"uppercase": {arity: 1, check: transformArg, prep: transformPrep(strings.ToUpper), operands: transformOperand},
|
||||||
|
"ascii": {arity: 1, check: transformArg, prep: transformPrep(asciiFold), operands: transformOperand},
|
||||||
// seq is the one stateful builtin: a per-session counter from 1, advancing on
|
// seq is the one stateful builtin: a per-session counter from 1, advancing on
|
||||||
// each call. An optional name selects an independent counter; no name uses the
|
// each call. An optional name selects an independent counter; no name uses the
|
||||||
// default one. Deterministic by construction, so seeded output stays stable.
|
// default one. Deterministic by construction, so seeded output stays stable.
|
||||||
@@ -74,9 +70,9 @@ var builtins = map[string]builtin{
|
|||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
// derive and sample are the two argument-free builtin shapes the README names: a
|
// derive and sample are the two argument-free builtin shapes: a derivation reads
|
||||||
// derivation reads the output emitted so far, a sample reads only the rng. Each
|
// the output emitted so far, a sample reads only the rng. chars is the shape of a
|
||||||
// lifts that one function into the prep every registry entry supplies.
|
// sample of n characters drawn from an alphabet.
|
||||||
func derive(f func(emitted string) string) func([]string) callFn {
|
func derive(f func(emitted string) string) func([]string) callFn {
|
||||||
return func([]string) callFn {
|
return func([]string) callFn {
|
||||||
return func(_ *session, emitted string, _ []string) string { return f(emitted) }
|
return func(_ *session, emitted string, _ []string) string { return f(emitted) }
|
||||||
@@ -89,8 +85,107 @@ func sample(f func(rng) string) func([]string) callFn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func chars(alphabet string) func([]string) callFn {
|
||||||
|
return func(a []string) callFn {
|
||||||
|
n := atoi(a[0])
|
||||||
|
return func(s *session, _ string, _ []string) string { return randChars(s, n, alphabet) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const hexDigits = "0123456789abcdef"
|
const hexDigits = "0123456789abcdef"
|
||||||
|
|
||||||
|
// transforms are the builtins that rewrite one operand's value; they nest, so
|
||||||
|
// {lowercase(ascii(x))} folds then lowers.
|
||||||
|
var transforms = map[string]func(string) string{
|
||||||
|
"ascii": asciiFold,
|
||||||
|
"lowercase": strings.ToLower,
|
||||||
|
"uppercase": strings.ToUpper,
|
||||||
|
}
|
||||||
|
|
||||||
|
// unwrapTransform peels nested transform calls off an operand arg, returning the
|
||||||
|
// field it finally names and the transforms to apply, innermost last.
|
||||||
|
func unwrapTransform(arg string) (leaf string, chain []func(string) string, err error) {
|
||||||
|
for {
|
||||||
|
name, args, isCall := funcCall(arg)
|
||||||
|
if !isCall {
|
||||||
|
return arg, chain, nil
|
||||||
|
}
|
||||||
|
fn, isTransform := transforms[name]
|
||||||
|
if !isTransform {
|
||||||
|
return "", nil, fmt.Errorf("%s(%s) is not a transform, so it cannot be an operand", name, strings.Join(args, ","))
|
||||||
|
}
|
||||||
|
if len(args) != 1 {
|
||||||
|
return "", nil, fmt.Errorf("%s takes 1 arg, got %d", name, len(args))
|
||||||
|
}
|
||||||
|
chain = append(chain, fn)
|
||||||
|
arg = args[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformArg(fields map[string]node, a []string) error {
|
||||||
|
leaf, _, err := unwrapTransform(a[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if isRef(leaf) {
|
||||||
|
if leaf == refPrefix {
|
||||||
|
return fmt.Errorf("reference has no path")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return checkArm(leaf, fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformOperand(a []string) []string {
|
||||||
|
leaf, _, err := unwrapTransform(a[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{leaf}
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformPrep(outer func(string) string) func([]string) callFn {
|
||||||
|
return func(a []string) callFn {
|
||||||
|
_, chain, err := unwrapTransform(a[0])
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("fejkdata: transform arg %q reached prep unvalidated: %v", a[0], err))
|
||||||
|
}
|
||||||
|
return func(_ *session, _ string, operands []string) string {
|
||||||
|
v := operands[0]
|
||||||
|
for i := len(chain) - 1; i >= 0; i-- {
|
||||||
|
v = chain[i](v)
|
||||||
|
}
|
||||||
|
return outer(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// asciiFolds maps the Latin letters with diacritics or ligatures to ASCII.
|
||||||
|
var asciiFolds = map[rune]string{
|
||||||
|
'À': "A", 'Á': "A", 'Â': "A", 'Ã': "A", 'Ä': "A", 'Å': "A", 'Æ': "AE", 'Ç': "C",
|
||||||
|
'È': "E", 'É': "E", 'Ê': "E", 'Ë': "E", 'Ì': "I", 'Í': "I", 'Î': "I", 'Ï': "I",
|
||||||
|
'Ð': "D", 'Ñ': "N", 'Ò': "O", 'Ó': "O", 'Ô': "O", 'Õ': "O", 'Ö': "O", 'Ø': "O",
|
||||||
|
'Ù': "U", 'Ú': "U", 'Û': "U", 'Ü': "U", 'Ý': "Y", 'Þ': "Th", 'ß': "ss", 'Œ': "OE",
|
||||||
|
'à': "a", 'á': "a", 'â': "a", 'ã': "a", 'ä': "a", 'å': "a", 'æ': "ae", 'ç': "c",
|
||||||
|
'è': "e", 'é': "e", 'ê': "e", 'ë': "e", 'ì': "i", 'í': "i", 'î': "i", 'ï': "i",
|
||||||
|
'ð': "d", 'ñ': "n", 'ò': "o", 'ó': "o", 'ô': "o", 'õ': "o", 'ö': "o", 'ø': "o",
|
||||||
|
'ù': "u", 'ú': "u", 'û': "u", 'ü': "u", 'ý': "y", 'þ': "th", 'ÿ': "y", 'œ': "oe",
|
||||||
|
}
|
||||||
|
|
||||||
|
// asciiFold rewrites s to ASCII: folded Latin letters stay, any other non-ASCII
|
||||||
|
// rune is dropped.
|
||||||
|
func asciiFold(s string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
for _, r := range s {
|
||||||
|
if r < unicode.MaxASCII {
|
||||||
|
b.WriteRune(r)
|
||||||
|
} else {
|
||||||
|
b.WriteString(asciiFolds[r])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
// atoi parses an arg a builtin's check already validated. It panics rather than
|
// atoi parses an arg a builtin's check already validated. It panics rather than
|
||||||
// returning zero, so a check that stops covering its own args is a stack trace and
|
// returning zero, so a check that stops covering its own args is a stack trace and
|
||||||
// not a silently wrong length, range or decimal count.
|
// not a silently wrong length, range or decimal count.
|
||||||
@@ -119,10 +214,10 @@ func randBytes(r rng, n int) []byte {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func randHex(r rng, n int) string {
|
func randChars(r rng, n int, alphabet string) string {
|
||||||
b := make([]byte, n)
|
b := make([]byte, n)
|
||||||
for i := range b {
|
for i := range b {
|
||||||
b[i] = hexDigits[r.IntN(16)]
|
b[i] = alphabet[r.IntN(len(alphabet))]
|
||||||
}
|
}
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
@@ -182,18 +277,9 @@ func seqArg(_ map[string]node, a []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// nanoidAlphabet is the 64-char URL-safe set Nano IDs use (order is irrelevant
|
// nanoidAlphabet is the 64-char URL-safe set Nano IDs use.
|
||||||
// to the uniform pick).
|
|
||||||
const nanoidAlphabet = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
const nanoidAlphabet = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
|
||||||
func nanoid(r rng, n int) string {
|
|
||||||
b := make([]byte, n)
|
|
||||||
for i := range b {
|
|
||||||
b[i] = nanoidAlphabet[r.IntN(len(nanoidAlphabet))]
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// uuidV7 builds an RFC 9562 v7 UUID. The 48-bit timestamp field is drawn from
|
// uuidV7 builds an RFC 9562 v7 UUID. The 48-bit timestamp field is drawn from
|
||||||
// the rng (not the clock) to stay reproducible, then the version (7) and variant
|
// the rng (not the clock) to stay reproducible, then the version (7) and variant
|
||||||
// (10) bits are forced; the rest is random.
|
// (10) bits are forced; the rest is random.
|
||||||
|
|||||||
+40
-27
@@ -13,10 +13,10 @@ func TestBuiltinIDGenerators(t *testing.T) {
|
|||||||
tmpl string
|
tmpl string
|
||||||
re *regexp.Regexp
|
re *regexp.Regexp
|
||||||
}{
|
}{
|
||||||
{"uuid v7", `{"format":"{uuid()}"}`, regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)},
|
{"uuid v7", `"{uuid()}"`, regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)},
|
||||||
{"ulid", `{"format":"{ulid()}"}`, regexp.MustCompile(`^[0-7][0-9A-HJKMNP-TV-Z]{25}$`)},
|
{"ulid", `"{ulid()}"`, regexp.MustCompile(`^[0-7][0-9A-HJKMNP-TV-Z]{25}$`)},
|
||||||
{"nanoid", `{"format":"{nanoid(21)}"}`, regexp.MustCompile(`^[A-Za-z0-9_-]{21}$`)},
|
{"nanoid", `"{nanoid(21)}"`, regexp.MustCompile(`^[A-Za-z0-9_-]{21}$`)},
|
||||||
{"hex", `{"format":"{hex(16)}"}`, regexp.MustCompile(`^[0-9a-f]{16}$`)},
|
{"hex", `"{hex(16)}"`, regexp.MustCompile(`^[0-9a-f]{16}$`)},
|
||||||
}
|
}
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
@@ -38,9 +38,9 @@ func TestBuiltinIDGenerators(t *testing.T) {
|
|||||||
// sample draws only from the seeded rng, so same seed -> same value.
|
// sample draws only from the seeded rng, so same seed -> same value.
|
||||||
func TestBuiltinSamplesReproducible(t *testing.T) {
|
func TestBuiltinSamplesReproducible(t *testing.T) {
|
||||||
for _, tmpl := range []string{
|
for _, tmpl := range []string{
|
||||||
`{"format":"{uuid()}"}`, `{"format":"{ulid()}"}`,
|
`"{uuid()}"`, `"{ulid()}"`,
|
||||||
`{"format":"{nanoid(12)}"}`, `{"format":"{int(1,1000000)}"}`,
|
`"{nanoid(12)}"`, `"{int(1,1000000)}"`,
|
||||||
`{"format":"{float(0,1,6)}"}`, `{"format":"{base64(12)}"}`, `{"format":"{iban(SE)}"}`,
|
`"{float(0,1,6)}"`, `"{base64(12)}"`, `"{iban(SE)}"`,
|
||||||
} {
|
} {
|
||||||
if a, b := mustRender(t, engine(7), tmpl), mustRender(t, engine(7), tmpl); a != b {
|
if a, b := mustRender(t, engine(7), tmpl), mustRender(t, engine(7), tmpl); a != b {
|
||||||
t.Fatalf("%s not reproducible: %q != %q", tmpl, a, b)
|
t.Fatalf("%s not reproducible: %q != %q", tmpl, a, b)
|
||||||
@@ -52,7 +52,7 @@ func TestBuiltinIntInclusiveRange(t *testing.T) {
|
|||||||
f := engine(1)
|
f := engine(1)
|
||||||
lo, hi := false, false
|
lo, hi := false, false
|
||||||
for i := 0; i < 1000; i++ {
|
for i := 0; i < 1000; i++ {
|
||||||
n, err := strconv.Atoi(mustRender(t, f, `{"format":"{int(3,7)}"}`))
|
n, err := strconv.Atoi(mustRender(t, f, `"{int(3,7)}"`))
|
||||||
if err != nil || n < 3 || n > 7 {
|
if err != nil || n < 3 || n > 7 {
|
||||||
t.Fatalf("int(3,7) = %d (err %v), out of range", n, err)
|
t.Fatalf("int(3,7) = %d (err %v), out of range", n, err)
|
||||||
}
|
}
|
||||||
@@ -66,14 +66,14 @@ func TestBuiltinIntInclusiveRange(t *testing.T) {
|
|||||||
func TestBuiltinFloat(t *testing.T) {
|
func TestBuiltinFloat(t *testing.T) {
|
||||||
f, re := engine(1), regexp.MustCompile(`^[12]\.\d{3}$`)
|
f, re := engine(1), regexp.MustCompile(`^[12]\.\d{3}$`)
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
if got := mustRender(t, f, `{"format":"{float(1,2,3)}"}`); !re.MatchString(got) {
|
if got := mustRender(t, f, `"{float(1,2,3)}"`); !re.MatchString(got) {
|
||||||
t.Fatalf("float(1,2,3) = %q, want d.ddd in [1,2]", got)
|
t.Fatalf("float(1,2,3) = %q, want d.ddd in [1,2]", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuiltinBase64(t *testing.T) {
|
func TestBuiltinBase64(t *testing.T) {
|
||||||
got := mustRender(t, engine(1), `{"format":"{base64(9)}"}`)
|
got := mustRender(t, engine(1), `"{base64(9)}"`)
|
||||||
if b, err := base64.StdEncoding.DecodeString(got); err != nil || len(b) != 9 {
|
if b, err := base64.StdEncoding.DecodeString(got); err != nil || len(b) != 9 {
|
||||||
t.Fatalf("base64(9) = %q decodes to %d bytes (err %v), want 9", got, len(b), err)
|
t.Fatalf("base64(9) = %q decodes to %d bytes (err %v), want 9", got, len(b), err)
|
||||||
}
|
}
|
||||||
@@ -83,9 +83,9 @@ func TestBuiltinBase64(t *testing.T) {
|
|||||||
// hand-computed check, like the luhn test. mod-11 emits X when it would be 10.
|
// hand-computed check, like the luhn test. mod-11 emits X when it would be 10.
|
||||||
func TestBuiltinChecksums(t *testing.T) {
|
func TestBuiltinChecksums(t *testing.T) {
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
`{"format":"#1#2#3#4#5#6#7#8{mod11()}"}`: "123456785", // weights 2..7 from the right
|
`"12345678{mod11()}"`: "123456785", // weights 2..7 from the right
|
||||||
`{"format":"#6{mod11()}"}`: "6X", // remainder 10 -> X
|
`"6{mod11()}"`: "6X", // remainder 10 -> X
|
||||||
`{"format":"#4#0#0#6#3#8#1#3#3#3#9#3{ean()}"}`: "4006381333931", // EAN-13 (= ISBN-13) check digit
|
`"400638133393{ean()}"`: "4006381333931", // EAN-13 (= ISBN-13) check digit
|
||||||
}
|
}
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
for tmpl, want := range cases {
|
for tmpl, want := range cases {
|
||||||
@@ -101,14 +101,14 @@ func TestBuiltinChecksums(t *testing.T) {
|
|||||||
func TestBuiltinSeqPerSession(t *testing.T) {
|
func TestBuiltinSeqPerSession(t *testing.T) {
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
for i := 1; i <= 5; i++ {
|
for i := 1; i <= 5; i++ {
|
||||||
if got := mustRender(t, f, `{"format":"{seq()}"}`); got != strconv.Itoa(i) {
|
if got := mustRender(t, f, `"{seq()}"`); got != strconv.Itoa(i) {
|
||||||
t.Fatalf("seq call %d = %q, want %d", i, got, i)
|
t.Fatalf("seq call %d = %q, want %d", i, got, i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if got := mustRender(t, f, `{"format":"{seq(orders)}"}`); got != "1" {
|
if got := mustRender(t, f, `"{seq(orders)}"`); got != "1" {
|
||||||
t.Fatalf("named seq(orders) = %q, want its own count from 1", got)
|
t.Fatalf("named seq(orders) = %q, want its own count from 1", got)
|
||||||
}
|
}
|
||||||
if got := mustRender(t, f, `{"format":"{seq()}"}`); got != "6" {
|
if got := mustRender(t, f, `"{seq()}"`); got != "6" {
|
||||||
t.Fatalf("default seq after the named one = %q, want 6 (counters are independent)", got)
|
t.Fatalf("default seq after the named one = %q, want 6 (counters are independent)", got)
|
||||||
}
|
}
|
||||||
// repeat drives one counter forward across its renders...
|
// repeat drives one counter forward across its renders...
|
||||||
@@ -116,7 +116,7 @@ func TestBuiltinSeqPerSession(t *testing.T) {
|
|||||||
t.Fatalf("repeat seq = %q, want 1,2,3", got)
|
t.Fatalf("repeat seq = %q, want 1,2,3", got)
|
||||||
}
|
}
|
||||||
// ...and a fresh session restarts from 1.
|
// ...and a fresh session restarts from 1.
|
||||||
if got := mustRender(t, engine(2), `{"format":"{seq()}"}`); got != "1" {
|
if got := mustRender(t, engine(2), `"{seq()}"`); got != "1" {
|
||||||
t.Fatalf("new session seq = %q, want 1", got)
|
t.Fatalf("new session seq = %q, want 1", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,14 +126,14 @@ func TestBuiltinSeqPerSession(t *testing.T) {
|
|||||||
// at compile, not detonate when the template is drawn.
|
// at compile, not detonate when the template is drawn.
|
||||||
func TestBuiltinArgLimits(t *testing.T) {
|
func TestBuiltinArgLimits(t *testing.T) {
|
||||||
bad := []string{
|
bad := []string{
|
||||||
`{"format":"{int(-9223372036854775808,9223372036854775807)}"}`, // span overflows int -> IntN panic
|
`"{int(-9223372036854775808,9223372036854775807)}"`, // span overflows int -> IntN panic
|
||||||
`{"format":"{hex(2000000000)}"}`, // ~2 GB string
|
`"{hex(2000000000)}"`, // ~2 GB string
|
||||||
`{"format":"{nanoid(2000000000)}"}`,
|
`"{nanoid(2000000000)}"`,
|
||||||
`{"format":"{base64(2000000000)}"}`,
|
`"{base64(2000000000)}"`,
|
||||||
`{"format":"{float(-1e308,1e308,2)}"}`, // span is +Inf
|
`"{float(-1e308,1e308,2)}"`, // span is +Inf
|
||||||
`{"format":"{float(0,1,100000)}"}`, // decimals -> huge string
|
`"{float(0,1,100000)}"`, // decimals -> huge string
|
||||||
`{"format":"{calc(1+1,100000)}"}`, // calc decimals -> huge string
|
`"{calc(1+1,100000)}"`, // calc decimals -> huge string
|
||||||
`{"format":"{x}","x":["1"],"repeat":2000000000}`, // repeat -> multi-GB join
|
`{"format":"{x}","x":"1","repeat":2000000000}`, // repeat -> multi-GB join
|
||||||
}
|
}
|
||||||
for _, tmpl := range bad {
|
for _, tmpl := range bad {
|
||||||
if _, err := compile(parse(t, tmpl)); err == nil {
|
if _, err := compile(parse(t, tmpl)); err == nil {
|
||||||
@@ -141,7 +141,7 @@ func TestBuiltinArgLimits(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ordinary values still compile.
|
// Ordinary values still compile.
|
||||||
if _, err := compile(parse(t, `{"format":"{hex(16)} {int(-5,5)} {float(0,1,3)} {calc(1+1,2)}"}`)); err != nil {
|
if _, err := compile(parse(t, `"{hex(16)} {int(-5,5)} {float(0,1,3)} {calc(1+1,2)}"`)); err != nil {
|
||||||
t.Errorf("compile of normal args failed: %v", err)
|
t.Errorf("compile of normal args failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ func TestBuiltinIBAN(t *testing.T) {
|
|||||||
wantLen := map[string]int{"SE": 24, "DE": 22, "NO": 15}
|
wantLen := map[string]int{"SE": 24, "DE": 22, "NO": 15}
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
for cc, n := range wantLen {
|
for cc, n := range wantLen {
|
||||||
tmpl := `{"format":"{iban(` + cc + `)}"}`
|
tmpl := `"{iban(` + cc + `)}"`
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
got := mustRender(t, f, tmpl)
|
got := mustRender(t, f, tmpl)
|
||||||
if got[:2] != cc || len(got) != n {
|
if got[:2] != cc || len(got) != n {
|
||||||
@@ -217,3 +217,16 @@ func mustPanic(t *testing.T, name string, call func()) {
|
|||||||
}()
|
}()
|
||||||
call()
|
call()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClassBuiltinArgs(t *testing.T) {
|
||||||
|
for _, bad := range []string{`"{digits(0)}"`, `"{digits(2000000000)}"`, `"{upper(-1)}"`, `"{lower(x)}"`, `"{digits()}"`, `"{upper(1,2)}"`} {
|
||||||
|
if _, err := compile(parse(t, bad)); err == nil {
|
||||||
|
t.Errorf("compile(%s) = nil error, want the arg rejected", bad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ok := range []string{`"{digits(1048576)}"`, `"{upper(1)}"`, `"{lower(26)}"`} {
|
||||||
|
if _, err := compile(parse(t, ok)); err != nil {
|
||||||
|
t.Errorf("compile(%s) = %v", ok, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,16 +8,6 @@ import (
|
|||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
// calc is the {calc(expr[, dp])} token: an arithmetic expression over number
|
|
||||||
// literals and sibling-field names, with + - * /, unary minus and parentheses. It is
|
|
||||||
// a registry builtin like any other {name(args)} function — the one that names
|
|
||||||
// operands, which expand reads for it (once per expansion, so the value computed is
|
|
||||||
// the value the format showed) and hands over as strings. The value prints in
|
|
||||||
// minimal decimal form, or rounded to dp when given. An operand that isn't a number
|
|
||||||
// becomes NaN, which propagates and prints as "NaN" — visible, never a render error.
|
|
||||||
// The expression is parsed once at New into an AST every render shares and none
|
|
||||||
// mutates, so render cannot fail and must not carry per-render state.
|
|
||||||
|
|
||||||
// calcNode is a parsed expression node. It evaluates over the operand values expand
|
// calcNode is a parsed expression node. It evaluates over the operand values expand
|
||||||
// read, so the evaluator touches neither the rng nor the node tree.
|
// read, so the evaluator touches neither the rng nor the node tree.
|
||||||
type calcNode interface {
|
type calcNode interface {
|
||||||
@@ -132,26 +122,10 @@ func indexVars(n calcNode, at map[string]int) calcNode {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
// calcOperands lists the sibling-field names every {calc(...)} token in a format
|
// calcOperands lists the sibling-field names a calc's args read. checkCalc reports
|
||||||
// reads, so cycle detection sees the field edges calc renders through (a function
|
// an expression that does not parse, so one that does not simply names nothing.
|
||||||
// token otherwise carries no field edge).
|
func calcOperands(args []string) []string {
|
||||||
func calcOperands(format string) []string {
|
if len(args) == 0 {
|
||||||
var names []string
|
|
||||||
_ = eachToken(format, func(t ftoken) error {
|
|
||||||
if t.kind == 'b' {
|
|
||||||
names = append(names, calcTokenOperands(t.body)...)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
|
|
||||||
// calcTokenOperands lists the sibling-field names one {token} body reads, empty
|
|
||||||
// for anything that is not a {calc(...)}. checkCalc reports an expression that does
|
|
||||||
// not parse, so one that does not simply names nothing here.
|
|
||||||
func calcTokenOperands(body string) []string {
|
|
||||||
name, args, ok := funcCall(body)
|
|
||||||
if !ok || name != "calc" || len(args) == 0 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
expr, err := parseCalc(args[0])
|
expr, err := parseCalc(args[0])
|
||||||
|
|||||||
+24
-24
@@ -12,14 +12,14 @@ import (
|
|||||||
func TestCalcArithmetic(t *testing.T) {
|
func TestCalcArithmetic(t *testing.T) {
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
`{"format":"{calc(2 + 3)}"}`: "5",
|
`"{calc(2 + 3)}"`: "5",
|
||||||
`{"format":"{calc(2 * 3 + 4)}"}`: "10", // * binds tighter than +
|
`"{calc(2 * 3 + 4)}"`: "10", // * binds tighter than +
|
||||||
`{"format":"{calc(2 + 3 * 4)}"}`: "14",
|
`"{calc(2 + 3 * 4)}"`: "14",
|
||||||
`{"format":"{calc((2 + 3) * 4)}"}`: "20", // parentheses override
|
`"{calc((2 + 3) * 4)}"`: "20", // parentheses override
|
||||||
`{"format":"{calc(10 / 4)}"}`: "2.5",
|
`"{calc(10 / 4)}"`: "2.5",
|
||||||
`{"format":"{calc(-2 + 5)}"}`: "3", // unary minus
|
`"{calc(-2 + 5)}"`: "3", // unary minus
|
||||||
`{"format":"{calc(2 - -3)}"}`: "5",
|
`"{calc(2 - -3)}"`: "5",
|
||||||
`{"format":"{calc(1.5 * 2)}"}`: "3", // whole result drops the decimals
|
`"{calc(1.5 * 2)}"`: "3", // whole result drops the decimals
|
||||||
}
|
}
|
||||||
for tmpl, want := range cases {
|
for tmpl, want := range cases {
|
||||||
if got := mustRender(t, f, tmpl); got != want {
|
if got := mustRender(t, f, tmpl); got != want {
|
||||||
@@ -33,9 +33,9 @@ func TestCalcArithmetic(t *testing.T) {
|
|||||||
func TestCalcAuto(t *testing.T) {
|
func TestCalcAuto(t *testing.T) {
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
`{"format":"{calc(10 / 3)}"}`: "3.3333333333333335",
|
`"{calc(10 / 3)}"`: "3.3333333333333335",
|
||||||
`{"format":"{calc(6 / 2)}"}`: "3",
|
`"{calc(6 / 2)}"`: "3",
|
||||||
`{"format":"{calc(1 / 4)}"}`: "0.25",
|
`"{calc(1 / 4)}"`: "0.25",
|
||||||
}
|
}
|
||||||
for tmpl, want := range cases {
|
for tmpl, want := range cases {
|
||||||
if got := mustRender(t, f, tmpl); got != want {
|
if got := mustRender(t, f, tmpl); got != want {
|
||||||
@@ -49,9 +49,9 @@ func TestCalcAuto(t *testing.T) {
|
|||||||
func TestCalcDecimals(t *testing.T) {
|
func TestCalcDecimals(t *testing.T) {
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
`{"format":"{calc(10 / 3, 2)}"}`: "3.33",
|
`"{calc(10 / 3, 2)}"`: "3.33",
|
||||||
`{"format":"{calc(10 / 3, 0)}"}`: "3",
|
`"{calc(10 / 3, 0)}"`: "3",
|
||||||
`{"format":"{calc(2 * 3, 2)}"}`: "6.00",
|
`"{calc(2 * 3, 2)}"`: "6.00",
|
||||||
}
|
}
|
||||||
for tmpl, want := range cases {
|
for tmpl, want := range cases {
|
||||||
if got := mustRender(t, f, tmpl); got != want {
|
if got := mustRender(t, f, tmpl); got != want {
|
||||||
@@ -63,11 +63,11 @@ func TestCalcDecimals(t *testing.T) {
|
|||||||
// TestCalcFields pins that bare names resolve to sibling fields, rendered then
|
// TestCalcFields pins that bare names resolve to sibling fields, rendered then
|
||||||
// parsed as numbers.
|
// parsed as numbers.
|
||||||
func TestCalcFields(t *testing.T) {
|
func TestCalcFields(t *testing.T) {
|
||||||
if got := mustRender(t, engine(1), `{"format":"{calc(price * qty, 2)}","price":["19.99"],"qty":["3"]}`); got != "59.97" {
|
if got := mustRender(t, engine(1), `{"format":"{calc(price * qty, 2)}","price":"19.99","qty":"3"}`); got != "59.97" {
|
||||||
t.Fatalf("calc over fields = %q, want 59.97", got)
|
t.Fatalf("calc over fields = %q, want 59.97", got)
|
||||||
}
|
}
|
||||||
// A field that is itself a template renders before parsing.
|
// A field that is itself a template renders before parsing.
|
||||||
if got := mustRender(t, engine(1), `{"format":"{calc(a - b)}","a":[{"format":"{n}","n":["10"]}],"b":["3"]}`); got != "7" {
|
if got := mustRender(t, engine(1), `{"format":"{calc(a - b)}","a":{"format":"{n}","n":"10"},"b":"3"}`); got != "7" {
|
||||||
t.Fatalf("calc(a - b) = %q, want 7", got)
|
t.Fatalf("calc(a - b) = %q, want 7", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,31 +75,31 @@ func TestCalcFields(t *testing.T) {
|
|||||||
// TestCalcNonNumericIsNaN pins the never-fail rule: a field that doesn't render
|
// TestCalcNonNumericIsNaN pins the never-fail rule: a field that doesn't render
|
||||||
// to a number becomes NaN, which propagates and prints visibly.
|
// to a number becomes NaN, which propagates and prints visibly.
|
||||||
func TestCalcNonNumericIsNaN(t *testing.T) {
|
func TestCalcNonNumericIsNaN(t *testing.T) {
|
||||||
if got := mustRender(t, engine(1), `{"format":"{calc(x * 2)}","x":["abc"]}`); got != "NaN" {
|
if got := mustRender(t, engine(1), `{"format":"{calc(x * 2)}","x":"abc"}`); got != "NaN" {
|
||||||
t.Fatalf("calc over non-numeric field = %q, want NaN", got)
|
t.Fatalf("calc over non-numeric field = %q, want NaN", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCalcReproducible pins that a calc over a random operand stays seed-stable.
|
// TestCalcReproducible pins that a calc over a random operand stays seed-stable.
|
||||||
func TestCalcReproducible(t *testing.T) {
|
func TestCalcReproducible(t *testing.T) {
|
||||||
tmpl := `{"format":"{calc(q * 2 + 1)}","q":[{"format":"{int(1,1000000)}"}]}`
|
tmpl := `{"format":"{calc(q * 2 + 1)}","q":"{int(1,1000000)}"}`
|
||||||
if a, b := mustRender(t, engine(7), tmpl), mustRender(t, engine(7), tmpl); a != b {
|
if a, b := mustRender(t, engine(7), tmpl), mustRender(t, engine(7), tmpl); a != b {
|
||||||
t.Fatalf("calc not reproducible: %q != %q", a, b)
|
t.Fatalf("calc not reproducible: %q != %q", a, b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCalcTokenOperandsReadsOnlyACalc pins what the helper answers for a body that
|
// TestTokenOperandsReadsOnlyAnOperandBuiltin pins what the helper answers for a body that
|
||||||
// is not a calc, and for one whose expression does not parse: nothing, either way.
|
// is not a calc, and for one whose expression does not parse: nothing, either way.
|
||||||
// checkCalc is what reports a bad expression, so the callers that run after it
|
// checkCalc is what reports a bad expression, so the callers that run after it
|
||||||
// never meet one — but they must not have to depend on that order to be safe.
|
// never meet one — but they must not have to depend on that order to be safe.
|
||||||
func TestCalcTokenOperandsReadsOnlyACalc(t *testing.T) {
|
func TestTokenOperandsReadsOnlyAnOperandBuiltin(t *testing.T) {
|
||||||
for _, body := range []string{"plain", "luhn()", "calc()", "calc(1 +)", "calc(()"} {
|
for _, body := range []string{"plain", "luhn()", "calc()", "calc(1 +)", "calc(()"} {
|
||||||
if got := calcTokenOperands(body); got != nil {
|
if got := tokenOperands(body); got != nil {
|
||||||
t.Errorf("calcTokenOperands(%q) = %v, want none", body, got)
|
t.Errorf("tokenOperands(%q) = %v, want none", body, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if got := calcTokenOperands("calc(net * qty)"); len(got) != 2 {
|
if got := tokenOperands("calc(net * qty)"); len(got) != 2 {
|
||||||
t.Errorf("calcTokenOperands(calc(net * qty)) = %v, want both operands", got)
|
t.Errorf("tokenOperands(calc(net * qty)) = %v, want both operands", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-24
@@ -1,24 +1,17 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "{street-number} {street}\n{locality}, {region} {postal-code}",
|
||||||
"format": "{street-number} {street}\n{locality}, {region} {postal-code}",
|
"street": {
|
||||||
"street": [
|
"format": "{name} {suffix}",
|
||||||
{
|
"name": ["Adams", "Ashby", "Aspen", "Bay", "Birch", "Bridge", "Cedar", "Chestnut", "Church", "Clark", "Cypress", "Dogwood", "Elm", "Forest", "Franklin", "Garden", "Grove", "Hawthorn", "Hickory", "Highland", "Jackson", "Jefferson", "Juniper", "Lake", "Laurel", "Liberty", "Lincoln", "Madison", "Magnolia", "Maple", "Market", "Meadow", "Mill", "Oak", "Park", "Pine", "Poplar", "Prospect", "Ridge", "River", "Spruce", "Sunset", "Sycamore", "Union", "Walnut", "Washington", "Willow", "Wilson"],
|
||||||
"format": "{name} {suffix}",
|
"suffix": ["Avenue", "Boulevard", "Circle", "Court", "Drive", "Lane", "Place", "Road", "Street", "Terrace", "Trail", "Way"]
|
||||||
"name": ["Adams", "Ashby", "Aspen", "Bay", "Birch", "Bridge", "Cedar", "Chestnut", "Church", "Clark", "Cypress", "Dogwood", "Elm", "Forest", "Franklin", "Garden", "Grove", "Hawthorn", "Hickory", "Highland", "Jackson", "Jefferson", "Juniper", "Lake", "Laurel", "Liberty", "Lincoln", "Madison", "Magnolia", "Maple", "Market", "Meadow", "Mill", "Oak", "Park", "Pine", "Poplar", "Prospect", "Ridge", "River", "Spruce", "Sunset", "Sycamore", "Union", "Walnut", "Washington", "Willow", "Wilson"],
|
},
|
||||||
"suffix": ["Avenue", "Boulevard", "Circle", "Court", "Drive", "Lane", "Place", "Road", "Street", "Terrace", "Trail", "Way"]
|
"street-number": [
|
||||||
}
|
"{int(10,99)}",
|
||||||
],
|
{ "format": "{int(100,999)}", "weight": 2 },
|
||||||
"street-number": [
|
{ "format": "{int(1000,9999)}", "weight": 0.5 },
|
||||||
{ "format": "10" },
|
{ "format": "{int(10,99)}{int(100,999)}", "weight": 0.3 }
|
||||||
{ "format": "100", "weight": 2 },
|
],
|
||||||
{ "format": "1000", "weight": 0.5 },
|
"locality": ["Albany", "Atlanta", "Austin", "Baltimore", "Boston", "Charlotte", "Chicago", "Cincinnati", "Cleveland", "Columbus", "Dallas", "Denver", "Detroit", "El Paso", "Fort Worth", "Fresno", "Houston", "Indianapolis", "Jacksonville", "Kansas City", "Las Vegas", "Long Beach", "Los Angeles", "Memphis", "Mesa", "Miami", "Milwaukee", "Minneapolis", "Nashville", "New Orleans", "Oakland", "Oklahoma City", "Omaha", "Orlando", "Philadelphia", "Phoenix", "Pittsburgh", "Portland", "Raleigh", "Sacramento", "San Antonio", "San Diego", "San Jose", "Seattle", "St. Louis", "Tampa", "Tucson", "Tulsa"],
|
||||||
{ "format": "10100", "weight": 0.3 }
|
"region": ["AL", "AZ", "CA", "CO", "CT", "FL", "GA", "IL", "IN", "KY", "LA", "MA", "MD", "MI", "MN", "MO", "NC", "NJ", "NV", "NY", "OH", "OK", "OR", "PA", "TN", "TX", "VA", "WA", "WI"],
|
||||||
],
|
"postal-code": ["{int(10000,99999)}", { "format": "{int(10000,99999)}-{digits(4)}", "weight": 0.3 }]
|
||||||
"locality": ["Albany", "Atlanta", "Austin", "Baltimore", "Boston", "Charlotte", "Chicago", "Cincinnati", "Cleveland", "Columbus", "Dallas", "Denver", "Detroit", "El Paso", "Fort Worth", "Fresno", "Houston", "Indianapolis", "Jacksonville", "Kansas City", "Las Vegas", "Long Beach", "Los Angeles", "Memphis", "Mesa", "Miami", "Milwaukee", "Minneapolis", "Nashville", "New Orleans", "Oakland", "Oklahoma City", "Omaha", "Orlando", "Philadelphia", "Phoenix", "Pittsburgh", "Portland", "Raleigh", "Sacramento", "San Antonio", "San Diego", "San Jose", "Seattle", "St. Louis", "Tampa", "Tucson", "Tulsa"],
|
}
|
||||||
"region": ["AL", "AZ", "CA", "CO", "CT", "FL", "GA", "IL", "IN", "KY", "LA", "MA", "MD", "MI", "MN", "MO", "NC", "NJ", "NV", "NY", "OH", "OK", "OR", "PA", "TN", "TX", "VA", "WA", "WI"],
|
|
||||||
"postal-code": [
|
|
||||||
{ "format": "10000" },
|
|
||||||
{ "format": "10000-0000", "weight": 0.3 }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"format": "##{x}{x}{x}{x}{x}{x}",
|
"format": "#{x}{x}{x}{x}{x}{x}",
|
||||||
"x": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"],
|
"x": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"],
|
||||||
"weight": 2
|
"weight": 2
|
||||||
},
|
},
|
||||||
|
|||||||
+12
-14
@@ -1,14 +1,12 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "{base} {suffix}",
|
||||||
"format": "{base} {suffix}",
|
"base": [
|
||||||
"base": [
|
{
|
||||||
{
|
"format": "{a}{b}",
|
||||||
"format": "{a}{b}",
|
"a": ["Acme", "Apex", "Atlas", "Aurora", "Blue", "Bright", "Cedar", "Crest", "Delta", "Eagle", "Ever", "Fair", "Falcon", "Iron", "Maple", "North", "Onyx", "Pioneer", "Polar", "Prime", "Quartz", "River", "Silver", "Solar", "Summit", "Sun", "Swift", "True", "Vertex", "West"],
|
||||||
"a": ["Acme", "Apex", "Atlas", "Aurora", "Blue", "Bright", "Cedar", "Crest", "Delta", "Eagle", "Ever", "Fair", "Falcon", "Iron", "Maple", "North", "Onyx", "Pioneer", "Polar", "Prime", "Quartz", "River", "Silver", "Solar", "Summit", "Sun", "Swift", "True", "Vertex", "West"],
|
"b": ["bridge", "core", "field", "flow", "forge", "gate", "grid", "hub", "line", "logic", "mark", "nest", "path", "point", "scape", "soft", "span", "spark", "stone", "tech", "ware", "wave", "works", "yard"]
|
||||||
"b": ["bridge", "core", "field", "flow", "forge", "gate", "grid", "hub", "line", "logic", "mark", "nest", "path", "point", "scape", "soft", "span", "spark", "stone", "tech", "ware", "wave", "works", "yard"]
|
},
|
||||||
},
|
["Atlas Industries", "Beacon Labs", "Cedar & Vine", "Compass Group", "Evergreen Trading", "Harbor Logistics", "Hudson Partners", "Liberty Holdings", "Meridian Capital", "Northgate Studios", "Pinnacle Systems", "Riverside", "Sequoia Foods", "Smith & Sons", "Stonebridge Ventures", "Sunrise Bakery", "Wexford Holdings"]
|
||||||
["Atlas Industries", "Beacon Labs", "Cedar & Vine", "Compass Group", "Evergreen Trading", "Harbor Logistics", "Hudson Partners", "Liberty Holdings", "Meridian Capital", "Northgate Studios", "Pinnacle Systems", "Riverside", "Sequoia Foods", "Smith & Sons", "Stonebridge Ventures", "Sunrise Bakery", "Wexford Holdings"]
|
],
|
||||||
],
|
"suffix": ["Co.", "Corp.", "Group", { "format": "Inc.", "weight": 2 }, { "format": "LLC", "weight": 2 }, "Ltd."]
|
||||||
"suffix": ["Co.", "Corp.", "Group", "Inc.", "Inc.", "LLC", "LLC", "Ltd."]
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
+13
-15
@@ -1,15 +1,13 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "{month}/{day}/{year}",
|
||||||
"format": "{month}/{day}/{year}",
|
"month": ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"],
|
||||||
"month": ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"],
|
"day": ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28"],
|
||||||
"day": ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28"],
|
"year": [
|
||||||
"year": [
|
"197{digits(1)}",
|
||||||
{ "format": "#1970" },
|
"198{digits(1)}",
|
||||||
{ "format": "#1980" },
|
{ "format": "199{digits(1)}", "weight": 2 },
|
||||||
{ "format": "#1990", "weight": 2 },
|
{ "format": "200{digits(1)}", "weight": 3 },
|
||||||
{ "format": "2#0#00", "weight": 3 },
|
{ "format": "201{digits(1)}", "weight": 3 },
|
||||||
{ "format": "2#0#10", "weight": 3 },
|
{ "format": "202{digits(1)}", "weight": 2 }
|
||||||
{ "format": "2#020", "weight": 2 }
|
]
|
||||||
]
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
+26
-29
@@ -1,29 +1,26 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "{local}@{domain}",
|
||||||
"format": "{local}@{domain}",
|
"local": [
|
||||||
"local": [
|
{
|
||||||
{
|
"format": "{first}{sep}{last}{n}",
|
||||||
"format": "{first}{sep}{last}{n}",
|
"weight": 3,
|
||||||
"weight": 3,
|
"first": ["alex", "amelia", "andrew", "anna", "ava", "benjamin", "carter", "charlotte", "chloe", "chris", "daniel", "david", "dylan", "elijah", "ella", "emily", "emma", "ethan", "evelyn", "gabriel", "grace", "hannah", "harper", "henry", "isaac", "isabella", "jack", "jacob", "james", "john", "joseph", "julia", "leah", "liam", "linda", "lily", "logan", "lucas", "luke", "mary", "mason", "matthew", "maya", "mia", "michael", "natalie", "noah", "nora", "oliver", "olivia", "owen", "patricia", "robert", "ryan", "samuel", "sarah", "sofia", "sophia", "thomas", "victoria", "william", "zoe"],
|
||||||
"first": ["alex", "amelia", "andrew", "anna", "ava", "benjamin", "carter", "charlotte", "chloe", "chris", "daniel", "david", "dylan", "elijah", "ella", "emily", "emma", "ethan", "evelyn", "gabriel", "grace", "hannah", "harper", "henry", "isaac", "isabella", "jack", "jacob", "james", "john", "joseph", "julia", "leah", "liam", "linda", "lily", "logan", "lucas", "luke", "mary", "mason", "matthew", "maya", "mia", "michael", "natalie", "noah", "nora", "oliver", "olivia", "owen", "patricia", "robert", "ryan", "samuel", "sarah", "sofia", "sophia", "thomas", "victoria", "william", "zoe"],
|
"last": ["adams", "allen", "anderson", "bailey", "baker", "bennett", "brooks", "brown", "campbell", "carter", "clark", "collins", "cook", "cooper", "davis", "edwards", "evans", "fisher", "flores", "foster", "garcia", "gonzalez", "gray", "green", "hall", "harris", "hayes", "hernandez", "hill", "howard", "hughes", "jackson", "jenkins", "johnson", "jones", "kelly", "king", "lee", "lewis", "long", "lopez", "martin", "martinez", "miller", "mitchell", "moore", "morgan", "morris", "murphy", "nelson", "parker", "perez", "perry", "phillips", "powell", "price", "reed", "reyes", "rivera", "roberts", "robinson", "rodriguez", "rogers", "ross", "russell", "sanchez", "sanders", "scott", "smith", "stewart", "sullivan", "taylor", "thomas", "thompson", "torres", "turner", "walker", "ward", "watson", "white", "williams", "wilson", "wood", "wright", "young"],
|
||||||
"last": ["adams", "allen", "anderson", "bailey", "baker", "bennett", "brooks", "brown", "campbell", "carter", "clark", "collins", "cook", "cooper", "davis", "edwards", "evans", "fisher", "flores", "foster", "garcia", "gonzalez", "gray", "green", "hall", "harris", "hayes", "hernandez", "hill", "howard", "hughes", "jackson", "jenkins", "johnson", "jones", "kelly", "king", "lee", "lewis", "long", "lopez", "martin", "martinez", "miller", "mitchell", "moore", "morgan", "morris", "murphy", "nelson", "parker", "perez", "perry", "phillips", "powell", "price", "reed", "reyes", "rivera", "roberts", "robinson", "rodriguez", "rogers", "ross", "russell", "sanchez", "sanders", "scott", "smith", "stewart", "sullivan", "taylor", "thomas", "thompson", "torres", "turner", "walker", "ward", "watson", "white", "williams", "wilson", "wood", "wright", "young"],
|
"sep": [{ "format": ".", "weight": 2 }, "_", "-", ""],
|
||||||
"sep": [".", ".", "_", "-", ""],
|
"n": [{ "format": "", "weight": 3 }, "1", "7", "21", "42", "88", "99", "2024"]
|
||||||
"n": ["", "", "", "1", "7", "21", "42", "88", "99", "2024"]
|
},
|
||||||
},
|
{
|
||||||
{
|
"format": "{adj}{noun}{n}",
|
||||||
"format": "{adj}{noun}{n}",
|
"weight": 2,
|
||||||
"weight": 2,
|
"adj": ["blue", "cool", "dark", "epic", "fast", "fluffy", "frosty", "funky", "fuzzy", "golden", "grumpy", "happy", "hyper", "jazzy", "lazy", "lucky", "mega", "mighty", "neon", "ninja", "quiet", "rad", "rusty", "salty", "shiny", "silent", "sleepy", "snappy", "sneaky", "spicy", "super", "swift", "turbo", "wild", "witty", "zany"],
|
||||||
"adj": ["blue", "cool", "dark", "epic", "fast", "fluffy", "frosty", "funky", "fuzzy", "golden", "grumpy", "happy", "hyper", "jazzy", "lazy", "lucky", "mega", "mighty", "neon", "ninja", "quiet", "rad", "rusty", "salty", "shiny", "silent", "sleepy", "snappy", "sneaky", "spicy", "super", "swift", "turbo", "wild", "witty", "zany"],
|
"noun": ["badger", "banana", "comet", "dragon", "falcon", "ferret", "goblin", "hamster", "koala", "llama", "mango", "muffin", "narwhal", "ninja", "noodle", "otter", "panda", "penguin", "pickle", "pirate", "pixel", "potato", "pretzel", "raccoon", "raptor", "robot", "taco", "toast", "trunk", "turtle", "unicorn", "viking", "waffle", "walrus", "wizard", "yeti"],
|
||||||
"noun": ["badger", "banana", "comet", "dragon", "falcon", "ferret", "goblin", "hamster", "koala", "llama", "mango", "muffin", "narwhal", "ninja", "noodle", "otter", "panda", "penguin", "pickle", "pirate", "pixel", "potato", "pretzel", "raccoon", "raptor", "robot", "taco", "toast", "trunk", "turtle", "unicorn", "viking", "waffle", "walrus", "wizard", "yeti"],
|
"n": [{ "format": "", "weight": 2 }, "7", "13", "14", "42", "77", "99", "123", "420", "2024"]
|
||||||
"n": ["", "", "7", "13", "14", "42", "77", "99", "123", "420", "2024"]
|
},
|
||||||
},
|
{
|
||||||
{
|
"format": "{w}{n}",
|
||||||
"format": "{w}{n}",
|
"w": ["blooper", "blupp", "boop", "byteme", "coolcat", "derpina", "doomguy", "epicgamer", "fizzbuzz", "glitchlord", "glorptron", "gronkzilla", "kapowski", "lazerwolf", "megashark", "mooncat", "narfle", "nightowl", "noodlemaster", "oofster", "pixelfox", "pixelpusher", "plonkers", "retrowave", "snork", "splatmaster", "stardust", "superdog", "voidwalker", "vroom", "wahoo", "waooooh", "wootwoot", "yeetus", "zapper", "zoomzoom"],
|
||||||
"weight": 1,
|
"n": [{ "format": "", "weight": 2 }, "7", "14", "42", "69", "99", "123", "420", "1337", "2000", "9000"]
|
||||||
"w": ["blooper", "blupp", "boop", "byteme", "coolcat", "derpina", "doomguy", "epicgamer", "fizzbuzz", "glitchlord", "glorptron", "gronkzilla", "kapowski", "lazerwolf", "megashark", "mooncat", "narfle", "nightowl", "noodlemaster", "oofster", "pixelfox", "pixelpusher", "plonkers", "retrowave", "snork", "splatmaster", "stardust", "superdog", "voidwalker", "vroom", "wahoo", "waooooh", "wootwoot", "yeetus", "zapper", "zoomzoom"],
|
}
|
||||||
"n": ["", "", "7", "14", "42", "69", "99", "123", "420", "1337", "2000", "9000"]
|
],
|
||||||
}
|
"domain": ["example.com", "example.org", "example.net", "example.io", "example.co", "example.dev", "example.app", "mail.example.com", "mail.example.org", "webmail.example.net", "inbox.example.com", "post.example.org", "demo.example.net", "test.example.org", "dev.example.io"]
|
||||||
],
|
}
|
||||||
"domain": ["example.com", "example.org", "example.net", "example.io", "example.co", "example.dev", "example.app", "mail.example.com", "mail.example.org", "webmail.example.net", "inbox.example.com", "post.example.org", "demo.example.net", "test.example.org", "dev.example.io"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
+6
-4
@@ -2,9 +2,11 @@
|
|||||||
{
|
{
|
||||||
"format": "{o}.{o}.{o}.{o}",
|
"format": "{o}.{o}.{o}.{o}",
|
||||||
"weight": 8,
|
"weight": 8,
|
||||||
"o": [{ "format": "0", "weight": 3 }, { "format": "10", "weight": 4 }, { "format": "#100", "weight": 2 }]
|
"o": [
|
||||||
|
{ "format": "{digits(1)}", "weight": 3 },
|
||||||
|
{ "format": "{int(10,99)}", "weight": 4 },
|
||||||
|
{ "format": "1{digits(2)}", "weight": 2 }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
"{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}"
|
||||||
"format": "{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|||||||
+7
-16
@@ -1,16 +1,7 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "{prefix}{femalefirst|malefirst} {last}",
|
||||||
"format": "{prefix}{femalefirst|malefirst} {last}",
|
"femalefirst": ["Abigail", "Addison", "Amelia", "Aria", "Aubrey", "Audrey", "Aurora", "Ava", "Bella", "Brooklyn", "Camila", "Caroline", "Charlotte", "Chloe", "Claire", "Eleanor", "Elizabeth", "Ella", "Ellie", "Emily", "Emma", "Evelyn", "Gianna", "Grace", "Hannah", "Harper", "Hazel", "Isabella", "Layla", "Leah", "Lillian", "Lily", "Lucy", "Luna", "Madison", "Maya", "Mia", "Mila", "Naomi", "Natalie", "Nora", "Olivia", "Paisley", "Penelope", "Riley", "Savannah", "Scarlett", "Sofia", "Sophia", "Stella", "Victoria", "Violet", "Zoe"],
|
||||||
"femalefirst": ["Abigail", "Addison", "Amelia", "Aria", "Aubrey", "Audrey", "Aurora", "Ava", "Bella", "Brooklyn", "Camila", "Caroline", "Charlotte", "Chloe", "Claire", "Eleanor", "Elizabeth", "Ella", "Ellie", "Emily", "Emma", "Evelyn", "Gianna", "Grace", "Hannah", "Harper", "Hazel", "Isabella", "Layla", "Leah", "Lillian", "Lily", "Lucy", "Luna", "Madison", "Maya", "Mia", "Mila", "Naomi", "Natalie", "Nora", "Olivia", "Paisley", "Penelope", "Riley", "Savannah", "Scarlett", "Sofia", "Sophia", "Stella", "Victoria", "Violet", "Zoe"],
|
"malefirst": ["Aiden", "Alexander", "Andrew", "Anthony", "Asher", "Benjamin", "Caleb", "Carter", "Charles", "Christopher", "Daniel", "David", "Dylan", "Elijah", "Ethan", "Ezra", "Gabriel", "Grayson", "Henry", "Isaac", "Jack", "Jackson", "Jacob", "James", "Jayden", "John", "Joseph", "Joshua", "Julian", "Levi", "Liam", "Lincoln", "Logan", "Lucas", "Luke", "Mason", "Mateo", "Matthew", "Michael", "Nathan", "Noah", "Oliver", "Owen", "Samuel", "Sebastian", "Theodore", "Thomas", "William", "Wyatt"],
|
||||||
"malefirst": ["Aiden", "Alexander", "Andrew", "Anthony", "Asher", "Benjamin", "Caleb", "Carter", "Charles", "Christopher", "Daniel", "David", "Dylan", "Elijah", "Ethan", "Ezra", "Gabriel", "Grayson", "Henry", "Isaac", "Jack", "Jackson", "Jacob", "James", "Jayden", "John", "Joseph", "Joshua", "Julian", "Levi", "Liam", "Lincoln", "Logan", "Lucas", "Luke", "Mason", "Mateo", "Matthew", "Michael", "Nathan", "Noah", "Oliver", "Owen", "Samuel", "Sebastian", "Theodore", "Thomas", "William", "Wyatt"],
|
"last": ["Adams", "Allen", "Anderson", "Bailey", "Baker", "Bell", "Bennett", "Brooks", "Brown", "Campbell", "Carter", "Clark", "Collins", "Cook", "Cooper", "Cox", "Davis", "Edwards", "Evans", "Flores", "Foster", "Garcia", "Gonzalez", "Gray", "Green", "Hall", "Harris", "Hayes", "Hernandez", "Hill", "Howard", "Hughes", "Jackson", "James", "Jenkins", "Johnson", "Jones", "Kelly", "King", "Lee", "Lewis", "Long", "Lopez", "Martin", "Martinez", "Miller", "Mitchell", "Moore", "Morgan", "Morris", "Murphy", "Nelson", "Parker", "Perez", "Perry", "Peterson", "Phillips", "Powell", "Price", "Ramirez", "Reed", "Richardson", "Rivera", "Roberts", "Robinson", "Rodriguez", "Rogers", "Ross", "Russell", "Sanchez", "Sanders", "Scott", "Smith", "Stewart", "Sullivan", "Taylor", "Thomas", "Thompson", "Torres", "Turner", "Walker", "Ward", "Watson", "White", "Williams", "Wilson", "Wood", "Wright", "Young"],
|
||||||
"last": ["Adams", "Allen", "Anderson", "Bailey", "Baker", "Bell", "Bennett", "Brooks", "Brown", "Campbell", "Carter", "Clark", "Collins", "Cook", "Cooper", "Cox", "Davis", "Edwards", "Evans", "Flores", "Foster", "Garcia", "Gonzalez", "Gray", "Green", "Hall", "Harris", "Hayes", "Hernandez", "Hill", "Howard", "Hughes", "Jackson", "James", "Jenkins", "Johnson", "Jones", "Kelly", "King", "Lee", "Lewis", "Long", "Lopez", "Martin", "Martinez", "Miller", "Mitchell", "Moore", "Morgan", "Morris", "Murphy", "Nelson", "Parker", "Perez", "Perry", "Peterson", "Phillips", "Powell", "Price", "Ramirez", "Reed", "Richardson", "Rivera", "Roberts", "Robinson", "Rodriguez", "Rogers", "Ross", "Russell", "Sanchez", "Sanders", "Scott", "Smith", "Stewart", "Sullivan", "Taylor", "Thomas", "Thompson", "Torres", "Turner", "Walker", "Ward", "Watson", "White", "Williams", "Wilson", "Wood", "Wright", "Young"],
|
"prefix": ["", { "format": "{title} ", "title": ["Dr", "Miss", "Mr", "Mrs", "Ms", "Mx", "Prof"], "weight": 0.1 }]
|
||||||
"prefix": [
|
}
|
||||||
"",
|
|
||||||
{
|
|
||||||
"format": "{title} ",
|
|
||||||
"title": ["Dr", "Miss", "Mr", "Mrs", "Ms", "Mx", "Prof"],
|
|
||||||
"weight": 0.1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
"format": "({area}) {exch}-{line}",
|
"format": "({area}) {exch}-{line}",
|
||||||
"weight": 2,
|
"weight": 2,
|
||||||
"area": ["202", "212", "213", "305", "312", "404", "415", "469", "503", "512", "602", "617", "646", "702", "718", "720", "737", "773", "786", "808", "917"],
|
"area": ["202", "212", "213", "305", "312", "404", "415", "469", "503", "512", "602", "617", "646", "702", "718", "720", "737", "773", "786", "808", "917"],
|
||||||
"exch": [{ "format": "100" }],
|
"exch": "{int(100,999)}",
|
||||||
"line": [{ "format": "0000" }]
|
"line": "{digits(4)}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"format": "{area}-{exch}-{line}",
|
"format": "{area}-{exch}-{line}",
|
||||||
"area": ["202", "212", "213", "305", "312", "404", "415", "469", "503", "512", "602", "617", "646", "702", "718", "720", "737", "773", "786", "808", "917"],
|
"area": ["202", "212", "213", "305", "312", "404", "415", "469", "503", "512", "602", "617", "646", "702", "718", "720", "737", "773", "786", "808", "917"],
|
||||||
"exch": [{ "format": "100" }],
|
"exch": "{int(100,999)}",
|
||||||
"line": [{ "format": "0000" }]
|
"line": "{digits(4)}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+12
-19
@@ -1,19 +1,12 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "${amt}.{cents}",
|
||||||
"format": "${amt}.{cents}",
|
"amt": [
|
||||||
"amt": [
|
{ "format": "{int(1,9)}", "weight": 2 },
|
||||||
{ "format": "1", "weight": 2 },
|
{ "format": "{int(10,99)}", "weight": 4 },
|
||||||
{ "format": "10", "weight": 4 },
|
{ "format": "{int(100,999)}", "weight": 3 },
|
||||||
{ "format": "100", "weight": 3 },
|
"{int(1,9)},{digits(3)}",
|
||||||
{ "format": "1#,000", "weight": 1 },
|
{ "format": "{int(10,99)},{digits(3)}", "weight": 0.4 },
|
||||||
{ "format": "10#,000", "weight": 0.4 },
|
{ "format": "{int(100,999)},{digits(3)}", "weight": 0.1 }
|
||||||
{ "format": "100#,000", "weight": 0.1 }
|
],
|
||||||
],
|
"cents": [{ "format": "{digits(2)}", "weight": 3 }, "49", "95", "99"]
|
||||||
"cents": [
|
}
|
||||||
{ "format": "00", "weight": 3 },
|
|
||||||
"49",
|
|
||||||
"95",
|
|
||||||
"99"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"noun": ["anchor", "archer", "beacon", "bridge", "captain", "cavern", "cottage", "ember", "engine", "falcon", "ferry", "forest", "fox", "garden", "glacier", "harbor", "hermit", "hollow", "island", "lantern", "lighthouse", "mariner", "meadow", "mountain", "orchard", "otter", "pilgrim", "prairie", "quarry", "raven", "ridge", "river", "sailor", "scholar", "signal", "sparrow", "stranger", "summit", "thicket", "tower", "traveler", "valley", "voyager", "wanderer", "willow", "woodland"],
|
"noun": ["anchor", "archer", "beacon", "bridge", "captain", "cavern", "cottage", "ember", "engine", "falcon", "ferry", "forest", "fox", "garden", "glacier", "harbor", "hermit", "hollow", "island", "lantern", "lighthouse", "mariner", "meadow", "mountain", "orchard", "otter", "pilgrim", "prairie", "quarry", "raven", "ridge", "river", "sailor", "scholar", "signal", "sparrow", "stranger", "summit", "thicket", "tower", "traveler", "valley", "voyager", "wanderer", "willow", "woodland"],
|
||||||
"verb": ["abandons", "anchors", "awakens", "befriends", "builds", "calms", "carries", "chases", "conceals", "crosses", "embraces", "follows", "gathers", "greets", "guards", "guides", "haunts", "heralds", "honors", "kindles", "leads", "mends", "mirrors", "outlasts", "ponders", "reveals", "salutes", "shapes", "shelters", "shields", "summons", "tends", "traces", "weathers"],
|
"verb": ["abandons", "anchors", "awakens", "befriends", "builds", "calms", "carries", "chases", "conceals", "crosses", "embraces", "follows", "gathers", "greets", "guards", "guides", "haunts", "heralds", "honors", "kindles", "leads", "mends", "mirrors", "outlasts", "ponders", "reveals", "salutes", "shapes", "shelters", "shields", "summons", "tends", "traces", "weathers"],
|
||||||
"prep": ["above", "across", "against", "alongside", "amid", "around", "beneath", "beside", "between", "beyond", "near", "over", "past", "through", "toward", "under", "within"],
|
"prep": ["above", "across", "against", "alongside", "amid", "around", "beneath", "beside", "between", "beyond", "near", "over", "past", "through", "toward", "under", "within"],
|
||||||
"end": [".", ".", ".", ".", "!"]
|
"end": [{ "format": ".", "weight": 4 }, "!"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"format": "{lead} {noun} {adv} {verb} {prep} {adj} {noun}{end}",
|
"format": "{lead} {noun} {adv} {verb} {prep} {adj} {noun}{end}",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"verb": ["builds", "carries", "chases", "crosses", "follows", "gathers", "guards", "guides", "leads", "mirrors", "outlasts", "reveals", "shapes", "shelters", "summons", "tends", "traces", "weathers"],
|
"verb": ["builds", "carries", "chases", "crosses", "follows", "gathers", "guards", "guides", "leads", "mirrors", "outlasts", "reveals", "shapes", "shelters", "summons", "tends", "traces", "weathers"],
|
||||||
"adj": ["ancient", "bright", "distant", "fierce", "fragile", "gentle", "golden", "hidden", "lonely", "quiet", "restless", "rugged", "silent", "stormy", "weary", "wild"],
|
"adj": ["ancient", "bright", "distant", "fierce", "fragile", "gentle", "golden", "hidden", "lonely", "quiet", "restless", "rugged", "silent", "stormy", "weary", "wild"],
|
||||||
"prep": ["across", "against", "alongside", "amid", "beneath", "between", "beyond", "near", "over", "past", "through", "toward", "within"],
|
"prep": ["across", "against", "alongside", "amid", "beneath", "between", "beyond", "near", "over", "past", "through", "toward", "within"],
|
||||||
"end": [".", ".", ".", "!"]
|
"end": [{ "format": ".", "weight": 3 }, "!"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"format": "{q} {adj} {noun} {verb} {prep} {adj} {noun}?",
|
"format": "{q} {adj} {noun} {verb} {prep} {adj} {noun}?",
|
||||||
|
|||||||
+1
-3
@@ -1,3 +1 @@
|
|||||||
[
|
"{int(100,999)}-{digits(2)}-{digits(4)}"
|
||||||
{ "format": "100-00-0000" }
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "{hour}:{minute} {ampm}",
|
||||||
"format": "{hour}:{minute} {ampm}",
|
"hour": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"],
|
||||||
"hour": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"],
|
"minute": { "format": "{t}{digits(1)}", "t": ["0", "1", "2", "3", "4", "5"] },
|
||||||
"minute": [{ "format": "{t}0", "t": ["0", "1", "2", "3", "4", "5"] }],
|
"ampm": ["AM", "PM"]
|
||||||
"ampm": ["AM", "PM"]
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
+17
-12
@@ -1,12 +1,17 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "https://{host}{path}",
|
||||||
"format": "https://{host}{path}",
|
"host": ["example.com", "www.example.com", "example.org", "www.example.org", "example.net", "example.io", "example.co", "blog.example.com", "blog.example.net", "shop.example.com", "store.example.org", "app.example.io", "api.example.com", "docs.example.org", "news.example.net", "mail.example.com"],
|
||||||
"host": ["example.com", "www.example.com", "example.org", "www.example.org", "example.net", "example.io", "example.co", "blog.example.com", "blog.example.net", "shop.example.com", "store.example.org", "app.example.io", "api.example.com", "docs.example.org", "news.example.net", "mail.example.com"],
|
"path": [
|
||||||
"path": [
|
{ "format": "", "weight": 2 },
|
||||||
"",
|
{
|
||||||
"",
|
"format": "/{seg}",
|
||||||
{ "format": "/{seg}", "seg": ["about", "account", "blog", "careers", "cart", "categories", "checkout", "contact", "dashboard", "downloads", "events", "faq", "features", "help", "home", "items", "login", "news", "order", "pricing", "privacy", "products", "profile", "register", "search", "settings", "signup", "support", "terms", "welcome"] },
|
"seg": ["about", "account", "blog", "careers", "cart", "categories", "checkout", "contact", "dashboard", "downloads", "events", "faq", "features", "help", "home", "items", "login", "news", "order", "pricing", "privacy", "products", "profile", "register", "search", "settings", "signup", "support", "terms", "welcome"]
|
||||||
{ "format": "/{seg}/{sub}", "weight": 0.5, "seg": ["blog", "docs", "help", "products", "category", "user"], "sub": ["getting-started", "overview", "guide", "2024", "latest", "popular", "archive", "details"] }
|
},
|
||||||
]
|
{
|
||||||
}
|
"format": "/{seg}/{sub}",
|
||||||
]
|
"weight": 0.5,
|
||||||
|
"seg": ["blog", "docs", "help", "products", "category", "user"],
|
||||||
|
"sub": ["getting-started", "overview", "guide", "2024", "latest", "popular", "archive", "details"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,21 +4,20 @@
|
|||||||
"weight": 2,
|
"weight": 2,
|
||||||
"first": ["alex", "ash", "avery", "bailey", "blake", "cameron", "casey", "charlie", "chris", "dakota", "drew", "elliot", "emerson", "finley", "frankie", "harley", "hayden", "jamie", "jordan", "kai", "kendall", "lane", "logan", "morgan", "parker", "quinn", "reese", "riley", "rowan", "sam", "sawyer", "skyler", "spencer", "taylor"],
|
"first": ["alex", "ash", "avery", "bailey", "blake", "cameron", "casey", "charlie", "chris", "dakota", "drew", "elliot", "emerson", "finley", "frankie", "harley", "hayden", "jamie", "jordan", "kai", "kendall", "lane", "logan", "morgan", "parker", "quinn", "reese", "riley", "rowan", "sam", "sawyer", "skyler", "spencer", "taylor"],
|
||||||
"last": ["adams", "brooks", "carter", "cole", "evans", "fisher", "fox", "grant", "gray", "hayes", "hunt", "lane", "lowe", "marsh", "mason", "morris", "north", "page", "quinn", "reed", "rhodes", "shaw", "stone", "vale", "wells", "west", "wolfe"],
|
"last": ["adams", "brooks", "carter", "cole", "evans", "fisher", "fox", "grant", "gray", "hayes", "hunt", "lane", "lowe", "marsh", "mason", "morris", "north", "page", "quinn", "reed", "rhodes", "shaw", "stone", "vale", "wells", "west", "wolfe"],
|
||||||
"sep": ["", "", "", ".", "_"],
|
"sep": [{ "format": "", "weight": 3 }, ".", "_"],
|
||||||
"n": ["", "", "", "", "1", "7", "12", "23", "42", "77", "99", "2024"]
|
"n": [{ "format": "", "weight": 4 }, "1", "7", "12", "23", "42", "77", "99", "2024"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"format": "{adj}{sep}{noun}{n}",
|
"format": "{adj}{sep}{noun}{n}",
|
||||||
"weight": 2,
|
"weight": 2,
|
||||||
"adj": ["bluish", "brave", "chill", "cosmic", "cool", "dizzy", "electric", "epic", "fluffy", "frosty", "funky", "fuzzy", "golden", "groovy", "grumpy", "happy", "hyper", "jazzy", "lucky", "mega", "mighty", "neon", "nimble", "quirky", "rad", "rusty", "salty", "shiny", "silent", "sleepy", "snappy", "sneaky", "sparkly", "spicy", "sunny", "super", "swift", "turbo", "wild", "witty", "zany"],
|
"adj": ["bluish", "brave", "chill", "cosmic", "cool", "dizzy", "electric", "epic", "fluffy", "frosty", "funky", "fuzzy", "golden", "groovy", "grumpy", "happy", "hyper", "jazzy", "lucky", "mega", "mighty", "neon", "nimble", "quirky", "rad", "rusty", "salty", "shiny", "silent", "sleepy", "snappy", "sneaky", "sparkly", "spicy", "sunny", "super", "swift", "turbo", "wild", "witty", "zany"],
|
||||||
"noun": ["badger", "biscuit", "cactus", "comet", "donut", "dragon", "falcon", "ferret", "gizmo", "goblin", "hamster", "koala", "llama", "mango", "muffin", "narwhal", "noodle", "otter", "panda", "penguin", "pickle", "pirate", "pixel", "potato", "pretzel", "raccoon", "raptor", "robot", "taco", "toast", "trunk", "turtle", "unicorn", "viking", "waffle", "walrus", "wizard", "yeti"],
|
"noun": ["badger", "biscuit", "cactus", "comet", "donut", "dragon", "falcon", "ferret", "gizmo", "goblin", "hamster", "koala", "llama", "mango", "muffin", "narwhal", "noodle", "otter", "panda", "penguin", "pickle", "pirate", "pixel", "potato", "pretzel", "raccoon", "raptor", "robot", "taco", "toast", "trunk", "turtle", "unicorn", "viking", "waffle", "walrus", "wizard", "yeti"],
|
||||||
"sep": ["", "", "", ".", "_"],
|
"sep": [{ "format": "", "weight": 3 }, ".", "_"],
|
||||||
"n": ["", "", "7", "13", "42", "77", "99", "123", "420", "2024"]
|
"n": [{ "format": "", "weight": 2 }, "7", "13", "42", "77", "99", "123", "420", "2024"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"format": "{w}{n}",
|
"format": "{w}{n}",
|
||||||
"weight": 1,
|
|
||||||
"w": ["blooper", "blupp", "boop", "byteme", "coolcat", "derpina", "doomguy", "epicgamer", "fizzbuzz", "glitchlord", "glorptron", "gronkzilla", "kapowski", "lazerwolf", "megashark", "mooncat", "narfle", "nightowl", "noodlemaster", "oofster", "pixelfox", "pixelpusher", "plonkers", "retrowave", "snork", "splatmaster", "stardust", "superdog", "voidwalker", "vroom", "wahoo", "waooooh", "wootwoot", "yeetus", "zapper", "zoomzoom"],
|
"w": ["blooper", "blupp", "boop", "byteme", "coolcat", "derpina", "doomguy", "epicgamer", "fizzbuzz", "glitchlord", "glorptron", "gronkzilla", "kapowski", "lazerwolf", "megashark", "mooncat", "narfle", "nightowl", "noodlemaster", "oofster", "pixelfox", "pixelpusher", "plonkers", "retrowave", "snork", "splatmaster", "stardust", "superdog", "voidwalker", "vroom", "wahoo", "waooooh", "wootwoot", "yeetus", "zapper", "zoomzoom"],
|
||||||
"n": ["", "", "7", "14", "42", "69", "99", "123", "420", "1337", "2000", "9000"]
|
"n": [{ "format": "", "weight": 2 }, "7", "14", "42", "69", "99", "123", "420", "1337", "2000", "9000"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "{pre}{n}.{n}.{n}{suffix}",
|
||||||
"format": "{pre}{n}.{n}.{n}{suffix}",
|
"n": [{ "format": "{digits(1)}", "weight": 3 }, "{int(10,99)}"],
|
||||||
"n": [{ "format": "0", "weight": 3 }, { "format": "10" }],
|
"pre": ["", { "format": "v", "weight": 0.4 }],
|
||||||
"pre": ["", { "format": "v", "weight": 0.4 }],
|
"suffix": ["", { "format": "-{tag}.{m}", "tag": ["alpha", "beta", "rc"], "m": ["1", "2", "3"], "weight": 0.3 }]
|
||||||
"suffix": ["", { "format": "-{tag}.{m}", "tag": ["alpha", "beta", "rc"], "m": ["1", "2", "3"], "weight": 0.3 }]
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
+8
-8
@@ -1,10 +1,10 @@
|
|||||||
[
|
[
|
||||||
{ "format": "{maker} {model}", "maker": ["BMW"], "model": ["3 Series", "5 Series", "X3", "X5"] },
|
{ "format": "{maker} {model}", "maker": "BMW", "model": ["3 Series", "5 Series", "X3", "X5"] },
|
||||||
{ "format": "{maker} {model}", "maker": ["Ford"], "model": ["Fiesta", "Focus", "Mustang", "Explorer"] },
|
{ "format": "{maker} {model}", "maker": "Ford", "model": ["Fiesta", "Focus", "Mustang", "Explorer"] },
|
||||||
{ "format": "{maker} {model}", "maker": ["Honda"], "model": ["Civic", "Accord", "CR-V", "Jazz"] },
|
{ "format": "{maker} {model}", "maker": "Honda", "model": ["Civic", "Accord", "CR-V", "Jazz"] },
|
||||||
{ "format": "{maker} {model}", "maker": ["Mercedes-Benz"], "model": ["A-Class", "C-Class", "E-Class", "GLC"] },
|
{ "format": "{maker} {model}", "maker": "Mercedes-Benz", "model": ["A-Class", "C-Class", "E-Class", "GLC"] },
|
||||||
{ "format": "{maker} {model}", "maker": ["Tesla"], "model": ["Model 3", "Model S", "Model X", "Model Y"] },
|
{ "format": "{maker} {model}", "maker": "Tesla", "model": ["Model 3", "Model S", "Model X", "Model Y"] },
|
||||||
{ "format": "{maker} {model}", "maker": ["Toyota"], "model": ["Corolla", "Camry", "RAV4", "Yaris"] },
|
{ "format": "{maker} {model}", "maker": "Toyota", "model": ["Corolla", "Camry", "RAV4", "Yaris"] },
|
||||||
{ "format": "{maker} {model}", "maker": ["Volkswagen"], "model": ["Golf", "Passat", "Polo", "Tiguan"] },
|
{ "format": "{maker} {model}", "maker": "Volkswagen", "model": ["Golf", "Passat", "Polo", "Tiguan"] },
|
||||||
{ "format": "{maker} {model}", "maker": ["Volvo"], "model": ["XC40", "XC60", "XC90", "V60"] }
|
{ "format": "{maker} {model}", "maker": "Volvo", "model": ["XC40", "XC60", "XC90", "V60"] }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,7 +1 @@
|
|||||||
[
|
{ "format": "{lat}, {lon}", "lat": "{float(-90,90,6)}", "lon": "{float(-180,180,6)}" }
|
||||||
{
|
|
||||||
"format": "{lat}, {lon}",
|
|
||||||
"lat": { "format": "{float(-90,90,6)}" },
|
|
||||||
"lon": { "format": "{float(-180,180,6)}" }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
+20
-20
@@ -1,22 +1,22 @@
|
|||||||
[
|
[
|
||||||
{ "format": "{name}", "name": ["Australia"], "alpha2": ["AU"], "alpha3": ["AUS"] },
|
{ "format": "{name}", "name": "Australia", "alpha2": "AU", "alpha3": "AUS" },
|
||||||
{ "format": "{name}", "name": ["Brazil"], "alpha2": ["BR"], "alpha3": ["BRA"] },
|
{ "format": "{name}", "name": "Brazil", "alpha2": "BR", "alpha3": "BRA" },
|
||||||
{ "format": "{name}", "name": ["Canada"], "alpha2": ["CA"], "alpha3": ["CAN"] },
|
{ "format": "{name}", "name": "Canada", "alpha2": "CA", "alpha3": "CAN" },
|
||||||
{ "format": "{name}", "name": ["China"], "alpha2": ["CN"], "alpha3": ["CHN"] },
|
{ "format": "{name}", "name": "China", "alpha2": "CN", "alpha3": "CHN" },
|
||||||
{ "format": "{name}", "name": ["Denmark"], "alpha2": ["DK"], "alpha3": ["DNK"] },
|
{ "format": "{name}", "name": "Denmark", "alpha2": "DK", "alpha3": "DNK" },
|
||||||
{ "format": "{name}", "name": ["Finland"], "alpha2": ["FI"], "alpha3": ["FIN"] },
|
{ "format": "{name}", "name": "Finland", "alpha2": "FI", "alpha3": "FIN" },
|
||||||
{ "format": "{name}", "name": ["France"], "alpha2": ["FR"], "alpha3": ["FRA"] },
|
{ "format": "{name}", "name": "France", "alpha2": "FR", "alpha3": "FRA" },
|
||||||
{ "format": "{name}", "name": ["Germany"], "alpha2": ["DE"], "alpha3": ["DEU"] },
|
{ "format": "{name}", "name": "Germany", "alpha2": "DE", "alpha3": "DEU" },
|
||||||
{ "format": "{name}", "name": ["India"], "alpha2": ["IN"], "alpha3": ["IND"] },
|
{ "format": "{name}", "name": "India", "alpha2": "IN", "alpha3": "IND" },
|
||||||
{ "format": "{name}", "name": ["Italy"], "alpha2": ["IT"], "alpha3": ["ITA"] },
|
{ "format": "{name}", "name": "Italy", "alpha2": "IT", "alpha3": "ITA" },
|
||||||
{ "format": "{name}", "name": ["Japan"], "alpha2": ["JP"], "alpha3": ["JPN"] },
|
{ "format": "{name}", "name": "Japan", "alpha2": "JP", "alpha3": "JPN" },
|
||||||
{ "format": "{name}", "name": ["Mexico"], "alpha2": ["MX"], "alpha3": ["MEX"] },
|
{ "format": "{name}", "name": "Mexico", "alpha2": "MX", "alpha3": "MEX" },
|
||||||
{ "format": "{name}", "name": ["Netherlands"], "alpha2": ["NL"], "alpha3": ["NLD"] },
|
{ "format": "{name}", "name": "Netherlands", "alpha2": "NL", "alpha3": "NLD" },
|
||||||
{ "format": "{name}", "name": ["Norway"], "alpha2": ["NO"], "alpha3": ["NOR"] },
|
{ "format": "{name}", "name": "Norway", "alpha2": "NO", "alpha3": "NOR" },
|
||||||
{ "format": "{name}", "name": ["Poland"], "alpha2": ["PL"], "alpha3": ["POL"] },
|
{ "format": "{name}", "name": "Poland", "alpha2": "PL", "alpha3": "POL" },
|
||||||
{ "format": "{name}", "name": ["Spain"], "alpha2": ["ES"], "alpha3": ["ESP"] },
|
{ "format": "{name}", "name": "Spain", "alpha2": "ES", "alpha3": "ESP" },
|
||||||
{ "format": "{name}", "name": ["Sweden"], "alpha2": ["SE"], "alpha3": ["SWE"] },
|
{ "format": "{name}", "name": "Sweden", "alpha2": "SE", "alpha3": "SWE" },
|
||||||
{ "format": "{name}", "name": ["Switzerland"], "alpha2": ["CH"], "alpha3": ["CHE"] },
|
{ "format": "{name}", "name": "Switzerland", "alpha2": "CH", "alpha3": "CHE" },
|
||||||
{ "format": "{name}", "name": ["United Kingdom"], "alpha2": ["GB"], "alpha3": ["GBR"] },
|
{ "format": "{name}", "name": "United Kingdom", "alpha2": "GB", "alpha3": "GBR" },
|
||||||
{ "format": "{name}", "name": ["United States"], "alpha2": ["US"], "alpha3": ["USA"] }
|
{ "format": "{name}", "name": "United States", "alpha2": "US", "alpha3": "USA" }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
[
|
[
|
||||||
{ "format": "4{d}{luhn()}", "d": { "format": "0", "repeat": 14 }, "weight": 4 },
|
{
|
||||||
{ "format": "5{m}{d}{luhn()}", "m": ["1", "2", "3", "4", "5"], "d": { "format": "0", "repeat": 13 }, "weight": 3 },
|
"format": "4{d}{luhn()}",
|
||||||
{ "format": "3{m}{d}{luhn()}", "m": ["4", "7"], "d": { "format": "0", "repeat": 12 }, "weight": 1 }
|
"d": { "format": "{digits(1)}", "repeat": 14 },
|
||||||
|
"weight": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "5{m}{d}{luhn()}",
|
||||||
|
"m": ["1", "2", "3", "4", "5"],
|
||||||
|
"d": { "format": "{digits(1)}", "repeat": 13 },
|
||||||
|
"weight": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "3{m}{d}{luhn()}",
|
||||||
|
"m": ["4", "7"],
|
||||||
|
"d": { "format": "{digits(1)}", "repeat": 12 }
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+16
-16
@@ -1,18 +1,18 @@
|
|||||||
[
|
[
|
||||||
{ "format": "{code}", "code": ["AUD"], "name": ["Australian Dollar"], "symbol": ["$"] },
|
{ "format": "{code}", "code": "AUD", "name": "Australian Dollar", "symbol": "$" },
|
||||||
{ "format": "{code}", "code": ["BRL"], "name": ["Brazilian Real"], "symbol": ["R$"] },
|
{ "format": "{code}", "code": "BRL", "name": "Brazilian Real", "symbol": "R$" },
|
||||||
{ "format": "{code}", "code": ["CAD"], "name": ["Canadian Dollar"], "symbol": ["$"] },
|
{ "format": "{code}", "code": "CAD", "name": "Canadian Dollar", "symbol": "$" },
|
||||||
{ "format": "{code}", "code": ["CHF"], "name": ["Swiss Franc"], "symbol": ["CHF"] },
|
{ "format": "{code}", "code": "CHF", "name": "Swiss Franc", "symbol": "CHF" },
|
||||||
{ "format": "{code}", "code": ["CNY"], "name": ["Chinese Yuan"], "symbol": ["¥"] },
|
{ "format": "{code}", "code": "CNY", "name": "Chinese Yuan", "symbol": "¥" },
|
||||||
{ "format": "{code}", "code": ["DKK"], "name": ["Danish Krone"], "symbol": ["kr"] },
|
{ "format": "{code}", "code": "DKK", "name": "Danish Krone", "symbol": "kr" },
|
||||||
{ "format": "{code}", "code": ["EUR"], "name": ["Euro"], "symbol": ["€"] },
|
{ "format": "{code}", "code": "EUR", "name": "Euro", "symbol": "€" },
|
||||||
{ "format": "{code}", "code": ["GBP"], "name": ["Pound Sterling"], "symbol": ["£"] },
|
{ "format": "{code}", "code": "GBP", "name": "Pound Sterling", "symbol": "£" },
|
||||||
{ "format": "{code}", "code": ["INR"], "name": ["Indian Rupee"], "symbol": ["₹"] },
|
{ "format": "{code}", "code": "INR", "name": "Indian Rupee", "symbol": "₹" },
|
||||||
{ "format": "{code}", "code": ["JPY"], "name": ["Japanese Yen"], "symbol": ["¥"] },
|
{ "format": "{code}", "code": "JPY", "name": "Japanese Yen", "symbol": "¥" },
|
||||||
{ "format": "{code}", "code": ["MXN"], "name": ["Mexican Peso"], "symbol": ["$"] },
|
{ "format": "{code}", "code": "MXN", "name": "Mexican Peso", "symbol": "$" },
|
||||||
{ "format": "{code}", "code": ["NOK"], "name": ["Norwegian Krone"], "symbol": ["kr"] },
|
{ "format": "{code}", "code": "NOK", "name": "Norwegian Krone", "symbol": "kr" },
|
||||||
{ "format": "{code}", "code": ["PLN"], "name": ["Polish Zloty"], "symbol": ["zł"] },
|
{ "format": "{code}", "code": "PLN", "name": "Polish Zloty", "symbol": "zł" },
|
||||||
{ "format": "{code}", "code": ["SEK"], "name": ["Swedish Krona"], "symbol": ["kr"] },
|
{ "format": "{code}", "code": "SEK", "name": "Swedish Krona", "symbol": "kr" },
|
||||||
{ "format": "{code}", "code": ["USD"], "name": ["US Dollar"], "symbol": ["$"] },
|
{ "format": "{code}", "code": "USD", "name": "US Dollar", "symbol": "$" },
|
||||||
{ "format": "{code}", "code": ["ZAR"], "name": ["South African Rand"], "symbol": ["R"] }
|
{ "format": "{code}", "code": "ZAR", "name": "South African Rand", "symbol": "R" }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1 @@
|
|||||||
[
|
["😀", "😂", "😍", "🤔", "😎", "😭", "😡", "👍", "👎", "🙏", "👏", "🙌", "💪", "🔥", "✨", "🎉", "❤️", "💔", "💯", "👀", "🚀", "⭐", "🌈", "☀️", "🌙", "⚡", "❄️", "🍕", "🍔", "🍺", "☕", "🎂", "🐶", "🐱", "🦊", "🐢", "🦄", "🌸", "🌍", "💡"]
|
||||||
"😀", "😂", "😍", "🤔", "😎", "😭", "😡", "👍", "👎", "🙏",
|
|
||||||
"👏", "🙌", "💪", "🔥", "✨", "🎉", "❤️", "💔", "💯", "👀",
|
|
||||||
"🚀", "⭐", "🌈", "☀️", "🌙", "⚡", "❄️", "🍕", "🍔", "🍺",
|
|
||||||
"☕", "🎂", "🐶", "🐱", "🦊", "🐢", "🦄", "🌸", "🌍", "💡"
|
|
||||||
]
|
|
||||||
|
|||||||
+16
-16
@@ -1,18 +1,18 @@
|
|||||||
[
|
[
|
||||||
{ "format": "{code} {reason}", "code": ["200"], "reason": ["OK"] },
|
{ "format": "{code} {reason}", "code": "200", "reason": "OK" },
|
||||||
{ "format": "{code} {reason}", "code": ["201"], "reason": ["Created"] },
|
{ "format": "{code} {reason}", "code": "201", "reason": "Created" },
|
||||||
{ "format": "{code} {reason}", "code": ["204"], "reason": ["No Content"] },
|
{ "format": "{code} {reason}", "code": "204", "reason": "No Content" },
|
||||||
{ "format": "{code} {reason}", "code": ["301"], "reason": ["Moved Permanently"] },
|
{ "format": "{code} {reason}", "code": "301", "reason": "Moved Permanently" },
|
||||||
{ "format": "{code} {reason}", "code": ["302"], "reason": ["Found"] },
|
{ "format": "{code} {reason}", "code": "302", "reason": "Found" },
|
||||||
{ "format": "{code} {reason}", "code": ["304"], "reason": ["Not Modified"] },
|
{ "format": "{code} {reason}", "code": "304", "reason": "Not Modified" },
|
||||||
{ "format": "{code} {reason}", "code": ["400"], "reason": ["Bad Request"] },
|
{ "format": "{code} {reason}", "code": "400", "reason": "Bad Request" },
|
||||||
{ "format": "{code} {reason}", "code": ["401"], "reason": ["Unauthorized"] },
|
{ "format": "{code} {reason}", "code": "401", "reason": "Unauthorized" },
|
||||||
{ "format": "{code} {reason}", "code": ["403"], "reason": ["Forbidden"] },
|
{ "format": "{code} {reason}", "code": "403", "reason": "Forbidden" },
|
||||||
{ "format": "{code} {reason}", "code": ["404"], "reason": ["Not Found"] },
|
{ "format": "{code} {reason}", "code": "404", "reason": "Not Found" },
|
||||||
{ "format": "{code} {reason}", "code": ["409"], "reason": ["Conflict"] },
|
{ "format": "{code} {reason}", "code": "409", "reason": "Conflict" },
|
||||||
{ "format": "{code} {reason}", "code": ["422"], "reason": ["Unprocessable Entity"] },
|
{ "format": "{code} {reason}", "code": "422", "reason": "Unprocessable Entity" },
|
||||||
{ "format": "{code} {reason}", "code": ["429"], "reason": ["Too Many Requests"] },
|
{ "format": "{code} {reason}", "code": "429", "reason": "Too Many Requests" },
|
||||||
{ "format": "{code} {reason}", "code": ["500"], "reason": ["Internal Server Error"] },
|
{ "format": "{code} {reason}", "code": "500", "reason": "Internal Server Error" },
|
||||||
{ "format": "{code} {reason}", "code": ["502"], "reason": ["Bad Gateway"] },
|
{ "format": "{code} {reason}", "code": "502", "reason": "Bad Gateway" },
|
||||||
{ "format": "{code} {reason}", "code": ["503"], "reason": ["Service Unavailable"] }
|
{ "format": "{code} {reason}", "code": "503", "reason": "Service Unavailable" }
|
||||||
]
|
]
|
||||||
|
|||||||
+16
-16
@@ -1,18 +1,18 @@
|
|||||||
[
|
[
|
||||||
{ "format": "{name}", "name": ["Arabic"], "code": ["ar"] },
|
{ "format": "{name}", "name": "Arabic", "code": "ar" },
|
||||||
{ "format": "{name}", "name": ["Chinese"], "code": ["zh"] },
|
{ "format": "{name}", "name": "Chinese", "code": "zh" },
|
||||||
{ "format": "{name}", "name": ["Danish"], "code": ["da"] },
|
{ "format": "{name}", "name": "Danish", "code": "da" },
|
||||||
{ "format": "{name}", "name": ["Dutch"], "code": ["nl"] },
|
{ "format": "{name}", "name": "Dutch", "code": "nl" },
|
||||||
{ "format": "{name}", "name": ["English"], "code": ["en"] },
|
{ "format": "{name}", "name": "English", "code": "en" },
|
||||||
{ "format": "{name}", "name": ["Finnish"], "code": ["fi"] },
|
{ "format": "{name}", "name": "Finnish", "code": "fi" },
|
||||||
{ "format": "{name}", "name": ["French"], "code": ["fr"] },
|
{ "format": "{name}", "name": "French", "code": "fr" },
|
||||||
{ "format": "{name}", "name": ["German"], "code": ["de"] },
|
{ "format": "{name}", "name": "German", "code": "de" },
|
||||||
{ "format": "{name}", "name": ["Italian"], "code": ["it"] },
|
{ "format": "{name}", "name": "Italian", "code": "it" },
|
||||||
{ "format": "{name}", "name": ["Japanese"], "code": ["ja"] },
|
{ "format": "{name}", "name": "Japanese", "code": "ja" },
|
||||||
{ "format": "{name}", "name": ["Norwegian"], "code": ["no"] },
|
{ "format": "{name}", "name": "Norwegian", "code": "no" },
|
||||||
{ "format": "{name}", "name": ["Polish"], "code": ["pl"] },
|
{ "format": "{name}", "name": "Polish", "code": "pl" },
|
||||||
{ "format": "{name}", "name": ["Portuguese"], "code": ["pt"] },
|
{ "format": "{name}", "name": "Portuguese", "code": "pt" },
|
||||||
{ "format": "{name}", "name": ["Russian"], "code": ["ru"] },
|
{ "format": "{name}", "name": "Russian", "code": "ru" },
|
||||||
{ "format": "{name}", "name": ["Spanish"], "code": ["es"] },
|
{ "format": "{name}", "name": "Spanish", "code": "es" },
|
||||||
{ "format": "{name}", "name": ["Swedish"], "code": ["sv"] }
|
{ "format": "{name}", "name": "Swedish", "code": "sv" }
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-3
@@ -1,3 +1 @@
|
|||||||
[
|
"{hex(2)}:{hex(2)}:{hex(2)}:{hex(2)}:{hex(2)}:{hex(2)}"
|
||||||
{ "format": "{hex(2)}:{hex(2)}:{hex(2)}:{hex(2)}:{hex(2)}:{hex(2)}" }
|
|
||||||
]
|
|
||||||
|
|||||||
+15
-15
@@ -1,17 +1,17 @@
|
|||||||
[
|
[
|
||||||
{ "format": "{type}", "type": ["application/gzip"], "ext": [".gz"] },
|
{ "format": "{type}", "type": "application/gzip", "ext": ".gz" },
|
||||||
{ "format": "{type}", "type": ["application/json"], "ext": [".json"] },
|
{ "format": "{type}", "type": "application/json", "ext": ".json" },
|
||||||
{ "format": "{type}", "type": ["application/pdf"], "ext": [".pdf"] },
|
{ "format": "{type}", "type": "application/pdf", "ext": ".pdf" },
|
||||||
{ "format": "{type}", "type": ["application/xml"], "ext": [".xml"] },
|
{ "format": "{type}", "type": "application/xml", "ext": ".xml" },
|
||||||
{ "format": "{type}", "type": ["application/zip"], "ext": [".zip"] },
|
{ "format": "{type}", "type": "application/zip", "ext": ".zip" },
|
||||||
{ "format": "{type}", "type": ["audio/mpeg"], "ext": [".mp3"] },
|
{ "format": "{type}", "type": "audio/mpeg", "ext": ".mp3" },
|
||||||
{ "format": "{type}", "type": ["image/gif"], "ext": [".gif"] },
|
{ "format": "{type}", "type": "image/gif", "ext": ".gif" },
|
||||||
{ "format": "{type}", "type": ["image/jpeg"], "ext": [".jpg"] },
|
{ "format": "{type}", "type": "image/jpeg", "ext": ".jpg" },
|
||||||
{ "format": "{type}", "type": ["image/png"], "ext": [".png"] },
|
{ "format": "{type}", "type": "image/png", "ext": ".png" },
|
||||||
{ "format": "{type}", "type": ["image/svg+xml"], "ext": [".svg"] },
|
{ "format": "{type}", "type": "image/svg+xml", "ext": ".svg" },
|
||||||
{ "format": "{type}", "type": ["image/webp"], "ext": [".webp"] },
|
{ "format": "{type}", "type": "image/webp", "ext": ".webp" },
|
||||||
{ "format": "{type}", "type": ["text/csv"], "ext": [".csv"] },
|
{ "format": "{type}", "type": "text/csv", "ext": ".csv" },
|
||||||
{ "format": "{type}", "type": ["text/html"], "ext": [".html"] },
|
{ "format": "{type}", "type": "text/html", "ext": ".html" },
|
||||||
{ "format": "{type}", "type": ["text/plain"], "ext": [".txt"] },
|
{ "format": "{type}", "type": "text/plain", "ext": ".txt" },
|
||||||
{ "format": "{type}", "type": ["video/mp4"], "ext": [".mp4"] }
|
{ "format": "{type}", "type": "video/mp4", "ext": ".mp4" }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
[
|
"{hex(24)}"
|
||||||
{ "format": "{hex(24)}" }
|
|
||||||
]
|
|
||||||
|
|||||||
+1
-29
@@ -1,29 +1 @@
|
|||||||
[
|
["UTC", "Africa/Cairo", "Africa/Johannesburg", "America/Chicago", "America/Denver", "America/Los_Angeles", "America/Mexico_City", "America/New_York", "America/Sao_Paulo", "America/Toronto", "Asia/Dubai", "Asia/Hong_Kong", "Asia/Kolkata", "Asia/Shanghai", "Asia/Singapore", "Asia/Tokyo", "Australia/Sydney", "Europe/Amsterdam", "Europe/Berlin", "Europe/London", "Europe/Madrid", "Europe/Moscow", "Europe/Oslo", "Europe/Paris", "Europe/Stockholm", "Europe/Warsaw", "Pacific/Auckland"]
|
||||||
"UTC",
|
|
||||||
"Africa/Cairo",
|
|
||||||
"Africa/Johannesburg",
|
|
||||||
"America/Chicago",
|
|
||||||
"America/Denver",
|
|
||||||
"America/Los_Angeles",
|
|
||||||
"America/Mexico_City",
|
|
||||||
"America/New_York",
|
|
||||||
"America/Sao_Paulo",
|
|
||||||
"America/Toronto",
|
|
||||||
"Asia/Dubai",
|
|
||||||
"Asia/Hong_Kong",
|
|
||||||
"Asia/Kolkata",
|
|
||||||
"Asia/Shanghai",
|
|
||||||
"Asia/Singapore",
|
|
||||||
"Asia/Tokyo",
|
|
||||||
"Australia/Sydney",
|
|
||||||
"Europe/Amsterdam",
|
|
||||||
"Europe/Berlin",
|
|
||||||
"Europe/London",
|
|
||||||
"Europe/Madrid",
|
|
||||||
"Europe/Moscow",
|
|
||||||
"Europe/Oslo",
|
|
||||||
"Europe/Paris",
|
|
||||||
"Europe/Stockholm",
|
|
||||||
"Europe/Warsaw",
|
|
||||||
"Pacific/Auckland"
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,10 +1 @@
|
|||||||
[
|
["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15", "Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0"]
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0",
|
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0",
|
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15",
|
|
||||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0"
|
|
||||||
]
|
|
||||||
|
|||||||
+1
-6
@@ -1,6 +1 @@
|
|||||||
[
|
{ "format": "{hex(8)}-{hex(4)}-4{hex(3)}-{variant}{hex(3)}-{hex(12)}", "variant": ["8", "9", "a", "b"] }
|
||||||
{
|
|
||||||
"format": "{hex(8)}-{hex(4)}-4{hex(3)}-{variant}{hex(3)}-{hex(12)}",
|
|
||||||
"variant": ["8", "9", "a", "b"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
+21
-25
@@ -1,25 +1,21 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "{street} {street-number}\n{postal-code} {locality}",
|
||||||
"format": "{street} {street-number}\n{postal-code} {locality}",
|
"street": [
|
||||||
"street": [
|
{
|
||||||
{
|
"format": "{first}{last}",
|
||||||
"format": "{first}{last}",
|
"first": ["Ängs", "Bergs", "Björk", "Drottning", "Eke", "Furu", "Hamn", "Köpmans", "Kungs", "Linné", "Norra", "Nybro", "Oden", "Öster", "Park", "Skogs", "Skol", "Slotts", "Söder", "Stations", "Stor", "Strand", "Trädgårds", "Vasa", "Väster"],
|
||||||
"first": ["Ängs", "Bergs", "Björk", "Drottning", "Eke", "Furu", "Hamn", "Köpmans", "Kungs", "Linné", "Norra", "Nybro", "Oden", "Öster", "Park", "Skogs", "Skol", "Slotts", "Söder", "Stations", "Stor", "Strand", "Trädgårds", "Vasa", "Väster"],
|
"last": ["gatan", "vägen", "stigen", "gränd", "backen", "torget", "allén"]
|
||||||
"last": ["gatan", "vägen", "stigen", "gränd", "backen", "torget", "allén"]
|
},
|
||||||
},
|
["Avenyn", "Birger Jarlsgatan", "Promenaden", "Staby", "Sveavägen", "Vintjärn"]
|
||||||
["Avenyn", "Birger Jarlsgatan", "Promenaden", "Staby", "Sveavägen", "Vintjärn"]
|
],
|
||||||
],
|
"street-number": [
|
||||||
"street-number": [
|
"{int(1,9)}",
|
||||||
{ "format": "1" },
|
"{int(10,99)}",
|
||||||
{ "format": "10" },
|
{ "format": "{int(100,999)}", "weight": 0.2 },
|
||||||
{ "format": "100", "weight": 0.2 },
|
{ "format": "{int(1,9)}{upper(1)}", "weight": 0.2 },
|
||||||
{ "format": "1A", "weight": 0.2 },
|
{ "format": "{int(10,99)}{upper(1)}", "weight": 0.1 },
|
||||||
{ "format": "10A", "weight": 0.1 },
|
{ "format": "{int(100,999)}{upper(1)}", "weight": 0.05 }
|
||||||
{ "format": "100A", "weight": 0.05 }
|
],
|
||||||
],
|
"postal-code": "{int(100,999)} {int(10,99)}",
|
||||||
"postal-code": [
|
"locality": ["Alingsås", "Alvesta", "Ängelholm", "Arboga", "Arvika", "Avesta", "Boden", "Bollnäs", "Borås", "Borlänge", "Enköping", "Eskilstuna", "Eslöv", "Fagersta", "Falkenberg", "Falköping", "Falun", "Finspång", "Gällivare", "Gävle", "Göteborg", "Halmstad", "Haparanda", "Härnösand", "Hässleholm", "Helsingborg", "Huddinge", "Hudiksvall", "Jönköping", "Kalmar", "Karlshamn", "Karlskoga", "Karlskrona", "Karlstad", "Katrineholm", "Kiruna", "Köping", "Kramfors", "Kristianstad", "Kristinehamn", "Landskrona", "Lidingö", "Lidköping", "Lindesberg", "Linköping", "Ljungby", "Ludvika", "Luleå", "Lund", "Lycksele", "Malmö", "Mariestad", "Mjölby", "Mölndal", "Mora", "Motala", "Nacka", "Nässjö", "Norrköping", "Norrtälje", "Nyköping", "Nynäshamn", "Örebro", "Örnsköldsvik", "Oskarshamn", "Östersund", "Piteå", "Rabbalshede", "Ronneby", "Säffle", "Sandviken", "Sävsjö", "Sigtuna", "Skara", "Skellefteå", "Skövde", "Söderhamn", "Södertälje", "Sollentuna", "Solna", "Sölvesborg", "Stockholm", "Strängnäs", "Sundbyberg", "Sundsvall", "Täby", "Tierp", "Tranås", "Trelleborg", "Trollhättan", "Uddevalla", "Ulricehamn", "Umeå", "Upplands Väsby", "Uppsala", "Vänersborg", "Varberg", "Värnamo", "Västerås", "Västervik", "Växjö", "Vetlanda", "Vimmerby", "Visby", "Ystad"]
|
||||||
{ "format": "100 10" }
|
}
|
||||||
],
|
|
||||||
"locality": ["Alingsås", "Alvesta", "Ängelholm", "Arboga", "Arvika", "Avesta", "Boden", "Bollnäs", "Borås", "Borlänge", "Enköping", "Eskilstuna", "Eslöv", "Fagersta", "Falkenberg", "Falköping", "Falun", "Finspång", "Gällivare", "Gävle", "Göteborg", "Halmstad", "Haparanda", "Härnösand", "Hässleholm", "Helsingborg", "Huddinge", "Hudiksvall", "Jönköping", "Kalmar", "Karlshamn", "Karlskoga", "Karlskrona", "Karlstad", "Katrineholm", "Kiruna", "Köping", "Kramfors", "Kristianstad", "Kristinehamn", "Landskrona", "Lidingö", "Lidköping", "Lindesberg", "Linköping", "Ljungby", "Ludvika", "Luleå", "Lund", "Lycksele", "Malmö", "Mariestad", "Mjölby", "Mölndal", "Mora", "Motala", "Nacka", "Nässjö", "Norrköping", "Norrtälje", "Nyköping", "Nynäshamn", "Örebro", "Örnsköldsvik", "Oskarshamn", "Östersund", "Piteå", "Rabbalshede", "Ronneby", "Säffle", "Sandviken", "Sävsjö", "Sigtuna", "Skara", "Skellefteå", "Skövde", "Söderhamn", "Södertälje", "Sollentuna", "Solna", "Sölvesborg", "Stockholm", "Strängnäs", "Sundbyberg", "Sundsvall", "Täby", "Tierp", "Tranås", "Trelleborg", "Trollhättan", "Uddevalla", "Ulricehamn", "Umeå", "Upplands Väsby", "Uppsala", "Vänersborg", "Varberg", "Värnamo", "Västerås", "Västervik", "Växjö", "Vetlanda", "Vimmerby", "Visby", "Ystad"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"format": "##{x}{x}{x}{x}{x}{x}",
|
"format": "#{x}{x}{x}{x}{x}{x}",
|
||||||
"x": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"],
|
"x": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"],
|
||||||
"weight": 2
|
"weight": 2
|
||||||
},
|
},
|
||||||
|
|||||||
+12
-14
@@ -1,14 +1,12 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "{base} {suffix}",
|
||||||
"format": "{base} {suffix}",
|
"base": [
|
||||||
"base": [
|
{
|
||||||
{
|
"format": "{a}{b}",
|
||||||
"format": "{a}{b}",
|
"a": ["Berg", "Blå", "Eko", "Fjäll", "Grön", "Järn", "Nord", "Ny", "Sjö", "Skog", "Sol", "Sten", "Stor", "Syd", "Söder", "Trä", "Vind", "Väst", "Ås", "Öst"],
|
||||||
"a": ["Berg", "Blå", "Eko", "Fjäll", "Grön", "Järn", "Nord", "Ny", "Sjö", "Skog", "Sol", "Sten", "Stor", "Syd", "Söder", "Trä", "Vind", "Väst", "Ås", "Öst"],
|
"b": ["bygg", "data", "design", "energi", "frakt", "gruppen", "handel", "konsult", "kraft", "logistik", "media", "produkter", "system", "teknik", "verkstad", "vård"]
|
||||||
"b": ["bygg", "data", "design", "energi", "frakt", "gruppen", "handel", "konsult", "kraft", "logistik", "media", "produkter", "system", "teknik", "verkstad", "vård"]
|
},
|
||||||
},
|
["Aktiebolaget Vega", "Bröderna Lund", "Ekonomihuset", "Hantverkargruppen", "Lindberg", "Mälardalens Bygg", "Nordström", "Skandinaviska Handelshuset", "Sundbergs Verkstad", "Svensson & Co", "Vasa Logistik"]
|
||||||
["Aktiebolaget Vega", "Bröderna Lund", "Ekonomihuset", "Hantverkargruppen", "Lindberg", "Mälardalens Bygg", "Nordström", "Skandinaviska Handelshuset", "Sundbergs Verkstad", "Svensson & Co", "Vasa Logistik"]
|
],
|
||||||
],
|
"suffix": [{ "format": "AB", "weight": 4 }, "HB", "KB"]
|
||||||
"suffix": ["AB", "AB", "AB", "AB", "HB", "KB"]
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
+13
-15
@@ -1,15 +1,13 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "{year}-{month}-{day}",
|
||||||
"format": "{year}-{month}-{day}",
|
"month": ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"],
|
||||||
"month": ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"],
|
"day": ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28"],
|
||||||
"day": ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28"],
|
"year": [
|
||||||
"year": [
|
"197{digits(1)}",
|
||||||
{ "format": "#1970" },
|
"198{digits(1)}",
|
||||||
{ "format": "#1980" },
|
{ "format": "199{digits(1)}", "weight": 2 },
|
||||||
{ "format": "#1990", "weight": 2 },
|
{ "format": "200{digits(1)}", "weight": 3 },
|
||||||
{ "format": "2#0#00", "weight": 3 },
|
{ "format": "201{digits(1)}", "weight": 3 },
|
||||||
{ "format": "2#0#10", "weight": 3 },
|
{ "format": "202{digits(1)}", "weight": 2 }
|
||||||
{ "format": "2#020", "weight": 2 }
|
]
|
||||||
]
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
+26
-29
@@ -1,29 +1,26 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "{local}@{domain}",
|
||||||
"format": "{local}@{domain}",
|
"local": [
|
||||||
"local": [
|
{
|
||||||
{
|
"format": "{first}{sep}{last}{n}",
|
||||||
"format": "{first}{sep}{last}{n}",
|
"weight": 3,
|
||||||
"weight": 3,
|
"first": ["agnes", "alice", "anders", "anna", "anton", "asa", "axel", "bjorn", "david", "ebba", "elin", "elsa", "emil", "emma", "erik", "ester", "frida", "fredrik", "gustav", "hampus", "hanna", "henrik", "hugo", "ida", "ingrid", "isak", "johan", "jonas", "julia", "karin", "kjell", "klara", "lars", "lena", "linus", "magnus", "maja", "mats", "mattias", "mikael", "moa", "nils", "olle", "oskar", "par", "patrik", "per", "pontus", "rasmus", "samuel", "sara", "sofia", "soren", "stina", "sven", "tobias", "tuva", "viktor", "wilma", "ylva"],
|
||||||
"first": ["agnes", "alice", "anders", "anna", "anton", "asa", "axel", "bjorn", "david", "ebba", "elin", "elsa", "emil", "emma", "erik", "ester", "frida", "fredrik", "gustav", "hampus", "hanna", "henrik", "hugo", "ida", "ingrid", "isak", "johan", "jonas", "julia", "karin", "kjell", "klara", "lars", "lena", "linus", "magnus", "maja", "mats", "mattias", "mikael", "moa", "nils", "olle", "oskar", "par", "patrik", "per", "pontus", "rasmus", "samuel", "sara", "sofia", "soren", "stina", "sven", "tobias", "tuva", "viktor", "wilma", "ylva"],
|
"last": ["ahlberg", "andersson", "berg", "berglund", "bergstrom", "blom", "dahlberg", "eklund", "ekstrom", "engstrom", "eriksson", "falk", "forsberg", "fredriksson", "gustafsson", "hellstrom", "holm", "holmberg", "jakobsson", "johansson", "jonsson", "karlsson", "larsson", "lind", "lindberg", "lindgren", "lindqvist", "lundberg", "lundgren", "lundqvist", "magnusson", "nilsson", "norberg", "nystrom", "olsson", "persson", "petersson", "samuelsson", "sandberg", "sjoberg", "sjogren", "strand", "strom", "sundberg", "svensson", "soderberg", "wallin", "aberg", "oberg"],
|
||||||
"last": ["ahlberg", "andersson", "berg", "berglund", "bergstrom", "blom", "dahlberg", "eklund", "ekstrom", "engstrom", "eriksson", "falk", "forsberg", "fredriksson", "gustafsson", "hellstrom", "holm", "holmberg", "jakobsson", "johansson", "jonsson", "karlsson", "larsson", "lind", "lindberg", "lindgren", "lindqvist", "lundberg", "lundgren", "lundqvist", "magnusson", "nilsson", "norberg", "nystrom", "olsson", "persson", "petersson", "samuelsson", "sandberg", "sjoberg", "sjogren", "strand", "strom", "sundberg", "svensson", "soderberg", "wallin", "aberg", "oberg"],
|
"sep": [{ "format": ".", "weight": 2 }, "_", "-", ""],
|
||||||
"sep": [".", ".", "_", "-", ""],
|
"n": [{ "format": "", "weight": 3 }, "1", "7", "42", "88", "99"]
|
||||||
"n": ["", "", "", "1", "7", "42", "88", "99"]
|
},
|
||||||
},
|
{
|
||||||
{
|
"format": "{adj}{noun}{n}",
|
||||||
"format": "{adj}{noun}{n}",
|
"weight": 2,
|
||||||
"weight": 2,
|
"adj": ["blixt", "busig", "cool", "frisk", "galen", "glad", "kaxig", "kvick", "listig", "lugn", "mega", "modig", "mysig", "pigg", "rolig", "smart", "snabb", "stark", "super", "trygg", "turbo", "varm", "vild"],
|
||||||
"adj": ["blixt", "busig", "cool", "frisk", "galen", "glad", "kaxig", "kvick", "listig", "lugn", "mega", "modig", "mysig", "pigg", "rolig", "smart", "snabb", "stark", "super", "trygg", "turbo", "varm", "vild"],
|
"noun": ["abborren", "bamsen", "bjorn", "drake", "ekorre", "falken", "grodan", "hund", "igelkott", "kaninen", "katt", "korpen", "krabban", "musen", "nallen", "ninja", "orn", "panda", "pirat", "raven", "robot", "sillen", "sparven", "uggla", "varg", "vargen"],
|
||||||
"noun": ["abborren", "bamsen", "bjorn", "drake", "ekorre", "falken", "grodan", "hund", "igelkott", "kaninen", "katt", "korpen", "krabban", "musen", "nallen", "ninja", "orn", "panda", "pirat", "raven", "robot", "sillen", "sparven", "uggla", "varg", "vargen"],
|
"n": [{ "format": "", "weight": 2 }, "7", "13", "14", "42", "77", "99", "123", "420", "2024"]
|
||||||
"n": ["", "", "7", "13", "14", "42", "77", "99", "123", "420", "2024"]
|
},
|
||||||
},
|
{
|
||||||
{
|
"format": "{w}{n}",
|
||||||
"format": "{w}{n}",
|
"w": ["blip", "blupp", "boop", "busigekorre", "coolkatten", "doot", "fizz", "floof", "galnaalgen", "gladgrodan", "glorp", "kapow", "kazoo", "kvickharen", "listigaraven", "megaraven", "mysmoln", "nattugglan", "pew", "pigghund", "pixelfox", "plonk", "rolighund", "snabbkatten", "snork", "splat", "superhunden", "turbohund", "vildvargen", "vroom", "wahoo", "waooooh", "woot", "yeet", "zap", "zoom"],
|
||||||
"weight": 1,
|
"n": [{ "format": "", "weight": 2 }, "7", "14", "42", "69", "99", "123", "420", "1337", "2000", "9000"]
|
||||||
"w": ["blip", "blupp", "boop", "busigekorre", "coolkatten", "doot", "fizz", "floof", "galnaalgen", "gladgrodan", "glorp", "kapow", "kazoo", "kvickharen", "listigaraven", "megaraven", "mysmoln", "nattugglan", "pew", "pigghund", "pixelfox", "plonk", "rolighund", "snabbkatten", "snork", "splat", "superhunden", "turbohund", "vildvargen", "vroom", "wahoo", "waooooh", "woot", "yeet", "zap", "zoom"],
|
}
|
||||||
"n": ["", "", "7", "14", "42", "69", "99", "123", "420", "1337", "2000", "9000"]
|
],
|
||||||
}
|
"domain": ["example.com", "example.org", "example.net", "example.se", "example.nu", "example.io", "mail.example.se", "webmail.example.se", "inkorg.example.se", "post.example.se", "demo.example.se", "test.example.org", "dev.example.se"]
|
||||||
],
|
}
|
||||||
"domain": ["example.com", "example.org", "example.net", "example.se", "example.nu", "example.io", "mail.example.se", "webmail.example.se", "inkorg.example.se", "post.example.se", "demo.example.se", "test.example.org", "dev.example.se"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
+6
-4
@@ -2,9 +2,11 @@
|
|||||||
{
|
{
|
||||||
"format": "{o}.{o}.{o}.{o}",
|
"format": "{o}.{o}.{o}.{o}",
|
||||||
"weight": 8,
|
"weight": 8,
|
||||||
"o": [{ "format": "0", "weight": 3 }, { "format": "10", "weight": 4 }, { "format": "#100", "weight": 2 }]
|
"o": [
|
||||||
|
{ "format": "{digits(1)}", "weight": 3 },
|
||||||
|
{ "format": "{int(10,99)}", "weight": 4 },
|
||||||
|
{ "format": "1{digits(2)}", "weight": 2 }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
"{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}"
|
||||||
"format": "{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|||||||
+32
-41
@@ -1,41 +1,32 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "{prefix}{femalefirst|malefirst} {last}",
|
||||||
"format": "{prefix}{femalefirst|malefirst} {last}",
|
"femalefirst": ["Agnes", "Alice", "Alicia", "Alma", "Amanda", "Anna", "Astrid", "Cornelia", "Ebba", "Elin", "Ella", "Ellen", "Elsa", "Emilia", "Emma", "Ester", "Eva", "Frida", "Hanna", "Hedda", "Ida", "Ingrid", "Isabelle", "Julia", "Karin", "Klara", "Kristina", "Lena", "Linnéa", "Lova", "Maja", "Maria", "Moa", "Molly", "Märta", "Nellie", "Nora", "Olivia", "Saga", "Sara", "Selma", "Signe", "Siri", "Sofia", "Stina", "Tilde", "Tuva", "Wilma", "Ylva", "Åsa"],
|
||||||
"femalefirst": ["Agnes", "Alice", "Alicia", "Alma", "Amanda", "Anna", "Astrid", "Cornelia", "Ebba", "Elin", "Ella", "Ellen", "Elsa", "Emilia", "Emma", "Ester", "Eva", "Frida", "Hanna", "Hedda", "Ida", "Ingrid", "Isabelle", "Julia", "Karin", "Klara", "Kristina", "Lena", "Linnéa", "Lova", "Maja", "Maria", "Moa", "Molly", "Märta", "Nellie", "Nora", "Olivia", "Saga", "Sara", "Selma", "Signe", "Siri", "Sofia", "Stina", "Tilde", "Tuva", "Wilma", "Ylva", "Åsa"],
|
"malefirst": ["Adam", "Albin", "Alexander", "Alfred", "Anders", "Anton", "Arvid", "Axel", "Bengt", "Bo", "Carl", "David", "Edvin", "Elias", "Emil", "Erik", "Filip", "Folke", "Fredrik", "Gustav", "Göran", "Hampus", "Hans", "Henrik", "Hugo", "Isak", "Johan", "Jonas", "Karl", "Kjell", "Lars", "Leo", "Linus", "Love", "Magnus", "Mats", "Mattias", "Mikael", "Nils", "Olle", "Oskar", "Otto", "Patrik", "Per", "Pontus", "Rasmus", "Samuel", "Sixten", "Stefan", "Sten", "Sven", "Theodor", "Tobias", "Viktor", "William", "Åke"],
|
||||||
"malefirst": ["Adam", "Albin", "Alexander", "Alfred", "Anders", "Anton", "Arvid", "Axel", "Bengt", "Bo", "Carl", "David", "Edvin", "Elias", "Emil", "Erik", "Filip", "Folke", "Fredrik", "Gustav", "Göran", "Hampus", "Hans", "Henrik", "Hugo", "Isak", "Johan", "Jonas", "Karl", "Kjell", "Lars", "Leo", "Linus", "Love", "Magnus", "Mats", "Mattias", "Mikael", "Nils", "Olle", "Oskar", "Otto", "Patrik", "Per", "Pontus", "Rasmus", "Samuel", "Sixten", "Stefan", "Sten", "Sven", "Theodor", "Tobias", "Viktor", "William", "Åke"],
|
"last": [
|
||||||
"last": [
|
{
|
||||||
{
|
"format": "{first}sson",
|
||||||
"format": "{first}sson",
|
"weight": 5,
|
||||||
"weight": 5,
|
"first": ["Ander", "Arvid", "Bengt", "Daniel", "David", "Erik", "Fredrik", "Gustaf", "Henrik", "Håkan", "Isak", "Jakob", "Johan", "Jon", "Jön", "Karl", "Lar", "Magnu", "Matt", "Mikael", "Mårten", "Nil", "Ol", "Per", "Petter", "Samuel", "Sven"]
|
||||||
"first": ["Ander", "Arvid", "Bengt", "Daniel", "David", "Erik", "Fredrik", "Gustaf", "Henrik", "Håkan", "Isak", "Jakob", "Johan", "Jon", "Jön", "Karl", "Lar", "Magnu", "Matt", "Mikael", "Mårten", "Nil", "Ol", "Per", "Petter", "Samuel", "Sven"]
|
},
|
||||||
},
|
{
|
||||||
{
|
"format": "{first}{last}",
|
||||||
"format": "{first}{last}",
|
"weight": 3.6,
|
||||||
"weight": 3.6,
|
"first": ["Ahl", "Berg", "Blom", "Ceder", "Dahl", "Ek", "Eng", "Falk", "Fors", "Gran", "Hag", "Hed", "Holm", "Lind", "Lund", "Mal", "Ny", "Rosen", "Sand", "Sjö", "Skog", "Söder", "Sten", "Strand", "Sund", "Wik", "Öst"],
|
||||||
"first": ["Ahl", "Berg", "Blom", "Ceder", "Dahl", "Ek", "Eng", "Falk", "Fors", "Gran", "Hag", "Hed", "Holm", "Lind", "Lund", "Mal", "Ny", "Rosen", "Sand", "Sjö", "Skog", "Söder", "Sten", "Strand", "Sund", "Wik", "Öst"],
|
"last": ["berg", "blad", "crona", "dahl", "ed", "fors", "gren", "holm", "in", "kvist", "löf", "lund", "man", "mark", "qvist", "roth", "stedt", "sten", "strand", "ström", "vall"]
|
||||||
"last": ["berg", "blad", "crona", "dahl", "ed", "fors", "gren", "holm", "in", "kvist", "löf", "lund", "man", "mark", "qvist", "roth", "stedt", "sten", "strand", "ström", "vall"]
|
},
|
||||||
},
|
{
|
||||||
{
|
"format": "{first}{last}",
|
||||||
"format": "{first}{last}",
|
"weight": 0.267,
|
||||||
"weight": 0.267,
|
"first": ["Hell", "Wall"],
|
||||||
"first": ["Hell", "Wall"],
|
"last": ["berg", "blad", "crona", "dahl", "ed", "fors", "gren", "holm", "in", "kvist", "man", "mark", "qvist", "roth", "stedt", "sten", "strand", "ström", "vall"]
|
||||||
"last": ["berg", "blad", "crona", "dahl", "ed", "fors", "gren", "holm", "in", "kvist", "man", "mark", "qvist", "roth", "stedt", "sten", "strand", "ström", "vall"]
|
},
|
||||||
},
|
{
|
||||||
{
|
"format": "{first}{last}",
|
||||||
"format": "{first}{last}",
|
"weight": 0.133,
|
||||||
"weight": 0.133,
|
"first": "Norr",
|
||||||
"first": ["Norr"],
|
"last": ["berg", "blad", "crona", "dahl", "ed", "fors", "gren", "holm", "in", "kvist", "löf", "lund", "man", "mark", "qvist", "stedt", "sten", "strand", "ström", "vall"]
|
||||||
"last": ["berg", "blad", "crona", "dahl", "ed", "fors", "gren", "holm", "in", "kvist", "löf", "lund", "man", "mark", "qvist", "stedt", "sten", "strand", "ström", "vall"]
|
},
|
||||||
},
|
["Berg", "Blom", "Eismar", "Falk", "Holm", "Lind", "Norberg", "Strand", "Ström", "von Flemming", "Åberg", "Öberg"]
|
||||||
["Berg", "Blom", "Eismar", "Falk", "Holm", "Lind", "Norberg", "Strand", "Ström", "von Flemming", "Åberg", "Öberg"]
|
],
|
||||||
],
|
"prefix": ["", { "format": "{string} ", "string": ["dr", "prof"], "weight": 0.05 }]
|
||||||
"prefix": [
|
}
|
||||||
"",
|
|
||||||
{
|
|
||||||
"format": "{string} ",
|
|
||||||
"string": ["dr", "prof"],
|
|
||||||
"weight": 0.05
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
"format": "{prefix}-{a} {b} {c}",
|
"format": "{prefix}-{a} {b} {c}",
|
||||||
"weight": 10,
|
"weight": 10,
|
||||||
"prefix": ["070", "072", "073", "076", "079"],
|
"prefix": ["070", "072", "073", "076", "079"],
|
||||||
"a": [{ "format": "000" }],
|
"a": "{digits(3)}",
|
||||||
"b": [{ "format": "00" }],
|
"b": "{digits(2)}",
|
||||||
"c": [{ "format": "00" }]
|
"c": "{digits(2)}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"format": "{prefix}-{a} {b} {c}",
|
"format": "{prefix}-{a} {b} {c}",
|
||||||
"prefix": ["08", "011", "013", "018", "019", "021", "023", "026", "031", "033", "035", "036", "040", "042", "044", "046", "054", "060", "063", "090"],
|
"prefix": ["08", "011", "013", "018", "019", "021", "023", "026", "031", "033", "035", "036", "040", "042", "044", "046", "054", "060", "063", "090"],
|
||||||
"a": [{ "format": "000" }],
|
"a": "{digits(3)}",
|
||||||
"b": [{ "format": "00" }],
|
"b": "{digits(2)}",
|
||||||
"c": [{ "format": "00" }]
|
"c": "{digits(2)}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+11
-13
@@ -1,13 +1,11 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "{amt}{ore} kr",
|
||||||
"format": "{amt}{ore} kr",
|
"amt": [
|
||||||
"amt": [
|
{ "format": "{int(1,9)}", "weight": 2 },
|
||||||
{ "format": "1", "weight": 2 },
|
{ "format": "{int(10,99)}", "weight": 4 },
|
||||||
{ "format": "10", "weight": 4 },
|
{ "format": "{int(100,999)}", "weight": 3 },
|
||||||
{ "format": "100", "weight": 3 },
|
"{int(1,9)} {digits(3)}",
|
||||||
{ "format": "1 000", "weight": 1 },
|
{ "format": "{int(10,99)} {digits(3)}", "weight": 0.3 }
|
||||||
{ "format": "10 000", "weight": 0.3 }
|
],
|
||||||
],
|
"ore": ["", { "format": ",{c}", "c": "{digits(2)}", "weight": 0.4 }]
|
||||||
"ore": ["", { "format": ",{c}", "c": [{ "format": "00" }], "weight": 0.4 }]
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"noun": ["arkivet", "berget", "bäcken", "dalen", "eken", "fjället", "floden", "fyren", "fågeln", "gården", "glaciären", "hamnen", "holmen", "klippan", "kusten", "lampan", "lyktan", "motorn", "måsen", "ravinen", "räven", "seglaren", "signalen", "sjön", "skogen", "slätten", "stigen", "stjärnan", "stranden", "tornet", "trädgården", "vandraren", "vinden", "vågen", "ängen"],
|
"noun": ["arkivet", "berget", "bäcken", "dalen", "eken", "fjället", "floden", "fyren", "fågeln", "gården", "glaciären", "hamnen", "holmen", "klippan", "kusten", "lampan", "lyktan", "motorn", "måsen", "ravinen", "räven", "seglaren", "signalen", "sjön", "skogen", "slätten", "stigen", "stjärnan", "stranden", "tornet", "trädgården", "vandraren", "vinden", "vågen", "ängen"],
|
||||||
"verb": ["anar", "bevakar", "bygger", "bär", "döljer", "famnar", "följer", "formar", "gömmer", "hälsar", "korsar", "leder", "lockar", "lyser", "minns", "möter", "når", "skyddar", "speglar", "söker", "tänder", "vaktar", "väcker", "värnar", "återvänder"],
|
"verb": ["anar", "bevakar", "bygger", "bär", "döljer", "famnar", "följer", "formar", "gömmer", "hälsar", "korsar", "leder", "lockar", "lyser", "minns", "möter", "når", "skyddar", "speglar", "söker", "tänder", "vaktar", "väcker", "värnar", "återvänder"],
|
||||||
"prep": ["bakom", "bland", "bortom", "framför", "genom", "intill", "kring", "längs", "mot", "nära", "ovanför", "runt", "under", "vid", "över"],
|
"prep": ["bakom", "bland", "bortom", "framför", "genom", "intill", "kring", "längs", "mot", "nära", "ovanför", "runt", "under", "vid", "över"],
|
||||||
"end": [".", ".", ".", "!"]
|
"end": [{ "format": ".", "weight": 3 }, "!"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"format": "{q} {adj} {noun} {verb} {prep} {noun}?",
|
"format": "{q} {adj} {noun} {verb} {prep} {noun}?",
|
||||||
|
|||||||
+22
-25
@@ -1,25 +1,22 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "{digits(2)}{mmdd}-{digits(3)}{luhn()}",
|
||||||
"format": "00{mmdd}-000{luhn()}",
|
"mmdd": [
|
||||||
"mmdd": [
|
{
|
||||||
{
|
"format": "{m}{d}",
|
||||||
"format": "{m}{d}",
|
"weight": 7,
|
||||||
"weight": 7,
|
"m": ["01", "03", "05", "07", "08", "10", "12"],
|
||||||
"m": ["01", "03", "05", "07", "08", "10", "12"],
|
"d": ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31"]
|
||||||
"d": ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31"]
|
},
|
||||||
},
|
{
|
||||||
{
|
"format": "{m}{d}",
|
||||||
"format": "{m}{d}",
|
"weight": 4,
|
||||||
"weight": 4,
|
"m": ["04", "06", "09", "11"],
|
||||||
"m": ["04", "06", "09", "11"],
|
"d": ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]
|
||||||
"d": ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]
|
},
|
||||||
},
|
{
|
||||||
{
|
"format": "{m}{d}",
|
||||||
"format": "{m}{d}",
|
"m": "02",
|
||||||
"weight": 1,
|
"d": ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28"]
|
||||||
"m": ["02"],
|
}
|
||||||
"d": ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28"]
|
]
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
+17
-8
@@ -1,8 +1,17 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "{hour}:{minute}{sec}",
|
||||||
"format": "{hour}:{minute}{sec}",
|
"hour": [
|
||||||
"hour": [{ "format": "#00", "weight": 4 }, { "format": "#10", "weight": 4 }, { "format": "2{t}", "t": ["0", "1", "2", "3"], "weight": 2 }],
|
{ "format": "0{digits(1)}", "weight": 4 },
|
||||||
"minute": [{ "format": "{t}0", "t": ["0", "1", "2", "3", "4", "5"] }],
|
{ "format": "1{digits(1)}", "weight": 4 },
|
||||||
"sec": ["", { "format": ":{s}", "s": [{ "format": "{t}0", "t": ["0", "1", "2", "3", "4", "5"] }], "weight": 0.4 }]
|
{ "format": "2{t}", "t": ["0", "1", "2", "3"], "weight": 2 }
|
||||||
}
|
],
|
||||||
]
|
"minute": { "format": "{t}{digits(1)}", "t": ["0", "1", "2", "3", "4", "5"] },
|
||||||
|
"sec": [
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
"format": ":{s}",
|
||||||
|
"s": { "format": "{t}{digits(1)}", "t": ["0", "1", "2", "3", "4", "5"] },
|
||||||
|
"weight": 0.4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
+17
-12
@@ -1,12 +1,17 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "https://{host}{path}",
|
||||||
"format": "https://{host}{path}",
|
"host": ["example.com", "www.example.com", "example.org", "example.se", "www.example.se", "blogg.example.net", "butik.example.com", "shop.example.se", "api.example.se", "dokument.example.org", "nyheter.example.net", "mail.example.se"],
|
||||||
"host": ["example.com", "www.example.com", "example.org", "example.se", "www.example.se", "blogg.example.net", "butik.example.com", "shop.example.se", "api.example.se", "dokument.example.org", "nyheter.example.net", "mail.example.se"],
|
"path": [
|
||||||
"path": [
|
{ "format": "", "weight": 2 },
|
||||||
"",
|
{
|
||||||
"",
|
"format": "/{seg}",
|
||||||
{ "format": "/{seg}", "seg": ["om", "om-oss", "produkter", "blogg", "kontakt", "hjalp", "artiklar", "logga-in", "nyheter", "start", "sok", "kundvagn", "kassa", "villkor", "integritet", "priser", "tjanster", "foretag", "karriar", "support", "konto", "installningar", "registrera"] },
|
"seg": ["om", "om-oss", "produkter", "blogg", "kontakt", "hjalp", "artiklar", "logga-in", "nyheter", "start", "sok", "kundvagn", "kassa", "villkor", "integritet", "priser", "tjanster", "foretag", "karriar", "support", "konto", "installningar", "registrera"]
|
||||||
{ "format": "/{seg}/{sub}", "weight": 0.5, "seg": ["blogg", "produkter", "hjalp", "dokument", "kategori"], "sub": ["oversikt", "kom-igang", "guide", "2024", "senaste", "arkiv", "detaljer"] }
|
},
|
||||||
]
|
{
|
||||||
}
|
"format": "/{seg}/{sub}",
|
||||||
]
|
"weight": 0.5,
|
||||||
|
"seg": ["blogg", "produkter", "hjalp", "dokument", "kategori"],
|
||||||
|
"sub": ["oversikt", "kom-igang", "guide", "2024", "senaste", "arkiv", "detaljer"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,21 +4,20 @@
|
|||||||
"weight": 2,
|
"weight": 2,
|
||||||
"first": ["agnes", "alice", "anders", "anna", "asa", "axel", "bjorn", "ebba", "elin", "emil", "emma", "erik", "frida", "gustav", "hanna", "hugo", "ida", "johan", "jonas", "julia", "karin", "klara", "lars", "lena", "magnus", "maja", "moa", "nils", "oskar", "per", "sara", "sofia", "viktor", "ylva"],
|
"first": ["agnes", "alice", "anders", "anna", "asa", "axel", "bjorn", "ebba", "elin", "emil", "emma", "erik", "frida", "gustav", "hanna", "hugo", "ida", "johan", "jonas", "julia", "karin", "klara", "lars", "lena", "magnus", "maja", "moa", "nils", "oskar", "per", "sara", "sofia", "viktor", "ylva"],
|
||||||
"last": ["ahl", "alm", "berg", "bjork", "dahl", "ek", "falk", "gran", "hag", "hed", "holm", "lind", "lund", "mark", "nor", "ohman", "ros", "sand", "sjo", "skog", "sten", "strom", "vik"],
|
"last": ["ahl", "alm", "berg", "bjork", "dahl", "ek", "falk", "gran", "hag", "hed", "holm", "lind", "lund", "mark", "nor", "ohman", "ros", "sand", "sjo", "skog", "sten", "strom", "vik"],
|
||||||
"sep": ["", "", ".", "_"],
|
"sep": [{ "format": "", "weight": 2 }, ".", "_"],
|
||||||
"n": ["", "", "", "1", "7", "23", "99", "2024"]
|
"n": [{ "format": "", "weight": 3 }, "1", "7", "23", "99", "2024"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"format": "{adj}{sep}{noun}{n}",
|
"format": "{adj}{sep}{noun}{n}",
|
||||||
"weight": 2,
|
"weight": 2,
|
||||||
"adj": ["blixt", "busig", "cool", "frisk", "galen", "glad", "kaxig", "kvick", "listig", "lugn", "mega", "modig", "mysig", "pigg", "rolig", "smart", "snabb", "stark", "super", "trygg", "turbo", "varm", "vild"],
|
"adj": ["blixt", "busig", "cool", "frisk", "galen", "glad", "kaxig", "kvick", "listig", "lugn", "mega", "modig", "mysig", "pigg", "rolig", "smart", "snabb", "stark", "super", "trygg", "turbo", "varm", "vild"],
|
||||||
"noun": ["abborre", "bamse", "bjorn", "drake", "ekorre", "falk", "groda", "hund", "igelkott", "kanin", "katt", "korp", "krabba", "mus", "nalle", "ninja", "orn", "panda", "pirat", "rav", "robot", "sill", "sparv", "uggla", "varg"],
|
"noun": ["abborre", "bamse", "bjorn", "drake", "ekorre", "falk", "groda", "hund", "igelkott", "kanin", "katt", "korp", "krabba", "mus", "nalle", "ninja", "orn", "panda", "pirat", "rav", "robot", "sill", "sparv", "uggla", "varg"],
|
||||||
"sep": ["", "", ".", "_"],
|
"sep": [{ "format": "", "weight": 2 }, ".", "_"],
|
||||||
"n": ["", "", "7", "13", "42", "77", "99", "123", "420", "2024"]
|
"n": [{ "format": "", "weight": 2 }, "7", "13", "42", "77", "99", "123", "420", "2024"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"format": "{w}{n}",
|
"format": "{w}{n}",
|
||||||
"weight": 1,
|
|
||||||
"w": ["blip", "blupp", "boop", "busigekorre", "coolkatten", "fizz", "floof", "galnaalgen", "gladgrodan", "glorp", "kapow", "kazoo", "kvickharen", "listigaraven", "megaraven", "mysmoln", "nattugglan", "pigghund", "pixelfox", "plonk", "rolighund", "snabbkatten", "snork", "splat", "superhunden", "turbohund", "vildvargen", "vroom", "wahoo", "waooooh", "woot", "yeet", "zap", "zoom"],
|
"w": ["blip", "blupp", "boop", "busigekorre", "coolkatten", "fizz", "floof", "galnaalgen", "gladgrodan", "glorp", "kapow", "kazoo", "kvickharen", "listigaraven", "megaraven", "mysmoln", "nattugglan", "pigghund", "pixelfox", "plonk", "rolighund", "snabbkatten", "snork", "splat", "superhunden", "turbohund", "vildvargen", "vroom", "wahoo", "waooooh", "woot", "yeet", "zap", "zoom"],
|
||||||
"n": ["", "", "7", "14", "42", "69", "99", "123", "420", "1337", "2000", "9000"]
|
"n": [{ "format": "", "weight": 2 }, "7", "14", "42", "69", "99", "123", "420", "1337", "2000", "9000"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
[
|
{
|
||||||
{
|
"format": "{pre}{n}.{n}.{n}{suffix}",
|
||||||
"format": "{pre}{n}.{n}.{n}{suffix}",
|
"n": [{ "format": "{digits(1)}", "weight": 3 }, "{int(10,99)}"],
|
||||||
"n": [{ "format": "0", "weight": 3 }, { "format": "10" }],
|
"pre": ["", { "format": "v", "weight": 0.4 }],
|
||||||
"pre": ["", { "format": "v", "weight": 0.4 }],
|
"suffix": ["", { "format": "-{tag}.{m}", "tag": ["alpha", "beta", "rc"], "m": ["1", "2", "3"], "weight": 0.3 }]
|
||||||
"suffix": ["", { "format": "-{tag}.{m}", "tag": ["alpha", "beta", "rc"], "m": ["1", "2", "3"], "weight": 0.3 }]
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
+46
-47
@@ -13,15 +13,14 @@ import (
|
|||||||
|
|
||||||
// --- format string edge cases ---
|
// --- format string edge cases ---
|
||||||
|
|
||||||
func TestEscapeEdgeCases(t *testing.T) {
|
func TestHashIsLiteral(t *testing.T) {
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
`{"format":""}`: "", // empty format
|
`{"format":"","x":"v"}`: "",
|
||||||
`{"format":"#"}`: "#", // trailing escape is a literal #
|
`{"format":"#","x":"v"}`: "#",
|
||||||
`{"format":"##"}`: "#", // escaped hash
|
`{"format":"##","x":"v"}`: "##",
|
||||||
`{"format":"#0#1#A#a"}`: "01Aa", // escaped class chars stay literal
|
`{"format":"#0#1#A#a","x":"v"}`: "#0#1#A#a",
|
||||||
`{"format":"#{x#}"}`: "{x}", // escaping braces disables tokens
|
`{"format":"#{x}","x":"v"}`: "#v",
|
||||||
`{"format":"x}y"}`: "x}y", // an unmatched } is literal (x, y aren't classes)
|
|
||||||
}
|
}
|
||||||
for tmpl, want := range cases {
|
for tmpl, want := range cases {
|
||||||
if got := mustRender(t, f, tmpl); got != want {
|
if got := mustRender(t, f, tmpl); got != want {
|
||||||
@@ -33,7 +32,7 @@ func TestEscapeEdgeCases(t *testing.T) {
|
|||||||
func TestMultibyteFormat(t *testing.T) {
|
func TestMultibyteFormat(t *testing.T) {
|
||||||
// Scanning is rune-aware: multibyte literals coexist with class chars and
|
// Scanning is rune-aware: multibyte literals coexist with class chars and
|
||||||
// tokens without corrupting indices.
|
// tokens without corrupting indices.
|
||||||
got := mustRender(t, engine(2), `{"format":"Öster{x}-0å","x":["väg"]}`)
|
got := mustRender(t, engine(2), `{"format":"Öster{x}-{digits(1)}å","x":"väg"}`)
|
||||||
if !regexp.MustCompile(`^Österväg-[0-9]å$`).MatchString(got) {
|
if !regexp.MustCompile(`^Österväg-[0-9]å$`).MatchString(got) {
|
||||||
t.Fatalf("multibyte format = %q", got)
|
t.Fatalf("multibyte format = %q", got)
|
||||||
}
|
}
|
||||||
@@ -43,7 +42,7 @@ func TestAlternationThreeWay(t *testing.T) {
|
|||||||
f := engine(4)
|
f := engine(4)
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
seen[mustRender(t, f, `{"format":"{a|b|c}","a":["A"],"b":["B"],"c":["C"]}`)] = true
|
seen[mustRender(t, f, `{"format":"{a|b|c}","a":"A","b":"B","c":"C"}`)] = true
|
||||||
}
|
}
|
||||||
if !seen["A"] || !seen["B"] || !seen["C"] || len(seen) != 3 {
|
if !seen["A"] || !seen["B"] || !seen["C"] || len(seen) != 3 {
|
||||||
t.Fatalf("3-way alternation produced %v, want A, B and C", seen)
|
t.Fatalf("3-way alternation produced %v, want A, B and C", seen)
|
||||||
@@ -70,11 +69,11 @@ func TestNewErrors(t *testing.T) {
|
|||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
"separator without repeat": {
|
"separator without repeat": {
|
||||||
map[string]string{"a": `{"format":"{x}","x":["1"],"separator":","}`},
|
map[string]string{"a": `{"format":"{x}","x":"1","separator":","}`},
|
||||||
"has no effect without a repeat above 1",
|
"has no effect without a repeat above 1",
|
||||||
},
|
},
|
||||||
"separator with an explicit repeat of 1": {
|
"separator with an explicit repeat of 1": {
|
||||||
map[string]string{"a": `{"format":"{x}","x":["1"],"repeat":1,"separator":","}`},
|
map[string]string{"a": `{"format":"{x}","x":"1","separator":","}`},
|
||||||
"has no effect without a repeat above 1",
|
"has no effect without a repeat above 1",
|
||||||
},
|
},
|
||||||
"weight outside a choice": {
|
"weight outside a choice": {
|
||||||
@@ -86,38 +85,38 @@ func TestNewErrors(t *testing.T) {
|
|||||||
"weight only skews a choice's items",
|
"weight only skews a choice's items",
|
||||||
},
|
},
|
||||||
"weight used as a field": {
|
"weight used as a field": {
|
||||||
map[string]string{"a": `{"format":"{name} {weight}kg","name":["Anvil"],"weight":["7"]}`},
|
map[string]string{"a": `{"format":"{name} {weight}kg","name":"Anvil","weight":["7"]}`},
|
||||||
"can never be a field",
|
"can never be a field",
|
||||||
},
|
},
|
||||||
"an option name used as a token": {
|
"an option name used as a token": {
|
||||||
map[string]string{"a": `{"format":"{weight}"}`},
|
map[string]string{"a": `"{weight}"`},
|
||||||
`"weight" is an option and can never be a field`,
|
`"weight" is an option and can never be a field`,
|
||||||
},
|
},
|
||||||
"category name with a dot": {
|
"category name with a dot": {
|
||||||
map[string]string{"a.b": `["1"]`},
|
map[string]string{"a.b": `"1"`},
|
||||||
`category "a.b" contains "."`,
|
`category "a.b" contains "."`,
|
||||||
},
|
},
|
||||||
"folder name with a dot": {
|
"folder name with a dot": {
|
||||||
map[string]string{"a.b/cat": `["1"]`},
|
map[string]string{"a.b/cat": `"1"`},
|
||||||
`/a.b: folder "a.b" contains "."`,
|
`/a.b: folder "a.b" contains "."`,
|
||||||
},
|
},
|
||||||
// The token grammar reserves three more characters. A name carrying one
|
// The token grammar reserves three more characters. A name carrying one
|
||||||
// still resolves by dot path, but no format can name it, so it is rejected
|
// still resolves by dot path, but no format can name it, so it is rejected
|
||||||
// where it is authored rather than at the token that cannot reach it.
|
// where it is authored rather than at the token that cannot reach it.
|
||||||
"field name with a pipe": {
|
"field name with a pipe": {
|
||||||
map[string]string{"a": `{"format":"{x}","x":["1"],"b|c":["2"]}`},
|
map[string]string{"a": `{"format":"{x}","x":"1","b|c":"2"}`},
|
||||||
`field "b|c" contains "|"`,
|
`field "b|c" contains "|"`,
|
||||||
},
|
},
|
||||||
"field name with a paren": {
|
"field name with a paren": {
|
||||||
map[string]string{"a": `{"format":"{x}","x":["1"],"b(c":["2"]}`},
|
map[string]string{"a": `{"format":"{x}","x":"1","b(c":"2"}`},
|
||||||
`field "b(c" contains "("`,
|
`field "b(c" contains "("`,
|
||||||
},
|
},
|
||||||
"field name with a closing brace": {
|
"field name with a closing brace": {
|
||||||
map[string]string{"a": `{"format":"{x}","x":["1"],"b}c":["2"]}`},
|
map[string]string{"a": `{"format":"{x}","x":"1","b}c":"2"}`},
|
||||||
`field "b}c" contains "}"`,
|
`field "b}c" contains "}"`,
|
||||||
},
|
},
|
||||||
"category name with a pipe": {
|
"category name with a pipe": {
|
||||||
map[string]string{"a|b": `["1"]`},
|
map[string]string{"a|b": `"1"`},
|
||||||
`category "a|b" contains "|"`,
|
`category "a|b" contains "|"`,
|
||||||
},
|
},
|
||||||
// An empty name is not a path segment, so List never offered it — while a
|
// An empty name is not a path segment, so List never offered it — while a
|
||||||
@@ -128,42 +127,42 @@ func TestNewErrors(t *testing.T) {
|
|||||||
`field "" is empty`,
|
`field "" is empty`,
|
||||||
},
|
},
|
||||||
"folder name with a paren": {
|
"folder name with a paren": {
|
||||||
map[string]string{"a(b/cat": `["1"]`},
|
map[string]string{"a(b/cat": `"1"`},
|
||||||
`folder "a(b" contains "("`,
|
`folder "a(b" contains "("`,
|
||||||
},
|
},
|
||||||
// A repeated arm skews an alternation, which weight is the spelling for.
|
// A repeated arm skews an alternation, which weight is the spelling for.
|
||||||
"repeated alternation arm": {
|
"repeated alternation arm": {
|
||||||
map[string]string{"a": `{"format":"{x|x}","x":["1"]}`},
|
map[string]string{"a": `{"format":"{x|x}","x":"1"}`},
|
||||||
`arm "x" is repeated`,
|
`arm "x" is repeated`,
|
||||||
},
|
},
|
||||||
"repeated arm among others": {
|
"repeated arm among others": {
|
||||||
map[string]string{"a": `{"format":"{x|y|x}","x":["1"],"y":["2"]}`},
|
map[string]string{"a": `{"format":"{x|y|x}","x":"1","y":"2"}`},
|
||||||
`arm "x" is repeated`,
|
`arm "x" is repeated`,
|
||||||
},
|
},
|
||||||
"repeated path arm": {
|
"repeated path arm": {
|
||||||
map[string]string{"a": `{"format":"{p.v|p.v}","p":{"format":"{v}","v":["1"]}}`},
|
map[string]string{"a": `{"format":"{p.v|p.v}","p":{"format":"{v}","v":"1"}}`},
|
||||||
`arm "p.v" is repeated`,
|
`arm "p.v" is repeated`,
|
||||||
},
|
},
|
||||||
// A reference arm is the only kind that reaches the repeat check by passing
|
// A reference arm is the only kind that reaches the repeat check by passing
|
||||||
// the per-arm checks rather than falling through them.
|
// the per-arm checks rather than falling through them.
|
||||||
"repeated reference arm": {
|
"repeated reference arm": {
|
||||||
map[string]string{"a": `["x"]`, "b": `{"format":"{..a|..a}"}`},
|
map[string]string{"a": `"x"`, "b": `"{..a|..a}"`},
|
||||||
`arm "..a" is repeated`,
|
`arm "..a" is repeated`,
|
||||||
},
|
},
|
||||||
// An arm that is broken on its own terms is reported as that, not as a
|
// An arm that is broken on its own terms is reported as that, not as a
|
||||||
// repeat: the repeat is a consequence of the real mistake.
|
// repeat: the repeat is a consequence of the real mistake.
|
||||||
"repeated arm with no path": {
|
"repeated arm with no path": {
|
||||||
map[string]string{"a": `{"format":"{..|..}"}`},
|
map[string]string{"a": `"{..|..}"`},
|
||||||
"reference has no path",
|
"reference has no path",
|
||||||
},
|
},
|
||||||
// No field can be named "", so the token is told that rather than sent to
|
// No field can be named "", so the token is told that rather than sent to
|
||||||
// name one — the fix "no field" points at is itself a load error.
|
// name one — the fix "no field" points at is itself a load error.
|
||||||
"repeated empty arm": {
|
"repeated empty arm": {
|
||||||
map[string]string{"a": `{"format":"{|}"}`},
|
map[string]string{"a": `"{|}"`},
|
||||||
"a name is never empty",
|
"a name is never empty",
|
||||||
},
|
},
|
||||||
"bare empty token": {
|
"bare empty token": {
|
||||||
map[string]string{"a": `{"format":"[{}]","x":["1"]}`},
|
map[string]string{"a": `{"format":"[{}]","x":"1"}`},
|
||||||
"a name is never empty",
|
"a name is never empty",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -183,16 +182,16 @@ func TestNewErrors(t *testing.T) {
|
|||||||
path string
|
path string
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
"option name as a field": {map[string]string{"a": `{"format":"{name} {Weight}kg","name":["Anvil"],"Weight":["7"]}`}, "a", "Anvil 7kg"},
|
"option name as a field": {map[string]string{"a": `{"format":"{name} {Weight}kg","name":"Anvil","Weight":"7"}`}, "a", "Anvil 7kg"},
|
||||||
"format spelling as a field": {map[string]string{"a": `{"format":"{Format}","Format":["PDF"]}`}, "a", "PDF"},
|
"format spelling as a field": {map[string]string{"a": `{"format":"{Format}","Format":"PDF"}`}, "a", "PDF"},
|
||||||
"hyphenated field": {map[string]string{"a": `{"format":"{x-y}","x-y":["1"]}`}, "a.x-y", "1"},
|
"hyphenated field": {map[string]string{"a": `{"format":"{x-y}","x-y":"1"}`}, "a.x-y", "1"},
|
||||||
"category named Format": {map[string]string{"Format": `["1"]`}, "Format", "1"},
|
"category named Format": {map[string]string{"Format": `"1"`}, "Format", "1"},
|
||||||
"folder named Repeat": {map[string]string{"Repeat/cat": `["1"]`}, "Repeat.cat", "1"},
|
"folder named Repeat": {map[string]string{"Repeat/cat": `"1"`}, "Repeat.cat", "1"},
|
||||||
"field with a closing paren": {map[string]string{"a": `{"format":"{b)c}","b)c":["2"]}`}, "a", "2"},
|
"field with a closing paren": {map[string]string{"a": `{"format":"{b)c}","b)c":"2"}`}, "a", "2"},
|
||||||
"repeat without a separator": {map[string]string{"a": `{"format":"{x}","repeat":3,"x":["1"]}`}, "a", "111"},
|
"repeat without a separator": {map[string]string{"a": `{"format":"{x}","repeat":3,"x":"1"}`}, "a", "111"},
|
||||||
// One name in two separate tokens is two independent draws, not a repeated
|
// One name in two separate tokens is two independent draws, not a repeated
|
||||||
// arm; only a repeat within one alternation is rejected.
|
// arm; only a repeat within one alternation is rejected.
|
||||||
"one name in two tokens": {map[string]string{"a": `{"format":"{x}{x}","x":["1"]}`}, "a", "11"},
|
"one name in two tokens": {map[string]string{"a": `{"format":"{x}{x}","x":"1"}`}, "a", "11"},
|
||||||
}
|
}
|
||||||
for name, c := range accepted {
|
for name, c := range accepted {
|
||||||
f, err := New(WithoutShippedData(), WithDataPath(writeData(t, c.files)))
|
f, err := New(WithoutShippedData(), WithDataPath(writeData(t, c.files)))
|
||||||
@@ -213,7 +212,7 @@ func TestDeepDottedPath(t *testing.T) {
|
|||||||
// on the path are single-variant, so it resolves deterministically.
|
// on the path are single-variant, so it resolves deterministically.
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
f.categories = map[string]node{
|
f.categories = map[string]node{
|
||||||
"deep": compiled(t, `[{"format":"{a}","a":[{"format":"{b}","b":[{"format":"{c}","c":[{"format":"{d}","d":["leaf"]}]}]}]}]`),
|
"deep": compiled(t, `{"format":"{a}","a":{"format":"{b}","b":{"format":"{c}","c":{"format":"{d}","d":"leaf"}}}}`),
|
||||||
}
|
}
|
||||||
if got, err := f.Fake("deep.a.b.c.d"); err != nil || got != "leaf" {
|
if got, err := f.Fake("deep.a.b.c.d"); err != nil || got != "leaf" {
|
||||||
t.Fatalf("Fake(deep.a.b.c.d) = %q, %v, want leaf", got, err)
|
t.Fatalf("Fake(deep.a.b.c.d) = %q, %v, want leaf", got, err)
|
||||||
@@ -224,11 +223,11 @@ func TestDeepDottedPath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDescendIntoLiteralErrors(t *testing.T) {
|
func TestDescendIntoStringErrors(t *testing.T) {
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
f.categories = map[string]node{"greeting": compiled(t, `["hej"]`)}
|
f.categories = map[string]node{"greeting": compiled(t, `"hej"`)}
|
||||||
if _, err := f.Fake("greeting.extra"); err == nil {
|
if _, err := f.Fake("greeting.extra"); err == nil || !strings.Contains(err.Error(), `no field "extra"`) {
|
||||||
t.Fatal("Fake(greeting.extra) = nil error, want descend-into-literal error")
|
t.Fatalf("Fake(greeting.extra) = %v, want a no-field error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,9 +235,9 @@ func TestDescendIntoLiteralErrors(t *testing.T) {
|
|||||||
// one call and failing on the next: every variant must carry the rest of the path.
|
// one call and failing on the next: every variant must carry the rest of the path.
|
||||||
func TestPathThroughChoice(t *testing.T) {
|
func TestPathThroughChoice(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"every": `[{"format":"{f}","f":["1"]},{"format":"{f}","f":["2"]}]`,
|
"every": `[{"format":"{f}","f":"1"},{"format":"{f}","f":"2"}]`,
|
||||||
"notall": `[{"format":"{f}","f":["1"]},["plain"]]`,
|
"notall": `[{"format":"{f}","f":"1"},"plain"]`,
|
||||||
"some": `[{"format":"{f}","f":["1"]},{"format":"{f}","f":["2"],"extra":["x"]}]`,
|
"some": `[{"format":"{f}","f":"1"},{"format":"{f}","f":"2","extra":"x"}]`,
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
@@ -270,7 +269,7 @@ func TestPathThroughChoice(t *testing.T) {
|
|||||||
// wherever it appears.
|
// wherever it appears.
|
||||||
func TestPathKeyIsUnambiguous(t *testing.T) {
|
func TestPathKeyIsUnambiguous(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"cat": `[{"format":"{a.b}","a.b":["1"]},{"format":"{a}","a":{"format":"{b}","b":["2"]}}]`,
|
"cat": `[{"format":"{a.b}","a.b":"1"},{"format":"{a}","a":{"format":"{b}","b":"2"}}]`,
|
||||||
})
|
})
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(dir))
|
_, err := New(WithoutShippedData(), WithDataPath(dir))
|
||||||
if err == nil || !strings.Contains(err.Error(), `field "a.b" contains "."`) {
|
if err == nil || !strings.Contains(err.Error(), `field "a.b" contains "."`) {
|
||||||
@@ -278,7 +277,7 @@ func TestPathKeyIsUnambiguous(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// The same data without the dotted key is fine, and the path resolves.
|
// The same data without the dotted key is fine, and the path resolves.
|
||||||
f := newGenerator(t, writeData(t, map[string]string{
|
f := newGenerator(t, writeData(t, map[string]string{
|
||||||
"cat": `{"format":"{a.b}","a":{"format":"{b}","b":["2"]}}`,
|
"cat": `{"format":"{a.b}","a":{"format":"{b}","b":"2"}}`,
|
||||||
}), WithSeed(1))
|
}), WithSeed(1))
|
||||||
if !slices.Contains(f.List(), "cat.a.b") {
|
if !slices.Contains(f.List(), "cat.a.b") {
|
||||||
t.Error("List() omits cat.a.b, which the data carries")
|
t.Error("List() omits cat.a.b, which the data carries")
|
||||||
@@ -303,8 +302,8 @@ func TestMissingFieldNamesItself(t *testing.T) {
|
|||||||
|
|
||||||
func TestCategoryRootShapes(t *testing.T) {
|
func TestCategoryRootShapes(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"obj": `{"format":"00"}`, // object root
|
"obj": `"{digits(2)}"`, // object root
|
||||||
"lit": `"hello"`, // bare-string root
|
"lit": `"hello"`, // bare-string root
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "obj"); !regexp.MustCompile(`^\d\d$`).MatchString(got) {
|
if got := fake(t, f, "obj"); !regexp.MustCompile(`^\d\d$`).MatchString(got) {
|
||||||
|
|||||||
+4
-10
@@ -109,10 +109,9 @@ func New(opts ...Option) (*Generator, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List returns the sorted dotted paths Fake can render: every category, the dotted
|
// List returns the sorted dotted paths Fake can render: every category, the dotted
|
||||||
// fields within a template, and folder segments — descending transparently through
|
// fields within a template, and folder segments. A choice consumes no segment, so a
|
||||||
// single-variant choices the way a reference does. A choice consumes no segment, so
|
// path continues through one only where every variant carries it, which is the
|
||||||
// a path continues through a multi-variant one only where every variant carries it,
|
// rule Fake applies too: List is the set of paths Fake accepts.
|
||||||
// which is the rule Fake applies too: List is the set of paths Fake accepts.
|
|
||||||
func (f *Generator) List() []string {
|
func (f *Generator) List() []string {
|
||||||
var out []string
|
var out []string
|
||||||
for _, name := range sortedNames(f.categories) {
|
for _, name := range sortedNames(f.categories) {
|
||||||
@@ -148,22 +147,17 @@ func paths(n node) []string {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
case *choice:
|
case *choice:
|
||||||
if len(n.items) == 1 {
|
|
||||||
return paths(n.items[0])
|
|
||||||
}
|
|
||||||
out := []string{""}
|
out := []string{""}
|
||||||
for p := range n.shared {
|
for p := range n.shared {
|
||||||
out = append(out, p)
|
out = append(out, p)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
case literal:
|
|
||||||
return []string{""}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sharedPaths is the sub-paths every item carries — the only ones a path may step
|
// sharedPaths is the sub-paths every item carries — the only ones a path may step
|
||||||
// through a multi-variant choice to reach. It intersects, bailing as soon as the set
|
// through a choice to reach. It intersects, bailing as soon as the set
|
||||||
// is empty, which is immediate for a choice of plain strings.
|
// is empty, which is immediate for a choice of plain strings.
|
||||||
func sharedPaths(items []node) map[string]bool {
|
func sharedPaths(items []node) map[string]bool {
|
||||||
shared := subPaths(items[0])
|
shared := subPaths(items[0])
|
||||||
|
|||||||
Executable
+72
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rewrite data files from the class-char format grammar to the literal-text one.
|
||||||
|
|
||||||
|
convert.py data/sv_SE/person.json ...
|
||||||
|
|
||||||
|
Old: 0 1 A a are character classes, # escapes. New: text is literal; a run of
|
||||||
|
0/A/a becomes {digits(n)}/{upper(n)}/{lower(n)}, 1 followed by n zeros becomes
|
||||||
|
{int(10^n, 10^(n+1)-1)}, a literal brace becomes {{ or }}.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
RUN = {"0": "{{digits({})}}", "A": "{{upper({})}}", "a": "{{lower({})}}"}
|
||||||
|
|
||||||
|
|
||||||
|
def literal(c):
|
||||||
|
return {"{": "{{", "}": "}}"}.get(c, c)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_format(f):
|
||||||
|
out, i, n = [], 0, len(f)
|
||||||
|
while i < n:
|
||||||
|
c = f[i]
|
||||||
|
if c == "#":
|
||||||
|
i += 1
|
||||||
|
if i < n:
|
||||||
|
out.append(literal(f[i]))
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
out.append("#")
|
||||||
|
elif c == "{":
|
||||||
|
j = f.find("}", i)
|
||||||
|
if j < 0:
|
||||||
|
out.append(f[i:])
|
||||||
|
break
|
||||||
|
out.append(f[i:j + 1])
|
||||||
|
i = j + 1
|
||||||
|
elif c in RUN:
|
||||||
|
k = 0
|
||||||
|
while i < n and f[i] == c:
|
||||||
|
k += 1
|
||||||
|
i += 1
|
||||||
|
out.append(RUN[c].format(k))
|
||||||
|
elif c == "1":
|
||||||
|
i += 1
|
||||||
|
k = 0
|
||||||
|
while i < n and f[i] == "0":
|
||||||
|
k += 1
|
||||||
|
i += 1
|
||||||
|
out.append("{{int({},{})}}".format(10 ** k, 10 ** (k + 1) - 1))
|
||||||
|
else:
|
||||||
|
out.append(literal(c))
|
||||||
|
i += 1
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
FORMAT_VALUE = re.compile(r'("format":\s*")((?:[^"\\]|\\.)*)(")')
|
||||||
|
|
||||||
|
|
||||||
|
def convert_text(text):
|
||||||
|
return FORMAT_VALUE.sub(lambda m: m.group(1) + convert_format(m.group(2)) + m.group(3), text)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
for path in sys.argv[1:]:
|
||||||
|
with open(path) as fh:
|
||||||
|
before = fh.read()
|
||||||
|
after = convert_text(before)
|
||||||
|
if after != before:
|
||||||
|
with open(path, "w") as fh:
|
||||||
|
fh.write(after)
|
||||||
|
print("converted", path)
|
||||||
Executable
+87
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rewrite data files to the one spelling each shape has.
|
||||||
|
|
||||||
|
reshape.py data/sv_SE/person.json ...
|
||||||
|
|
||||||
|
A one-item choice becomes its item; an object holding only a format becomes that
|
||||||
|
string; weight 1 and repeat 1 are dropped; a repeated choice item becomes one
|
||||||
|
item with a weight.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
OPTIONS = ("format", "weight", "repeat", "separator")
|
||||||
|
WIDTH = 110
|
||||||
|
|
||||||
|
|
||||||
|
def reshape(n):
|
||||||
|
if isinstance(n, list):
|
||||||
|
items = [reshape(x) for x in n]
|
||||||
|
if len(items) == 1:
|
||||||
|
return items[0]
|
||||||
|
out, at = [], {}
|
||||||
|
for x in items:
|
||||||
|
key = json.dumps(x, sort_keys=True)
|
||||||
|
if key in at:
|
||||||
|
i = at[key]
|
||||||
|
if isinstance(out[i], str):
|
||||||
|
out[i] = {"format": out[i], "weight": 2}
|
||||||
|
elif isinstance(out[i], dict):
|
||||||
|
out[i]["weight"] = out[i].get("weight", 1) + 1
|
||||||
|
else:
|
||||||
|
out.append(x)
|
||||||
|
continue
|
||||||
|
at[key] = len(out)
|
||||||
|
out.append(x)
|
||||||
|
return out
|
||||||
|
if isinstance(n, dict):
|
||||||
|
d = {k: (v if k in OPTIONS else reshape(v)) for k, v in n.items()}
|
||||||
|
if d.get("weight") == 1:
|
||||||
|
del d["weight"]
|
||||||
|
if d.get("repeat") == 1:
|
||||||
|
del d["repeat"]
|
||||||
|
if list(d) == ["format"]:
|
||||||
|
return d["format"]
|
||||||
|
return d
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def scalar(v):
|
||||||
|
return json.dumps(v, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
def one_line(n):
|
||||||
|
if isinstance(n, dict):
|
||||||
|
return "{ " + ", ".join(scalar(k) + ": " + one_line(v) for k, v in n.items()) + " }"
|
||||||
|
if isinstance(n, list):
|
||||||
|
return "[" + ", ".join(one_line(x) for x in n) + "]"
|
||||||
|
return scalar(n)
|
||||||
|
|
||||||
|
|
||||||
|
def dump(n, indent=0):
|
||||||
|
pad = " " * indent
|
||||||
|
if isinstance(n, dict):
|
||||||
|
line = one_line(n)
|
||||||
|
if len(pad) + len(line) <= WIDTH and not any(isinstance(v, dict) for v in n.values()):
|
||||||
|
return line
|
||||||
|
body = ",\n".join(pad + " " + scalar(k) + ": " + dump(v, indent + 1) for k, v in n.items())
|
||||||
|
return "{\n" + body + "\n" + pad + "}"
|
||||||
|
if isinstance(n, list):
|
||||||
|
if all(isinstance(x, str) for x in n):
|
||||||
|
return one_line(n)
|
||||||
|
line = one_line(n)
|
||||||
|
if len(pad) + len(line) <= WIDTH:
|
||||||
|
return line
|
||||||
|
body = ",\n".join(pad + " " + dump(x, indent + 1) for x in n)
|
||||||
|
return "[\n" + body + "\n" + pad + "]"
|
||||||
|
return scalar(n)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
for path in sys.argv[1:]:
|
||||||
|
with open(path) as fh:
|
||||||
|
before = json.load(fh)
|
||||||
|
after = dump(reshape(before)) + "\n"
|
||||||
|
with open(path, "w") as fh:
|
||||||
|
fh.write(after)
|
||||||
|
print("reshaped", path)
|
||||||
+16
-16
@@ -13,13 +13,13 @@ import (
|
|||||||
// folder segments — descending transparently through single-variant choices.
|
// folder segments — descending transparently through single-variant choices.
|
||||||
func TestList(t *testing.T) {
|
func TestList(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"person": `{"format":"{first} {last}","first":["A"],"last":["B"]}`,
|
"person": `{"format":"{first} {last}","first":"A","last":"B"}`,
|
||||||
"word": `["x", "y"]`,
|
"word": `["x", "y"]`,
|
||||||
"geo/city": `["Z"]`,
|
"geo/city": `"Z"`,
|
||||||
// A bound {..path} reference is a render edge, not an addressable field.
|
// A bound {..path} reference is a render edge, not an addressable field.
|
||||||
"greeting": `{"format":"hej {..person.first} and {own}","own":["x"]}`,
|
"greeting": `{"format":"hej {..person.first} and {own}","own":"x"}`,
|
||||||
// Only the fields every variant carries are addressable, so "extra" is not.
|
// Only the fields every variant carries are addressable, so "extra" is not.
|
||||||
"coin": `[{"format":"{code}","code":["A"],"name":["Aa"]},{"format":"{code}","code":["B"],"name":["Bb"],"extra":["x"]}]`,
|
"coin": `[{"format":"{code}","code":"A","name":"Aa"},{"format":"{code}","code":"B","name":"Bb","extra":"x"}]`,
|
||||||
})
|
})
|
||||||
got := newGenerator(t, dir, WithSeed(1)).List()
|
got := newGenerator(t, dir, WithSeed(1)).List()
|
||||||
want := []string{"coin", "coin.code", "coin.name", "geo.city", "greeting", "greeting.own", "person", "person.first", "person.last", "word"}
|
want := []string{"coin", "coin.code", "coin.name", "geo.city", "greeting", "greeting.own", "person", "person.first", "person.last", "word"}
|
||||||
@@ -66,8 +66,8 @@ func TestNewEmptyDirErrors(t *testing.T) {
|
|||||||
// namespace, so data/<loc>/person.json is reachable as "<loc>.person".
|
// namespace, so data/<loc>/person.json is reachable as "<loc>.person".
|
||||||
func TestFoldersBecomeDotPaths(t *testing.T) {
|
func TestFoldersBecomeDotPaths(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"sv_SE/greeting": `["hej"]`,
|
"sv_SE/greeting": `"hej"`,
|
||||||
"en_US/greeting": `["hi"]`,
|
"en_US/greeting": `"hi"`,
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "sv_SE.greeting"); got != "hej" {
|
if got := fake(t, f, "sv_SE.greeting"); got != "hej" {
|
||||||
@@ -82,7 +82,7 @@ func TestFoldersBecomeDotPaths(t *testing.T) {
|
|||||||
// a/b/c, then deep JSON fields inside the file at the end of that path.
|
// a/b/c, then deep JSON fields inside the file at the end of that path.
|
||||||
func TestNestedFoldersAndJSON(t *testing.T) {
|
func TestNestedFoldersAndJSON(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"a/b/c/thing": `{"format":"{x}","x":{"format":"{y}","y":{"format":"{z}","z":["leaf"]}}}`,
|
"a/b/c/thing": `{"format":"{x}","x":{"format":"{y}","y":{"format":"{z}","z":"leaf"}}}`,
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
// Folders a.b.c, file thing, then JSON fields x.y.z — one continuous path.
|
// Folders a.b.c, file thing, then JSON fields x.y.z — one continuous path.
|
||||||
@@ -96,7 +96,7 @@ func TestNestedFoldersAndJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderingAFolderErrors(t *testing.T) {
|
func TestRenderingAFolderErrors(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{"sv_SE/greeting": `["hej"]`})
|
dir := writeData(t, map[string]string{"sv_SE/greeting": `"hej"`})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
if _, err := f.Fake("sv_SE"); err == nil {
|
if _, err := f.Fake("sv_SE"); err == nil {
|
||||||
t.Fatal("Fake(folder) = nil error, want a not-a-value error")
|
t.Fatal("Fake(folder) = nil error, want a not-a-value error")
|
||||||
@@ -106,8 +106,8 @@ func TestRenderingAFolderErrors(t *testing.T) {
|
|||||||
// TestMultiPathLastWins loads two dirs; on a name clash the later dir wins, and
|
// TestMultiPathLastWins loads two dirs; on a name clash the later dir wins, and
|
||||||
// non-clashing entries from both are reachable (data combines).
|
// non-clashing entries from both are reachable (data combines).
|
||||||
func TestMultiPathLastWins(t *testing.T) {
|
func TestMultiPathLastWins(t *testing.T) {
|
||||||
a := writeData(t, map[string]string{"greeting": `["from-a"]`, "only-a": `["a"]`})
|
a := writeData(t, map[string]string{"greeting": `"from-a"`, "only-a": `"a"`})
|
||||||
b := writeData(t, map[string]string{"greeting": `["from-b"]`, "only-b": `["b"]`})
|
b := writeData(t, map[string]string{"greeting": `"from-b"`, "only-b": `"b"`})
|
||||||
f := newGeneratorN(t, []string{a, b}, WithSeed(1))
|
f := newGeneratorN(t, []string{a, b}, WithSeed(1))
|
||||||
if got := fake(t, f, "greeting"); got != "from-b" {
|
if got := fake(t, f, "greeting"); got != "from-b" {
|
||||||
t.Fatalf("greeting = %q, want from-b (last loaded wins)", got)
|
t.Fatalf("greeting = %q, want from-b (last loaded wins)", got)
|
||||||
@@ -124,8 +124,8 @@ func TestMultiPathLastWins(t *testing.T) {
|
|||||||
// children rather than replacing wholesale: each dir adds a file to sv_SE, and
|
// children rather than replacing wholesale: each dir adds a file to sv_SE, and
|
||||||
// a per-file clash inside still resolves last-wins.
|
// a per-file clash inside still resolves last-wins.
|
||||||
func TestMultiPathMergesFolders(t *testing.T) {
|
func TestMultiPathMergesFolders(t *testing.T) {
|
||||||
a := writeData(t, map[string]string{"sv_SE/person": `["from-a"]`, "sv_SE/shared": `["a"]`})
|
a := writeData(t, map[string]string{"sv_SE/person": `"from-a"`, "sv_SE/shared": `"a"`})
|
||||||
b := writeData(t, map[string]string{"sv_SE/company": `["from-b"]`, "sv_SE/shared": `["b"]`})
|
b := writeData(t, map[string]string{"sv_SE/company": `"from-b"`, "sv_SE/shared": `"b"`})
|
||||||
f := newGeneratorN(t, []string{a, b}, WithSeed(1))
|
f := newGeneratorN(t, []string{a, b}, WithSeed(1))
|
||||||
if got := fake(t, f, "sv_SE.person"); got != "from-a" {
|
if got := fake(t, f, "sv_SE.person"); got != "from-a" {
|
||||||
t.Fatalf("sv_SE.person = %q, want from-a (folder merged, not replaced)", got)
|
t.Fatalf("sv_SE.person = %q, want from-a (folder merged, not replaced)", got)
|
||||||
@@ -163,10 +163,10 @@ func TestListedPathsAllRender(t *testing.T) {
|
|||||||
// carrying no JSON never contributed a namespace, so neither may fail the load.
|
// carrying no JSON never contributed a namespace, so neither may fail the load.
|
||||||
func TestHiddenEntriesAreSkipped(t *testing.T) {
|
func TestHiddenEntriesAreSkipped(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"cat": `["V"]`,
|
"cat": `"V"`,
|
||||||
".git/HEAD": `["ignored"]`,
|
".git/HEAD": `"ignored"`,
|
||||||
".hidden": `["ignored"]`,
|
".hidden": `"ignored"`,
|
||||||
"empty.folder/doc": `["ignored"]`,
|
"empty.folder/doc": `"ignored"`,
|
||||||
})
|
})
|
||||||
if err := os.Rename(filepath.Join(dir, "empty.folder", "doc.json"), filepath.Join(dir, "empty.folder", "doc.txt")); err != nil {
|
if err := os.Rename(filepath.Join(dir, "empty.folder", "doc.json"), filepath.Join(dir, "empty.folder", "doc.txt")); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
package fejkdata
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// node is a compiled template element: literal, choice, or template. Compiling
|
// node is a compiled template element: a choice or a template. Compiling JSON
|
||||||
// JSON into these once (see compile) means rendering never re-inspects the raw
|
// into these once (see compile) means rendering never re-inspects the raw JSON or
|
||||||
// JSON or re-sums weights.
|
// re-sums weights.
|
||||||
type node interface{ isNode() }
|
type node interface{ isNode() }
|
||||||
|
|
||||||
// literal is emitted verbatim, never formatted.
|
|
||||||
type literal string
|
|
||||||
|
|
||||||
func (literal) isNode() {}
|
|
||||||
|
|
||||||
// group is a namespace of named children, built from a directory of JSON files
|
// group is a namespace of named children, built from a directory of JSON files
|
||||||
// and subdirectories. It has no value of its own: descend into a named child by
|
// and subdirectories. It has no value of its own: descend into a named child by
|
||||||
// dot path; rendering one is an error (see Fake).
|
// dot path; rendering one is an error (see Fake).
|
||||||
@@ -36,16 +32,20 @@ type choice struct {
|
|||||||
|
|
||||||
func (*choice) isNode() {}
|
func (*choice) isNode() {}
|
||||||
|
|
||||||
// template renders a format string, substituting {tokens} from fields. repeat
|
// template renders a format string, substituting {tokens} from fields. A bare
|
||||||
// (default 1) renders that format that many times and joins the results with
|
// JSON string is a template with no fields. repeat (default 1) renders that format
|
||||||
// separator (default ""), each render an independent pick.
|
// that many times and joins the results with separator (default ""), each render
|
||||||
|
// an independent pick.
|
||||||
type template struct {
|
type template struct {
|
||||||
format string
|
format string
|
||||||
fields map[string]node
|
fields map[string]node
|
||||||
repeat int
|
repeat int
|
||||||
separator string
|
separator string
|
||||||
ops []op // format compiled once (see compileOps); what expand walks
|
ops []op // format compiled once (see compileOps); what expand walks
|
||||||
grow int // minimum output size, to size the render buffer
|
grow int // minimum output size, to size the render buffer
|
||||||
|
fixed bool // no op varies, so every render is lit
|
||||||
|
lit string // the whole output when fixed
|
||||||
|
refs map[string]string // each {..path} the format reads -> the head it is bound under
|
||||||
// bound maps each field the format addresses by dotted path to one path token
|
// bound maps each field the format addresses by dotted path to one path token
|
||||||
// reading it, which is the half of an overlap the fences name. nil when the
|
// reading it, which is the half of an overlap the fences name. nil when the
|
||||||
// format takes no path.
|
// format takes no path.
|
||||||
@@ -72,7 +72,7 @@ func compile(v any) (node, error) {
|
|||||||
func compileItem(v any) (node, error) {
|
func compileItem(v any) (node, error) {
|
||||||
switch v := v.(type) {
|
switch v := v.(type) {
|
||||||
case string:
|
case string:
|
||||||
return literal(v), nil
|
return compileString(v)
|
||||||
case []any:
|
case []any:
|
||||||
return compileChoice(v)
|
return compileChoice(v)
|
||||||
case map[string]any:
|
case map[string]any:
|
||||||
@@ -82,10 +82,39 @@ func compileItem(v any) (node, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func compileString(s string) (node, error) {
|
||||||
|
if err := checkTokens(s, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
t := &template{format: s, repeat: 1}
|
||||||
|
t.compileFormat()
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// compileFormat compiles the format into ops once every field is in place.
|
||||||
|
func (t *template) compileFormat() {
|
||||||
|
t.ops, t.grow, t.bound, t.held = compileOps(t.format, t.refs)
|
||||||
|
t.fixed = true
|
||||||
|
for _, o := range t.ops {
|
||||||
|
if o.kind != 'l' {
|
||||||
|
t.fixed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.fixed && len(t.ops) == 1 {
|
||||||
|
t.lit = t.ops[0].lit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func compileChoice(items []any) (node, error) {
|
func compileChoice(items []any) (node, error) {
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return nil, fmt.Errorf("empty choice")
|
return nil, fmt.Errorf("empty choice")
|
||||||
}
|
}
|
||||||
|
if len(items) == 1 {
|
||||||
|
return nil, fmt.Errorf("a one-item choice is its item; write the item")
|
||||||
|
}
|
||||||
|
if err := checkNoRepeatedItem(items); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
c := &choice{items: make([]node, len(items))}
|
c := &choice{items: make([]node, len(items))}
|
||||||
cum := make([]float64, len(items))
|
cum := make([]float64, len(items))
|
||||||
var total float64
|
var total float64
|
||||||
@@ -112,14 +141,37 @@ func compileChoice(items []any) (node, error) {
|
|||||||
}
|
}
|
||||||
c.cum = cum
|
c.cum = cum
|
||||||
}
|
}
|
||||||
if len(c.items) > 1 {
|
// Safe to precompute: a choice's items come from one file, so no group can
|
||||||
// Safe to precompute: a choice's items come from one file, so no group can
|
// appear inside one, and neither mergeChildren nor linkRefs can reach in.
|
||||||
// appear inside one, and neither mergeChildren nor linkRefs can reach in.
|
c.shared = sharedPaths(c.items)
|
||||||
c.shared = sharedPaths(c.items)
|
|
||||||
}
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkNoRepeatedItem rejects a choice that lists one item twice: a pick is even
|
||||||
|
// over the items, so a repeat is a second spelling of weight. The error names the
|
||||||
|
// spelling that does skew a pick.
|
||||||
|
func checkNoRepeatedItem(items []any) error {
|
||||||
|
seen := make(map[string]int, len(items))
|
||||||
|
for i, raw := range items {
|
||||||
|
key, isString := raw.(string)
|
||||||
|
if !isString {
|
||||||
|
b, err := json.Marshal(raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key = "\x00" + string(b)
|
||||||
|
}
|
||||||
|
if j, dup := seen[key]; dup {
|
||||||
|
if s, isString := raw.(string); isString {
|
||||||
|
return fmt.Errorf("choice item %q is repeated; skew the odds with a weight instead: { \"format\": %q, \"weight\": 2 }", s, s)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("choice item %d repeats item %d; skew the odds with a weight on one of them instead", i, j)
|
||||||
|
}
|
||||||
|
seen[string(key)] = i
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func compileTemplate(m map[string]any) (node, error) {
|
func compileTemplate(m map[string]any) (node, error) {
|
||||||
format, ok := m["format"].(string)
|
format, ok := m["format"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -160,19 +212,22 @@ func compileTemplate(m map[string]any) (node, error) {
|
|||||||
}
|
}
|
||||||
t.fields[k] = n
|
t.fields[k] = n
|
||||||
}
|
}
|
||||||
|
if _, weighted := m["weight"]; len(t.fields) == 0 && repeat == 1 && !weighted {
|
||||||
|
return nil, fmt.Errorf("an object holding only a format is a string; write %q", format)
|
||||||
|
}
|
||||||
if err := checkTokens(format, t.fields); err != nil {
|
if err := checkTokens(format, t.fields); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
t.ops, t.grow, t.bound, t.held = compileOps(format)
|
t.compileFormat()
|
||||||
if err := checkNoOverlap(format, t.bound); err != nil {
|
if err := checkNoOverlap(format, t.bound, nil); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkPath reports whether a token's dotted tail can address a node whichever way
|
// checkPath reports whether a token's dotted tail can address a node whichever way
|
||||||
// the draw goes, by the reachability rule descend applies — a multi-variant choice
|
// the draw goes, by the reachability rule descend applies — a choice must carry
|
||||||
// must carry the whole remaining path in the set every variant shares — plus the
|
// the whole remaining path in the set every variant shares — plus the
|
||||||
// rules a held draw adds, which descend has no need of: a level a path reads may
|
// rules a held draw adds, which descend has no need of: a level a path reads may
|
||||||
// not carry a repeat, and each variant answers for that itself. So a path that
|
// not carry a repeat, and each variant answers for that itself. So a path that
|
||||||
// validates here resolves on every render, and a typo is a New-time error.
|
// validates here resolves on every render, and a typo is a New-time error.
|
||||||
@@ -191,29 +246,24 @@ func checkPath(n node, tail []string, level string) error {
|
|||||||
}
|
}
|
||||||
return checkPath(child, tail[1:], level+"."+tail[0])
|
return checkPath(child, tail[1:], level+"."+tail[0])
|
||||||
case *choice:
|
case *choice:
|
||||||
if len(n.items) > 1 {
|
if want := strings.Join(tail, "."); !n.shared[want] {
|
||||||
if want := strings.Join(tail, "."); !n.shared[want] {
|
return unreachableInChoice(n, want)
|
||||||
return unreachableInChoice(n, want)
|
|
||||||
}
|
|
||||||
// Reachability is settled; each variant still answers for itself, so a
|
|
||||||
// rule about the level (its repeat) holds behind a choice as in front.
|
|
||||||
for _, item := range n.items {
|
|
||||||
if err := checkPath(item, tail, level); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
return checkPath(n.items[0], tail, level)
|
// Reachability is settled; each variant still answers for itself, so a
|
||||||
case literal:
|
// rule about the level (its repeat) holds behind a choice as in front.
|
||||||
return fmt.Errorf("a plain string has no field %q", tail[0])
|
for _, item := range n.items {
|
||||||
|
if err := checkPath(item, tail, level); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("cannot descend into %T at %q", n, tail[0])
|
return fmt.Errorf("cannot descend into %T at %q", n, tail[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// repeatOf reads a template's "repeat" (default 1): how many times its format
|
// repeatOf reads a template's "repeat" (default 1): how many times its format
|
||||||
// is rendered and concatenated. A present one must be a positive integer.
|
// is rendered and concatenated. A present one must be an integer above 1.
|
||||||
func repeatOf(m map[string]any) (int, error) {
|
func repeatOf(m map[string]any) (int, error) {
|
||||||
rv, ok := m["repeat"]
|
rv, ok := m["repeat"]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -226,6 +276,9 @@ func repeatOf(m map[string]any) (int, error) {
|
|||||||
if math.IsNaN(r) || math.IsInf(r, 0) || r < 1 || r != math.Trunc(r) {
|
if math.IsNaN(r) || math.IsInf(r, 0) || r < 1 || r != math.Trunc(r) {
|
||||||
return 0, fmt.Errorf("repeat must be a positive integer, got %v", rv)
|
return 0, fmt.Errorf("repeat must be a positive integer, got %v", rv)
|
||||||
}
|
}
|
||||||
|
if r == 1 {
|
||||||
|
return 0, fmt.Errorf("repeat 1 is the default, so it has no effect; drop it")
|
||||||
|
}
|
||||||
if r > maxLen { // cap so a fat-fingered repeat can't build a multi-GB string
|
if r > maxLen { // cap so a fat-fingered repeat can't build a multi-GB string
|
||||||
return 0, fmt.Errorf("repeat %v exceeds the maximum %d", rv, maxLen)
|
return 0, fmt.Errorf("repeat %v exceeds the maximum %d", rv, maxLen)
|
||||||
}
|
}
|
||||||
@@ -250,14 +303,18 @@ func weightOf(raw any) (float64, error) {
|
|||||||
if w < 0 || math.IsNaN(w) || math.IsInf(w, 0) {
|
if w < 0 || math.IsNaN(w) || math.IsInf(w, 0) {
|
||||||
return 0, fmt.Errorf("weight must be finite and non-negative, got %v", w)
|
return 0, fmt.Errorf("weight must be finite and non-negative, got %v", w)
|
||||||
}
|
}
|
||||||
|
if w == 1 {
|
||||||
|
return 0, fmt.Errorf("weight 1 is the default, so it has no effect; drop it")
|
||||||
|
}
|
||||||
return w, nil
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// reservedInName is what a category, folder or field name may not contain: a dot
|
// reservedInName is what a category, folder or field name may not contain: a dot
|
||||||
// separates the segments of a path, '|' the arms of a token, '(' opens a function
|
// separates the segments of a path, '|' the arms of a token, '(' opens a function
|
||||||
// call and '}' ends the token. A name carrying one is reachable by no format, so it
|
// call and braces delimit the token. A name carrying one is reachable by no format,
|
||||||
// is rejected where it is authored rather than at the token that cannot reach it.
|
// so it is rejected where it is authored rather than at the token that cannot
|
||||||
const reservedInName = ".|(}"
|
// reach it.
|
||||||
|
const reservedInName = ".|({}"
|
||||||
|
|
||||||
// reservedList spells reservedInName for an error message, so the two cannot drift.
|
// reservedList spells reservedInName for an error message, so the two cannot drift.
|
||||||
var reservedList = strings.Join(strings.Split(reservedInName, ""), " ")
|
var reservedList = strings.Join(strings.Split(reservedInName, ""), " ")
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package fejkdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOneItemChoiceIsRejected(t *testing.T) {
|
||||||
|
for _, src := range []string{`["x"]`, `[{"format":"{d}","d":"x"}]`, `{"format":"{w}","w":["only"]}`} {
|
||||||
|
if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), "one-item choice") {
|
||||||
|
t.Errorf("compile(%s) = %v, want the one-item choice rejected", src, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepeatedChoiceItemIsRejected(t *testing.T) {
|
||||||
|
for src, want := range map[string]string{
|
||||||
|
`["a", "a", "b"]`: `{ "format": "a", "weight": 2 }`,
|
||||||
|
`[{"format":"{x}","x":"1"},{"format":"{x}","x":"1"}]`: "repeats item",
|
||||||
|
`{"format":"{w}","w":["", "", "x"]}`: `{ "format": "", "weight": 2 }`,
|
||||||
|
} {
|
||||||
|
if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) {
|
||||||
|
t.Errorf("compile(%s) = %v, want an error naming %s", src, err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := compile(parse(t, `[{"format":"a","weight":2}, "b"]`)); err != nil {
|
||||||
|
t.Errorf("compile(weighted a, b) = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInertObjectIsRejected(t *testing.T) {
|
||||||
|
for src, want := range map[string]string{
|
||||||
|
`{"format":"Malmö"}`: `write "Malmö"`,
|
||||||
|
`{"format":"{digits(3)}"}`: `write "{digits(3)}"`,
|
||||||
|
`[{"format":"a","weight":1},"b"]`: "weight 1",
|
||||||
|
`{"format":"{x}","x":"v","repeat":1}`: "repeat 1",
|
||||||
|
`{"format":"{x}","x":"v","separator":","}`: "separator",
|
||||||
|
} {
|
||||||
|
if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) {
|
||||||
|
t.Errorf("compile(%s) = %v, want an error mentioning %s", src, err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ok := range []string{`[{"format":"a","weight":2},"b"]`, `{"format":"ab","repeat":2}`, `{"format":"{x}","x":"v"}`, `"{digits(3)}"`} {
|
||||||
|
if _, err := compile(parse(t, ok)); err != nil {
|
||||||
|
t.Errorf("compile(%s) = %v", ok, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package fejkdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var jsonBlock = regexp.MustCompile("(?s)```json\n(.*?)```")
|
||||||
|
|
||||||
|
func readme(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
src, err := os.ReadFile("README.md")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return string(src)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadmeExamplesLoadAndRender(t *testing.T) {
|
||||||
|
n := 0
|
||||||
|
for _, m := range jsonBlock.FindAllStringSubmatch(readme(t), -1) {
|
||||||
|
body := m[1]
|
||||||
|
if strings.Contains(body, "…") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
f, err := New(WithDataPath(writeData(t, map[string]string{"example": body})), WithSeed(1))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("README example does not load: %v\n%s", err, body)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
|
if _, err := f.Fake("example"); err != nil {
|
||||||
|
t.Errorf("README example does not render: %v\n%s", err, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
if n < 8 {
|
||||||
|
t.Fatalf("found only %d README examples", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadmeSQLExampleOutput(t *testing.T) {
|
||||||
|
src := readme(t)
|
||||||
|
src = src[strings.Index(src, "### Your own data"):]
|
||||||
|
block := jsonBlock.FindStringSubmatch(src)
|
||||||
|
want := regexp.MustCompile(`(?m)^# (INSERT .*)$`).FindStringSubmatch(src)
|
||||||
|
if block == nil || want == nil {
|
||||||
|
t.Fatal("README lost the SQL example or its printed output")
|
||||||
|
}
|
||||||
|
f, err := New(WithDataPath(writeData(t, map[string]string{"sql": block[1]})), WithSeed(1))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := fake(t, f, "sql"); got != want[1] {
|
||||||
|
t.Errorf("README SQL example with seed 1 = %q, README prints %q", got, want[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
+151
-84
@@ -8,31 +8,47 @@ import (
|
|||||||
|
|
||||||
// refPrefix marks a {..path} token: a reference to a node elsewhere in the data
|
// refPrefix marks a {..path} token: a reference to a node elsewhere in the data
|
||||||
// root rather than a sibling field. The path is resolved across every loaded
|
// root rather than a sibling field. The path is resolved across every loaded
|
||||||
// directory (see linkRefs), and is stricter than the one Fake takes: a reference
|
// directory (see linkRefs).
|
||||||
// binds one node, so it cannot step through a multi-variant choice even where Fake
|
|
||||||
// and List can.
|
|
||||||
const refPrefix = ".."
|
const refPrefix = ".."
|
||||||
|
|
||||||
func isRef(name string) bool { return strings.HasPrefix(name, refPrefix) }
|
func isRef(name string) bool { return strings.HasPrefix(name, refPrefix) }
|
||||||
|
|
||||||
// linkRefs resolves every {..path} reference in the assembled tree, binding the
|
// linkRefs resolves every {..path} reference in the assembled tree. The head of the
|
||||||
// target node into the referring template's fields under the token's key so the
|
// path — up to the category it names — is bound into the referring template's
|
||||||
// ordinary resolver renders it like a sibling. It runs once, after all data is
|
// fields, and the rest reads into it the way a sibling path does, so a reference
|
||||||
// merged, so a reference sees the final (override-resolved) tree. A path that is
|
// is held like a sibling. It runs once, after all data is merged, so a reference
|
||||||
// unknown, names a folder, or steps through a multi-variant choice fails here,
|
// sees the final (override-resolved) tree. A path that is unknown, names a folder,
|
||||||
// keeping a bad reference a New-time error, never a random render-time one.
|
// or reads a field not every variant carries fails here, keeping a bad reference a
|
||||||
|
// New-time error, never a random render-time one.
|
||||||
func linkRefs(root map[string]node) error {
|
func linkRefs(root map[string]node) error {
|
||||||
return walkNodes(root, func(path string, n node) error {
|
return walkNodes(root, func(path string, n node) error {
|
||||||
t, ok := n.(*template)
|
t, ok := n.(*template)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, name := range refTokens(t.format) {
|
names := refTokens(t.format)
|
||||||
target, err := lookup(root, strings.Split(name[len(refPrefix):], "."))
|
if len(names) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if t.fields == nil {
|
||||||
|
t.fields = map[string]node{}
|
||||||
|
}
|
||||||
|
t.refs = make(map[string]string, len(names))
|
||||||
|
for _, name := range names {
|
||||||
|
head, target, tail, err := resolveRef(root, strings.Split(name[len(refPrefix):], "."))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
||||||
}
|
}
|
||||||
t.fields[name] = target
|
key := refPrefix + strings.Join(head, ".")
|
||||||
|
if err := checkPath(target, tail, key); err != nil {
|
||||||
|
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
||||||
|
}
|
||||||
|
t.fields[key] = target
|
||||||
|
t.refs[name] = key
|
||||||
|
}
|
||||||
|
t.compileFormat()
|
||||||
|
if err := checkNoOverlap(t.format, t.bound, t.refs); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", path, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -41,7 +57,7 @@ func linkRefs(root map[string]node) error {
|
|||||||
// checkBoundLevelsHeld rejects every route to a held name except the ones that read
|
// checkBoundLevelsHeld rejects every route to a held name except the ones that read
|
||||||
// its draw. An expansion holds one draw of that name; anything else that renders it
|
// its draw. An expansion holds one draw of that name; anything else that renders it
|
||||||
// draws again, and the two disagree. checkNoOverlap settles the spellings within one
|
// draws again, and the two disagree. checkNoOverlap settles the spellings within one
|
||||||
// format (a token, a calc operand); this settles the rest — a reference, whether it
|
// format (a token, an operand); this settles the rest — a reference, whether it
|
||||||
// sits in that format or in anything the format renders, however deep.
|
// sits in that format or in anything the format renders, however deep.
|
||||||
//
|
//
|
||||||
// It runs after checkNoCycles, whose guarantee is what lets the walk terminate.
|
// It runs after checkNoCycles, whose guarantee is what lets the walk terminate.
|
||||||
@@ -55,33 +71,48 @@ func checkBoundLevelsHeld(root map[string]node) error {
|
|||||||
for head := range t.held {
|
for head := range t.held {
|
||||||
heads = append(heads, head)
|
heads = append(heads, head)
|
||||||
}
|
}
|
||||||
sort.Strings(heads) // so which overlap is reported does not vary
|
// Operand heads first, then paths, each in name order: a level read both
|
||||||
|
// ways is reported by the operand's fence, and which overlap is reported
|
||||||
|
// does not vary.
|
||||||
|
sort.Slice(heads, func(i, j int) bool {
|
||||||
|
_, pi := t.bound[heads[i]]
|
||||||
|
_, pj := t.bound[heads[j]]
|
||||||
|
if pi != pj {
|
||||||
|
return !pi
|
||||||
|
}
|
||||||
|
return heads[i] < heads[j]
|
||||||
|
})
|
||||||
|
readers := boundReaders(t.format, t.bound, t.refs)
|
||||||
for _, head := range heads {
|
for _, head := range heads {
|
||||||
// What one draw answers for depends on how the draw is read. A path may
|
// What one draw answers for depends on how the draw is read: a path pins
|
||||||
// read into anything the level contains; a calc renders its operand, so
|
// the levels it passes through and the leaf it lands on, an operand
|
||||||
// that draw fixes exactly the value the render produces.
|
// exactly the value its render produces.
|
||||||
held := map[node]bool{}
|
held := map[node]bool{}
|
||||||
reader, isPath := t.bound[head]
|
reader, isPath := t.bound[head]
|
||||||
if isPath {
|
if isPath {
|
||||||
cover(t.fields[head], held)
|
for _, r := range readers {
|
||||||
|
if a := splitArm(r.name, t.refs); a.key == head {
|
||||||
|
coverPath(t.fields[head], a.tail, held)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
operandDraw(t.fields[head], held)
|
operandDraw(t.fields[head], held)
|
||||||
}
|
}
|
||||||
if len(held) == 0 {
|
if len(held) == 0 {
|
||||||
continue // an early out: a literal head holds nothing to reach
|
continue // an early out: a fixed head holds nothing to reach
|
||||||
}
|
}
|
||||||
// One seen set across the edges: a node that cannot reach the level
|
// One seen set across the edges: a node that cannot reach the level
|
||||||
// cannot reach it by another route either, so it is walked once here.
|
// cannot reach it by another route either, so it is walked once here.
|
||||||
seen := map[node]bool{}
|
seen := map[node]bool{}
|
||||||
for _, e := range renderEdges(t) {
|
for _, e := range renderEdges(t) {
|
||||||
if splitArm(e.label).key == head {
|
if splitArm(e.label, t.refs).key == head {
|
||||||
continue // a token or operand reading this draw, the routes allowed
|
continue // a token or operand reading this draw, the routes allowed
|
||||||
}
|
}
|
||||||
if renders(e.to, held, seen) {
|
if renders(e.to, held, seen) {
|
||||||
if isPath {
|
if isPath {
|
||||||
return fmt.Errorf("%s: %s renders %q, which {%s} reads a path into; name the fields you want instead", path, e.reached(), head, reader)
|
return fmt.Errorf("%s: %s renders %q, which {%s} reads a path into; name the fields you want instead", path, e.reached(), head, reader)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("%s: %s renders %q, which a {calc()} also reads; reach it one way so it is drawn once", path, e.reached(), head)
|
return fmt.Errorf("%s: %s renders %q, which a {%s()} also reads; reach it one way so it is drawn once", path, e.reached(), head, operandReader(t, head))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,32 +120,65 @@ func checkBoundLevelsHeld(root map[string]node) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// cover collects what one held draw of a level answers for: the level and
|
// operandReader names the builtin whose operand holds head.
|
||||||
// everything contained in it, since a path may read any of it. Literals are left
|
func operandReader(t *template, head string) string {
|
||||||
// out — one fixed string cannot disagree with itself, and a literal is a value, so
|
fn := ""
|
||||||
// two that spell the same text are indistinguishable.
|
_ = eachToken(t.format, func(tok ftoken) error {
|
||||||
func cover(n node, into map[node]bool) {
|
if tok.kind != 'b' || fn != "" {
|
||||||
if _, fixed := n.(literal); fixed {
|
return nil
|
||||||
|
}
|
||||||
|
if name, _, isFunc := funcCall(tok.body); isFunc {
|
||||||
|
for _, operand := range tokenOperands(tok.body) {
|
||||||
|
if splitArm(operand, t.refs).key == head {
|
||||||
|
fn = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return fn
|
||||||
|
}
|
||||||
|
|
||||||
|
// coverPath collects what holding one path pins: every choice level the path
|
||||||
|
// passes through, whole, and the leaf it renders.
|
||||||
|
func coverPath(n node, tail []string, into map[node]bool) {
|
||||||
|
if _, isChoice := n.(*choice); isChoice || len(tail) == 0 {
|
||||||
|
cover(n, into, false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
into[n] = true
|
t, ok := n.(*template)
|
||||||
for _, c := range contained(n) {
|
if !ok {
|
||||||
cover(c.node, into)
|
return
|
||||||
|
}
|
||||||
|
if child, ok := t.fields[tail[0]]; ok {
|
||||||
|
coverPath(child, tail[1:], into)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// operandDraw collects what one held draw of a {calc()} operand answers for: the
|
// cover collects a level and everything contained in it. A fixed string outside a
|
||||||
// operand and what rendering it settles inside itself. A calc renders its operand
|
// choice is left out — it cannot disagree with itself — but inside one each
|
||||||
|
// variant carries its own, so there it counts.
|
||||||
|
func cover(n node, into map[node]bool, inChoice bool) {
|
||||||
|
if isFixed(n) && !inChoice {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
into[n] = true
|
||||||
|
_, isChoice := n.(*choice)
|
||||||
|
for _, c := range contained(n) {
|
||||||
|
cover(c.node, into, inChoice || isChoice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// operandDraw collects what one held draw of an operand answers for: the operand
|
||||||
|
// and what rendering it settles inside itself. The builtin renders its operand
|
||||||
// whole, so that draw fixes every value the render produced, and a second route to
|
// whole, so that draw fixes every value the render produced, and a second route to
|
||||||
// any of them disagrees with it.
|
// any of them disagrees with it.
|
||||||
//
|
//
|
||||||
// The walk stops at a {..path} edge, which is where the operand's own value ends
|
// The walk stops at a {..path} edge, which is where the operand's own value ends
|
||||||
// and a shared source begins: two names referencing one category are two draws, the
|
// and a shared source begins: two names referencing one category are two draws, the
|
||||||
// same rule {word} {word} follows. cover stops there too, by way of named, so both
|
// same rule {word} {word} follows.
|
||||||
// halves of the fence end at the same boundary. A literal is left out for the
|
|
||||||
// reason cover leaves one out.
|
|
||||||
func operandDraw(n node, into map[node]bool) {
|
func operandDraw(n node, into map[node]bool) {
|
||||||
if _, fixed := n.(literal); fixed {
|
if isFixed(n) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if into[n] {
|
if into[n] {
|
||||||
@@ -129,6 +193,12 @@ func operandDraw(n node, into map[node]bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isFixed is a string that varies nothing: fixed text with no fields to read into.
|
||||||
|
func isFixed(n node) bool {
|
||||||
|
t, ok := n.(*template)
|
||||||
|
return ok && t.fixed && len(t.fields) == 0
|
||||||
|
}
|
||||||
|
|
||||||
// renders reports whether rendering n can reach anything in want, following the
|
// renders reports whether rendering n can reach anything in want, following the
|
||||||
// same edges expand does. seen keeps a node shared by several routes from being
|
// same edges expand does. seen keeps a node shared by several routes from being
|
||||||
// walked twice; checkNoCycles has already proved the graph is a DAG, so the walk
|
// walked twice; checkNoCycles has already proved the graph is a DAG, so the walk
|
||||||
@@ -226,47 +296,35 @@ func sortedNames(m map[string]node) []string {
|
|||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookup finds the single node a reference path names, walking groups and
|
// resolveRef walks a reference path through the folders to the category it names,
|
||||||
// template fields by segment and descending a single-variant choice as a
|
// returning that head, the node, and the tail left to read into it.
|
||||||
// transparent wrapper. A missing segment, a folder target, or a step through a
|
func resolveRef(root map[string]node, segments []string) (head []string, target node, tail []string, err error) {
|
||||||
// multi-variant choice (which has no one value to bind) is an error.
|
|
||||||
func lookup(root map[string]node, segments []string) (node, error) {
|
|
||||||
var n node = &group{children: root}
|
var n node = &group{children: root}
|
||||||
for i := 0; i < len(segments); i++ {
|
i := 0
|
||||||
switch c := n.(type) {
|
for ; i < len(segments); i++ {
|
||||||
case *group:
|
g, ok := n.(*group)
|
||||||
child, ok := c.children[segments[i]]
|
if !ok {
|
||||||
if !ok {
|
break
|
||||||
return nil, fmt.Errorf("no entry %q", segments[i])
|
|
||||||
}
|
|
||||||
n = child
|
|
||||||
case *template:
|
|
||||||
child, ok := c.fields[segments[i]]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("no field %q", segments[i])
|
|
||||||
}
|
|
||||||
n = child
|
|
||||||
case *choice:
|
|
||||||
if len(c.items) != 1 {
|
|
||||||
return nil, fmt.Errorf("%q steps through a %d-way choice", segments[i], len(c.items))
|
|
||||||
}
|
|
||||||
n, i = c.items[0], i-1 // a choice consumes no segment; reprocess it unwrapped
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("cannot descend into %T at %q", n, segments[i])
|
|
||||||
}
|
}
|
||||||
|
child, ok := g.children[segments[i]]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, nil, fmt.Errorf("no entry %q", segments[i])
|
||||||
|
}
|
||||||
|
n = child
|
||||||
}
|
}
|
||||||
if _, ok := n.(*group); ok {
|
if _, ok := n.(*group); ok {
|
||||||
return nil, fmt.Errorf("names a folder, not a value")
|
return nil, nil, nil, fmt.Errorf("names a folder, not a value")
|
||||||
}
|
}
|
||||||
return n, nil
|
return segments[:i], n, segments[i:], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// refTokens returns just the {..path} reference names among a format's field
|
// refTokens returns the {..path} names a format reads, as tokens or as operands.
|
||||||
// tokens (linkRefs binds each into the template's fields).
|
|
||||||
func refTokens(format string) []string {
|
func refTokens(format string) []string {
|
||||||
var refs []string
|
var refs []string
|
||||||
for _, name := range fieldTokens(format) {
|
seen := map[string]bool{}
|
||||||
if isRef(name) {
|
for _, name := range append(fieldTokens(format), operandTokens(format)...) {
|
||||||
|
if isRef(name) && !seen[name] {
|
||||||
|
seen[name] = true
|
||||||
refs = append(refs, name)
|
refs = append(refs, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,27 +332,27 @@ func refTokens(format string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// renderEdge is a child a node renders into, labelled by what reaches it (a field
|
// renderEdge is a child a node renders into, labelled by what reaches it (a field
|
||||||
// name, reference, or choice index) for a readable cycle report. operand marks a
|
// name, reference, or choice index) for a readable cycle report. operand names
|
||||||
// label that is a {calc()} operand name rather than a token, so an error can name
|
// the builtin when the label is its operand rather than a token, so an error can
|
||||||
// it the way the author wrote it.
|
// name it the way the author wrote it.
|
||||||
type renderEdge struct {
|
type renderEdge struct {
|
||||||
to node
|
to node
|
||||||
label string
|
label string
|
||||||
operand bool
|
operand string
|
||||||
}
|
}
|
||||||
|
|
||||||
// reached names an edge as the author spelled it, the vocabulary boundReaders uses
|
// reached names an edge as the author spelled it, the vocabulary boundReaders uses
|
||||||
// for the sibling fence.
|
// for the sibling fence.
|
||||||
func (e renderEdge) reached() string {
|
func (e renderEdge) reached() string {
|
||||||
if e.operand {
|
if e.operand != "" {
|
||||||
return fmt.Sprintf("calc operand %q", e.label)
|
return fmt.Sprintf("%s operand %q", e.operand, e.label)
|
||||||
}
|
}
|
||||||
return "{" + e.label + "}"
|
return "{" + e.label + "}"
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderEdges lists the children rendering n recurses into, mirroring expand: a
|
// renderEdges lists the children rendering n recurses into, mirroring expand: a
|
||||||
// choice's items, and a template's field/reference tokens plus its calc operands.
|
// choice's items, and a template's field/reference tokens plus its operands. A
|
||||||
// A literal or group renders nothing, so it has no edges.
|
// group renders nothing, so it has no edges.
|
||||||
func renderEdges(n node) []renderEdge {
|
func renderEdges(n node) []renderEdge {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case *choice:
|
case *choice:
|
||||||
@@ -305,8 +363,8 @@ func renderEdges(n node) []renderEdge {
|
|||||||
return es
|
return es
|
||||||
case *template:
|
case *template:
|
||||||
var es []renderEdge
|
var es []renderEdge
|
||||||
add := func(name string, operand bool) {
|
add := func(name, operand string) {
|
||||||
a := splitArm(name)
|
a := splitArm(name, n.refs)
|
||||||
c, ok := n.fields[a.key]
|
c, ok := n.fields[a.key]
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@@ -315,12 +373,21 @@ func renderEdges(n node) []renderEdge {
|
|||||||
es = append(es, renderEdge{leaf, name, operand})
|
es = append(es, renderEdge{leaf, name, operand})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, name := range fieldTokens(n.format) {
|
_ = eachToken(n.format, func(t ftoken) error {
|
||||||
add(name, false)
|
if t.kind != 'b' {
|
||||||
}
|
return nil
|
||||||
for _, name := range calcOperands(n.format) {
|
}
|
||||||
add(name, true)
|
if fn, _, isFunc := funcCall(t.body); isFunc {
|
||||||
}
|
for _, operand := range tokenOperands(t.body) {
|
||||||
|
add(operand, fn)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, name := range strings.Split(t.body, "|") {
|
||||||
|
add(name, "")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
return es
|
return es
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+98
-41
@@ -1,13 +1,16 @@
|
|||||||
package fejkdata
|
package fejkdata
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
// TestRootReferenceAcrossFolders is the headline case: a category in one folder
|
// TestRootReferenceAcrossFolders is the headline case: a category in one folder
|
||||||
// pulls a value from another via a {..path} reference resolved from the data root.
|
// pulls a value from another via a {..path} reference resolved from the data root.
|
||||||
func TestRootReferenceAcrossFolders(t *testing.T) {
|
func TestRootReferenceAcrossFolders(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"en_US/person": `["Pat Smith"]`,
|
"en_US/person": `"Pat Smith"`,
|
||||||
"sv_SE/greeting": `{"format":"Hej, {..en_US.person}!"}`,
|
"sv_SE/greeting": `"Hej, {..en_US.person}!"`,
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "sv_SE.greeting"); got != "Hej, Pat Smith!" {
|
if got := fake(t, f, "sv_SE.greeting"); got != "Hej, Pat Smith!" {
|
||||||
@@ -15,12 +18,11 @@ func TestRootReferenceAcrossFolders(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestReferenceIntoAField reaches a field inside a referenced category, crossing
|
// TestReferenceIntoAField reaches a field inside a referenced category.
|
||||||
// a single-variant choice and then a template field (..who.last).
|
|
||||||
func TestReferenceIntoAField(t *testing.T) {
|
func TestReferenceIntoAField(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"who": `[{"format":"{first} {last}","first":["Ada"],"last":["Byron"]}]`,
|
"who": `{"format":"{first} {last}","first":"Ada","last":"Byron"}`,
|
||||||
"card": `{"format":"signed {..who.last}"}`,
|
"card": `"signed {..who.last}"`,
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "card"); got != "signed Byron" {
|
if got := fake(t, f, "card"); got != "signed Byron" {
|
||||||
@@ -32,8 +34,8 @@ func TestReferenceIntoAField(t *testing.T) {
|
|||||||
// alternation, so a field and a cross-file value share one slot.
|
// alternation, so a field and a cross-file value share one slot.
|
||||||
func TestReferenceInAlternation(t *testing.T) {
|
func TestReferenceInAlternation(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"far": `["X"]`,
|
"far": `"X"`,
|
||||||
"near": `{"format":"{here|..far}","here":["H"]}`,
|
"near": `{"format":"{here|..far}","here":"H"}`,
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(2))
|
f := newGenerator(t, dir, WithSeed(2))
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
@@ -48,8 +50,8 @@ func TestReferenceInAlternation(t *testing.T) {
|
|||||||
// TestReferenceCombinesLoadedPaths is the point of references over the merge
|
// TestReferenceCombinesLoadedPaths is the point of references over the merge
|
||||||
// model: data layered from two dirs can point at each other through the root.
|
// model: data layered from two dirs can point at each other through the root.
|
||||||
func TestReferenceCombinesLoadedPaths(t *testing.T) {
|
func TestReferenceCombinesLoadedPaths(t *testing.T) {
|
||||||
a := writeData(t, map[string]string{"en_US/word": `["river"]`})
|
a := writeData(t, map[string]string{"en_US/word": `"river"`})
|
||||||
b := writeData(t, map[string]string{"mine/slug": `{"format":"the-{..en_US.word}"}`})
|
b := writeData(t, map[string]string{"mine/slug": `"the-{..en_US.word}"`})
|
||||||
f := newGeneratorN(t, []string{a, b}, WithSeed(1))
|
f := newGeneratorN(t, []string{a, b}, WithSeed(1))
|
||||||
if got := fake(t, f, "mine.slug"); got != "the-river" {
|
if got := fake(t, f, "mine.slug"); got != "the-river" {
|
||||||
t.Fatalf("slug = %q, want the-river", got)
|
t.Fatalf("slug = %q, want the-river", got)
|
||||||
@@ -60,9 +62,9 @@ func TestReferenceCombinesLoadedPaths(t *testing.T) {
|
|||||||
// linking order cannot matter.
|
// linking order cannot matter.
|
||||||
func TestReferenceChain(t *testing.T) {
|
func TestReferenceChain(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"a": `{"format":"{..b}"}`,
|
"a": `"{..b}"`,
|
||||||
"b": `{"format":"{..c}"}`,
|
"b": `"{..c}"`,
|
||||||
"c": `["deep"]`,
|
"c": `"deep"`,
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "a"); got != "deep" {
|
if got := fake(t, f, "a"); got != "deep" {
|
||||||
@@ -74,37 +76,37 @@ func TestReferenceChain(t *testing.T) {
|
|||||||
// fails at load, never at a random render.
|
// fails at load, never at a random render.
|
||||||
func TestReferenceErrors(t *testing.T) {
|
func TestReferenceErrors(t *testing.T) {
|
||||||
cases := map[string]map[string]string{
|
cases := map[string]map[string]string{
|
||||||
"missing target": {"card": `{"format":"{..nope.gone}"}`},
|
"missing target": {"card": `"{..nope.gone}"`},
|
||||||
"folder target": {"en_US/word": `["w"]`, "card": `{"format":"{..en_US}"}`},
|
"folder target": {"en_US/word": `"w"`, "card": `"{..en_US}"`},
|
||||||
"multi-variant on the path": {
|
"a variant on the path lacks the field": {
|
||||||
"who": `[{"format":"{f}","f":["1"]},{"format":"{f}","f":["2"]}]`,
|
"who": `[{"format":"{f}","f":"1"},{"format":"{g}","g":"2"}]`,
|
||||||
"card": `{"format":"{..who.f}"}`,
|
"card": `"{..who.f}"`,
|
||||||
},
|
},
|
||||||
"empty reference path": {"card": `{"format":"{..}"}`},
|
"empty reference path": {"card": `"{..}"`},
|
||||||
// A reference that leads back to its own value never terminates at render,
|
// A reference that leads back to its own value never terminates at render,
|
||||||
// so New must reject the cycle up front (direct, mutual, or chained).
|
// so New must reject the cycle up front (direct, mutual, or chained).
|
||||||
"direct cycle": {"a": `{"format":"x{..a}"}`},
|
"direct cycle": {"a": `"x{..a}"`},
|
||||||
"mutual cycle": {"a": `{"format":"{..b}"}`, "b": `{"format":"{..a}"}`},
|
"mutual cycle": {"a": `"{..b}"`, "b": `"{..a}"`},
|
||||||
"chain cycle": {"a": `{"format":"{..b}"}`, "b": `{"format":"{..c}"}`, "c": `{"format":"{..a}"}`},
|
"chain cycle": {"a": `"{..b}"`, "b": `"{..c}"`, "c": `"{..a}"`},
|
||||||
// calc renders its operands, so a cycle through one must be caught too.
|
// calc renders its operands, so a cycle through one must be caught too.
|
||||||
"calc operand cycle": {"x": `{"format":"{calc(y)}","y":[{"format":"{..x}"}]}`},
|
"calc operand cycle": {"x": `{"format":"{calc(y)}","y":"{..x}"}`},
|
||||||
// A field its parent's format never renders is still reachable by dot path,
|
// A field its parent's format never renders is still reachable by dot path,
|
||||||
// so a cycle hiding in one must fail at New rather than at render.
|
// so a cycle hiding in one must fail at New rather than at render.
|
||||||
"cycle in an unrendered field": {"cat": `{"format":"hi","x":{"format":"{..cat.x}"}}`},
|
"cycle in an unrendered field": {"cat": `{"format":"hi","x":"{..cat.x}"}`},
|
||||||
"mutual cycle between unrendered fields": {
|
"mutual cycle between unrendered fields": {
|
||||||
"cat": `{"format":"hi","x":{"format":"{..cat.y}"},"y":{"format":"{..cat.x}"}}`,
|
"cat": `{"format":"hi","x":"{..cat.y}","y":"{..cat.x}"}`,
|
||||||
},
|
},
|
||||||
"cycle in an unrendered field of a choice arm": {
|
"cycle in an unrendered field of a choice arm": {
|
||||||
"cat": `[{"format":"hi","x":{"format":"{..cat.x}"}}]`,
|
"cat": `{"format":"hi","x":"{..cat.x}"}`,
|
||||||
},
|
},
|
||||||
// The shipped layout puts categories in folders, so a cycle one level down
|
// The shipped layout puts categories in folders, so a cycle one level down
|
||||||
// is the common case, not an edge case.
|
// is the common case, not an edge case.
|
||||||
"cycle in a subfolder": {"sv_SE/a": `{"format":"x{..sv_SE.a}"}`},
|
"cycle in a subfolder": {"sv_SE/a": `"x{..sv_SE.a}"`},
|
||||||
"mutual cycle within a subfolder": {"sv_SE/a": `{"format":"{..sv_SE.b}"}`, "sv_SE/b": `{"format":"{..sv_SE.a}"}`},
|
"mutual cycle within a subfolder": {"sv_SE/a": `"{..sv_SE.b}"`, "sv_SE/b": `"{..sv_SE.a}"`},
|
||||||
"mutual cycle across two folders": {"en_US/a": `{"format":"{..sv_SE.b}"}`, "sv_SE/b": `{"format":"{..en_US.a}"}`},
|
"mutual cycle across two folders": {"en_US/a": `"{..sv_SE.b}"`, "sv_SE/b": `"{..en_US.a}"`},
|
||||||
// ".." is reserved for bound references, so an authored key using it would
|
// ".." is reserved for bound references, so an authored key using it would
|
||||||
// name a node nothing can reach and nothing would validate.
|
// name a node nothing can reach and nothing would validate.
|
||||||
"field key using the reference prefix": {"cat": `{"format":"hi","..x":{"format":"{..nope}"}}`},
|
"field key using the reference prefix": {"cat": `{"format":"hi","..x":"{..nope}"}`},
|
||||||
}
|
}
|
||||||
for name, files := range cases {
|
for name, files := range cases {
|
||||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err == nil {
|
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err == nil {
|
||||||
@@ -119,10 +121,10 @@ func TestReferenceErrors(t *testing.T) {
|
|||||||
// starts with "." — it is skipped, not rejected.
|
// starts with "." — it is skipped, not rejected.
|
||||||
func TestDotPrefixedDataEntriesAreSkipped(t *testing.T) {
|
func TestDotPrefixedDataEntriesAreSkipped(t *testing.T) {
|
||||||
f := newGenerator(t, writeData(t, map[string]string{
|
f := newGenerator(t, writeData(t, map[string]string{
|
||||||
"sv_SE/ok": `["fine"]`,
|
"sv_SE/ok": `"fine"`,
|
||||||
"sv_SE/..bad": `{"format":"{..nope}"}`,
|
"sv_SE/..bad": `"{..nope}"`,
|
||||||
"sv_SE/..y/ct": `{"format":"{..nope}"}`,
|
"sv_SE/..y/ct": `"{..nope}"`,
|
||||||
".git/config": `["not data"]`,
|
".git/config": `"not data"`,
|
||||||
}), WithSeed(1))
|
}), WithSeed(1))
|
||||||
if got := f.List(); len(got) != 1 || got[0] != "sv_SE.ok" {
|
if got := f.List(); len(got) != 1 || got[0] != "sv_SE.ok" {
|
||||||
t.Fatalf("List() = %v, want only sv_SE.ok", got)
|
t.Fatalf("List() = %v, want only sv_SE.ok", got)
|
||||||
@@ -133,7 +135,7 @@ func TestDotPrefixedDataEntriesAreSkipped(t *testing.T) {
|
|||||||
// over-rejecting: a field the format never renders may point back at its own
|
// over-rejecting: a field the format never renders may point back at its own
|
||||||
// category, which terminates, and stays renderable by path.
|
// category, which terminates, and stays renderable by path.
|
||||||
func TestReferenceFromUnrenderedFieldTerminates(t *testing.T) {
|
func TestReferenceFromUnrenderedFieldTerminates(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{"cat": `{"format":"hi","x":{"format":"see {..cat}"}}`})
|
dir := writeData(t, map[string]string{"cat": `{"format":"hi","x":"see {..cat}"}`})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "cat"); got != "hi" {
|
if got := fake(t, f, "cat"); got != "hi" {
|
||||||
t.Fatalf("cat = %q, want hi", got)
|
t.Fatalf("cat = %q, want hi", got)
|
||||||
@@ -149,9 +151,9 @@ func TestReferenceFromUnrenderedFieldTerminates(t *testing.T) {
|
|||||||
func TestNewErrorIsDeterministic(t *testing.T) {
|
func TestNewErrorIsDeterministic(t *testing.T) {
|
||||||
cases := map[string]map[string]string{
|
cases := map[string]map[string]string{
|
||||||
"three bad references": {
|
"three bad references": {
|
||||||
"a": `{"format":"{..nope.one}"}`,
|
"a": `"{..nope.one}"`,
|
||||||
"b": `{"format":"{..nope.two}"}`,
|
"b": `"{..nope.two}"`,
|
||||||
"c": `{"format":"{..nope.three}"}`,
|
"c": `"{..nope.three}"`,
|
||||||
},
|
},
|
||||||
"two bad fields in one template": {
|
"two bad fields in one template": {
|
||||||
"cat": `{"format":"hi","aaa":{"no":1},"zzz":{"no":2}}`,
|
"cat": `{"format":"hi","aaa":{"no":1},"zzz":{"no":2}}`,
|
||||||
@@ -188,12 +190,12 @@ func TestNewErrorPathIsCanonical(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"cycle inside a choice arm",
|
"cycle inside a choice arm",
|
||||||
map[string]string{"cat": `[{"format":"hi","x":{"format":"{..cat.x}"}}]`},
|
map[string]string{"cat": `{"format":"hi","x":"{..cat.x}"}`},
|
||||||
"fejkdata: reference cycle: cat.x -> ..cat.x",
|
"fejkdata: reference cycle: cat.x -> ..cat.x",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bad reference reached through another reference",
|
"bad reference reached through another reference",
|
||||||
map[string]string{"a": `{"format":"{..b}"}`, "b": `{"format":"{..nope}"}`},
|
map[string]string{"a": `"{..b}"`, "b": `"{..nope}"`},
|
||||||
`fejkdata: b: reference {..nope}: no entry "nope"`,
|
`fejkdata: b: reference {..nope}: no entry "nope"`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -208,3 +210,58 @@ func TestNewErrorPathIsCanonical(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReferencePathIsHeld(t *testing.T) {
|
||||||
|
dir := writeData(t, map[string]string{
|
||||||
|
"person": `[{"format":"{first} {last}","first":"Anna","last":"Andersson"},{"format":"{first} {last}","first":"Bo","last":"Berg"}]`,
|
||||||
|
"card": `"{..person.first} {..person.last}"`,
|
||||||
|
})
|
||||||
|
f := newGenerator(t, dir, WithSeed(3))
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
got := fake(t, f, "card")
|
||||||
|
if got != "Anna Andersson" && got != "Bo Berg" {
|
||||||
|
t.Fatalf("card = %q, want one person's first and last name", got)
|
||||||
|
}
|
||||||
|
seen[got] = true
|
||||||
|
}
|
||||||
|
if len(seen) != 2 {
|
||||||
|
t.Fatalf("card only ever rendered %v", seen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReferenceThroughChoiceNeedsEveryVariant(t *testing.T) {
|
||||||
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
|
"who": `[{"format":"{f}{h}","f":"1","h":"x"},{"format":"{g}{h}","g":"2","h":"y"}]`,
|
||||||
|
"card": `"{..who.f}"`,
|
||||||
|
})))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "not every variant") {
|
||||||
|
t.Fatalf("New = %v, want the missing variant named", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBareReferenceDrawsEachTime(t *testing.T) {
|
||||||
|
dir := writeData(t, map[string]string{
|
||||||
|
"die": `["1","2","3","4","5","6"]`,
|
||||||
|
"roll": `"{..die} {..die}"`,
|
||||||
|
})
|
||||||
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
if got := fake(t, f, "roll"); got[0] != got[2] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatal("two bare references always agreed; each should be its own draw")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReferenceOverlapIsRejected(t *testing.T) {
|
||||||
|
for name, file := range map[string]string{
|
||||||
|
"head beside a path": `{"format":"{..cat.p} {..cat.p.first}","p":[{"format":"{first}","first":"A"},{"format":"{first}","first":"B"}]}`,
|
||||||
|
"sibling path beside a reference path": `{"format":"{p.first} {..cat.p.last}","p":[{"format":"{first}","first":"A","last":"1"},{"format":"{first}","first":"B","last":"2"}]}`,
|
||||||
|
} {
|
||||||
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file})))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "reads a path into") {
|
||||||
|
t.Errorf("%s: New = %v, want the overlap rejected", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,14 +53,10 @@ func descend(s *session, n node, segments []string) (node, error) {
|
|||||||
}
|
}
|
||||||
return descend(s, child, segments[1:])
|
return descend(s, child, segments[1:])
|
||||||
case *choice:
|
case *choice:
|
||||||
if len(n.items) > 1 {
|
if want := strings.Join(segments, "."); !n.shared[want] {
|
||||||
if want := strings.Join(segments, "."); !n.shared[want] {
|
return nil, unreachableInChoice(n, want)
|
||||||
return nil, unreachableInChoice(n, want)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return descend(s, pick(s, n), segments)
|
return descend(s, pick(s, n), segments)
|
||||||
case literal:
|
|
||||||
return nil, fmt.Errorf("a plain string has no field %q", segments[0])
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("cannot descend into %T at %q", n, segments[0])
|
return nil, fmt.Errorf("cannot descend into %T at %q", n, segments[0])
|
||||||
}
|
}
|
||||||
@@ -85,12 +81,13 @@ func unreachableInChoice(c *choice, want string) error {
|
|||||||
// front, so rendering a compiled tree cannot fail.
|
// front, so rendering a compiled tree cannot fail.
|
||||||
func render(s *session, n node) string {
|
func render(s *session, n node) string {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case literal:
|
|
||||||
return string(n)
|
|
||||||
case *choice:
|
case *choice:
|
||||||
return render(s, pick(s, n))
|
return render(s, pick(s, n))
|
||||||
case *template:
|
case *template:
|
||||||
if n.repeat == 1 {
|
if n.repeat == 1 {
|
||||||
|
if n.fixed {
|
||||||
|
return n.lit
|
||||||
|
}
|
||||||
return expand(s, n)
|
return expand(s, n)
|
||||||
}
|
}
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
@@ -119,21 +116,6 @@ func pick(r rng, c *choice) node {
|
|||||||
return c.items[i]
|
return c.items[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
// classChar randomises one character-class rune: '0' digit 0-9, '1' digit 1-9,
|
|
||||||
// 'A' letter A-Z, 'a' letter a-z.
|
|
||||||
func classChar(s *session, c rune) byte {
|
|
||||||
switch c {
|
|
||||||
case '0':
|
|
||||||
return byte('0' + s.IntN(10))
|
|
||||||
case '1':
|
|
||||||
return byte('1' + s.IntN(9))
|
|
||||||
case 'A':
|
|
||||||
return byte('A' + s.IntN(26))
|
|
||||||
default: // 'a'
|
|
||||||
return byte('a' + s.IntN(26))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// expand renders a template's compiled ops. compile validated every token, so this
|
// expand renders a template's compiled ops. compile validated every token, so this
|
||||||
// cannot fail.
|
// cannot fail.
|
||||||
func expand(s *session, t *template) string {
|
func expand(s *session, t *template) string {
|
||||||
@@ -153,8 +135,6 @@ func expand(s *session, t *template) string {
|
|||||||
switch o.kind {
|
switch o.kind {
|
||||||
case 'l':
|
case 'l':
|
||||||
b.WriteString(o.lit)
|
b.WriteString(o.lit)
|
||||||
case 'c':
|
|
||||||
b.WriteByte(classChar(s, o.r))
|
|
||||||
case 'f':
|
case 'f':
|
||||||
b.WriteString(readField(s, t, held, o.arms[s.IntN(len(o.arms))]))
|
b.WriteString(readField(s, t, held, o.arms[s.IntN(len(o.arms))]))
|
||||||
case 'b':
|
case 'b':
|
||||||
@@ -163,8 +143,8 @@ func expand(s *session, t *template) string {
|
|||||||
var operands []string
|
var operands []string
|
||||||
if len(o.operands) > 0 {
|
if len(o.operands) > 0 {
|
||||||
operands = make([]string, len(o.operands))
|
operands = make([]string, len(o.operands))
|
||||||
for j, name := range o.operands {
|
for j, a := range o.operands {
|
||||||
operands[j] = readField(s, t, held, arm{name: name, key: name})
|
operands[j] = readField(s, t, held, a)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b.WriteString(o.call(s, b.String(), operands)) // b.String() is the output so far
|
b.WriteString(o.call(s, b.String(), operands)) // b.String() is the output so far
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func TestNewDefaultsToShippedData(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWithDataPathLayersOverShipped(t *testing.T) {
|
func TestWithDataPathLayersOverShipped(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{"sv_SE/word": `["only-mine"]`, "greeting": `["hej"]`})
|
dir := writeData(t, map[string]string{"sv_SE/word": `"only-mine"`, "greeting": `"hej"`})
|
||||||
f, err := New(WithDataPath(dir), WithSeed(1))
|
f, err := New(WithDataPath(dir), WithSeed(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("New = %v", err)
|
t.Fatalf("New = %v", err)
|
||||||
@@ -43,7 +43,7 @@ func TestWithDataPathLayersOverShipped(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUserDataMayReferenceShipped(t *testing.T) {
|
func TestUserDataMayReferenceShipped(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{"greeting": `{"format":"Hej {..sv_SE.person}!"}`})
|
dir := writeData(t, map[string]string{"greeting": `"Hej {..sv_SE.person}!"`})
|
||||||
f, err := New(WithDataPath(dir), WithSeed(1))
|
f, err := New(WithDataPath(dir), WithSeed(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("New = %v", err)
|
t.Fatalf("New = %v", err)
|
||||||
@@ -61,7 +61,7 @@ func TestWithoutShippedDataNeedsASource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWithoutShippedDataListsOnlyOwn(t *testing.T) {
|
func TestWithoutShippedDataListsOnlyOwn(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{"greeting": `["hej"]`})
|
dir := writeData(t, map[string]string{"greeting": `"hej"`})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
if got := f.List(); !reflect.DeepEqual(got, []string{"greeting"}) {
|
if got := f.List(); !reflect.DeepEqual(got, []string{"greeting"}) {
|
||||||
t.Errorf("List() = %v, want only the loaded dir", got)
|
t.Errorf("List() = %v, want only the loaded dir", got)
|
||||||
@@ -70,10 +70,10 @@ func TestWithoutShippedDataListsOnlyOwn(t *testing.T) {
|
|||||||
|
|
||||||
func TestWithDataFS(t *testing.T) {
|
func TestWithDataFS(t *testing.T) {
|
||||||
fsys := fstest.MapFS{
|
fsys := fstest.MapFS{
|
||||||
"greeting.json": {Data: []byte(`["hej"]`)},
|
"greeting.json": {Data: []byte(`"hej"`)},
|
||||||
"nested/x.json": {Data: []byte(`{"format":"{y}","y":["z"]}`)},
|
"nested/x.json": {Data: []byte(`{"format":"{y}","y":"z"}`)},
|
||||||
".hidden.json": {Data: []byte(`["ignored"]`)},
|
".hidden.json": {Data: []byte(`"ignored"`)},
|
||||||
"broken/no.json": {Data: []byte(`["x"]`)},
|
"broken/no.json": {Data: []byte(`"x"`)},
|
||||||
}
|
}
|
||||||
f, err := New(WithoutShippedData(), WithDataFS(fsys), WithSeed(1))
|
f, err := New(WithoutShippedData(), WithDataFS(fsys), WithSeed(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+147
-109
@@ -6,41 +6,33 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This file is the {token} grammar of a format string: how it is scanned
|
// ftoken is one unit of a scanned format string: a literal rune or the body of a
|
||||||
// (eachToken), how a function token is parsed (funcCall) and validated
|
// {…} token.
|
||||||
// (checkFunc, checkTokens), and the builtin contract its functions implement.
|
|
||||||
// render.go evaluates these tokens; node.go compiles the surrounding JSON;
|
|
||||||
// reference.go binds {..path} tokens across the tree.
|
|
||||||
|
|
||||||
// ftoken is one unit of a scanned format string: a literal rune to emit, a class
|
|
||||||
// char to randomise ('0' '1' 'A' 'a'), or the body of a {…} token.
|
|
||||||
type ftoken struct {
|
type ftoken struct {
|
||||||
kind byte // 'l' literal rune, 'c' class char, 'b' brace body
|
kind byte // 'l' literal rune, 'b' brace body
|
||||||
r rune // for kinds 'l' and 'c'
|
r rune
|
||||||
body string
|
body string
|
||||||
}
|
}
|
||||||
|
|
||||||
// eachToken scans a format string once and calls fn for each unit, the single
|
// eachToken scans a format string once and calls fn for each unit, the single
|
||||||
// source of truth for how '#' escapes and {…} braces are read — compileOps,
|
// source of truth for how braces are read: "{{" and "}}" are literal braces, a "{"
|
||||||
// checkTokens, fieldTokens and refTokens all drive off it so the grammar can't
|
// opens a token that must reach its "}", and a lone "}" is an error.
|
||||||
// drift between the validator and the renderer. '#' escapes the next char to a
|
|
||||||
// literal ("#0" -> '0', "##" -> '#'); a '{' must reach a '}' (else an error); an
|
|
||||||
// unmatched '}' is an ordinary literal. The error stops the scan early.
|
|
||||||
func eachToken(format string, fn func(ftoken) error) error {
|
func eachToken(format string, fn func(ftoken) error) error {
|
||||||
rs := []rune(format)
|
rs := []rune(format)
|
||||||
for i := 0; i < len(rs); i++ {
|
for i := 0; i < len(rs); i++ {
|
||||||
var t ftoken
|
var t ftoken
|
||||||
switch c := rs[i]; c {
|
switch c := rs[i]; c {
|
||||||
case '#':
|
|
||||||
t.kind, t.r = 'l', '#'
|
|
||||||
if i++; i < len(rs) {
|
|
||||||
t.r = rs[i]
|
|
||||||
}
|
|
||||||
case '0', '1', 'A', 'a':
|
|
||||||
t.kind, t.r = 'c', c
|
|
||||||
case '{':
|
case '{':
|
||||||
|
if i+1 < len(rs) && rs[i+1] == '{' {
|
||||||
|
t.kind, t.r = 'l', '{'
|
||||||
|
i++
|
||||||
|
break
|
||||||
|
}
|
||||||
end := i + 1
|
end := i + 1
|
||||||
for end < len(rs) && rs[end] != '}' {
|
for end < len(rs) && rs[end] != '}' {
|
||||||
|
if rs[end] == '{' {
|
||||||
|
return fmt.Errorf("'{' inside a token in %q; a literal brace is written {{", format)
|
||||||
|
}
|
||||||
end++
|
end++
|
||||||
}
|
}
|
||||||
if end >= len(rs) {
|
if end >= len(rs) {
|
||||||
@@ -48,6 +40,13 @@ func eachToken(format string, fn func(ftoken) error) error {
|
|||||||
}
|
}
|
||||||
t.kind, t.body = 'b', string(rs[i+1:end])
|
t.kind, t.body = 'b', string(rs[i+1:end])
|
||||||
i = end
|
i = end
|
||||||
|
case '}':
|
||||||
|
if i+1 < len(rs) && rs[i+1] == '}' {
|
||||||
|
t.kind, t.r = 'l', '}'
|
||||||
|
i++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return fmt.Errorf("lone '}' in %q; a literal brace is written }}", format)
|
||||||
default:
|
default:
|
||||||
t.kind, t.r = 'l', c
|
t.kind, t.r = 'l', c
|
||||||
}
|
}
|
||||||
@@ -61,18 +60,18 @@ func eachToken(format string, fn func(ftoken) error) error {
|
|||||||
// builtin is a format-string function invoked as {name(args)}. It receives the
|
// builtin is a format-string function invoked as {name(args)}. It receives the
|
||||||
// session (its rng, and the {seq()} counters), the output emitted so far in the
|
// session (its rng, and the {seq()} counters), the output emitted so far in the
|
||||||
// current expansion (for derivations such as a checksum over preceding digits), and
|
// current expansion (for derivations such as a checksum over preceding digits), and
|
||||||
// the values of the operands it named (only calc names any).
|
// the values of the operands it named (only calc names any). All must stay pure
|
||||||
// Almost all are pure over (rng, emitted, args) — no wall-clock, no crypto/rand —
|
// over (rng, emitted, args) so seeded output is reproducible; seq advances
|
||||||
// so seeding stays reproducible; a time-based id derives its time from the rng.
|
// per-session counter state, which is itself deterministic. arity is the exact arg
|
||||||
// seq is the one exception: it advances per-session counter state, which is itself
|
// count, or -1 for variadic (then check does all the validation).
|
||||||
// deterministic (1, 2, 3 …). arity is the exact arg count, or -1 for variadic
|
|
||||||
// (then check does all the validation). The optional check validates args at
|
|
||||||
// compile time (their values, beyond the count). The registry lives in builtins.go.
|
|
||||||
type builtin struct {
|
type builtin struct {
|
||||||
arity int
|
arity int
|
||||||
// prep parses validated args once, at compile time, into the closure expand calls.
|
// prep parses validated args once, at compile time, into the closure expand calls.
|
||||||
prep func(args []string) callFn
|
prep func(args []string) callFn
|
||||||
check func(fields map[string]node, args []string) error
|
check func(fields map[string]node, args []string) error
|
||||||
|
// operands names the fields the call reads, which expand renders for it; nil
|
||||||
|
// for a builtin that reads none.
|
||||||
|
operands func(args []string) []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// funcCall splits a "{token}" body shaped name(args) into its parts; ok is false
|
// funcCall splits a "{token}" body shaped name(args) into its parts; ok is false
|
||||||
@@ -141,23 +140,9 @@ func checkTokens(format string, fields map[string]node) error {
|
|||||||
}
|
}
|
||||||
continue // a root reference; its target is checked at New (see linkRefs)
|
continue // a root reference; its target is checked at New (see linkRefs)
|
||||||
}
|
}
|
||||||
a := splitArm(name)
|
if err := checkArm(name, fields); err != nil {
|
||||||
if err := checkSegments(a); err != nil {
|
|
||||||
return fmt.Errorf("token {%s}: %w", t.body, err)
|
return fmt.Errorf("token {%s}: %w", t.body, err)
|
||||||
}
|
}
|
||||||
head, ok := fields[a.key]
|
|
||||||
if !ok {
|
|
||||||
if a.key == "" {
|
|
||||||
return fmt.Errorf("token {%s}: a name is never empty, so this token can name no field", t.body)
|
|
||||||
}
|
|
||||||
if isOption(a.key) {
|
|
||||||
return fmt.Errorf("token {%s}: %q is an option and can never be a field", t.body, a.key)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("token {%s}: no field %q", t.body, a.key)
|
|
||||||
}
|
|
||||||
if err := checkPath(head, a.tail, a.key); err != nil {
|
|
||||||
return fmt.Errorf("token {%s}: field %q: %w", t.body, a.key, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Last, so an arm broken on its own terms is reported as that: a repeat is
|
// Last, so an arm broken on its own terms is reported as that: a repeat is
|
||||||
// the consequence of such a mistake, not the mistake itself.
|
// the consequence of such a mistake, not the mistake itself.
|
||||||
@@ -165,11 +150,57 @@ func checkTokens(format string, fields map[string]node) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkNoRepeatedArm rejects {a|a|b}. An alternation picks its arms evenly, so a
|
// checkArm validates one sibling name or path against a template's fields.
|
||||||
// repeated one skews the odds — a third spelling of what weight is for, and one an
|
func checkArm(name string, fields map[string]node) error {
|
||||||
// author is far likelier to have typed by accident than meant. The error names the
|
a := splitArm(name, nil)
|
||||||
// spelling that does skew a pick. It runs over the arms as written, so a repeated
|
if err := checkSegments(a); err != nil {
|
||||||
// reference arm ({..a|..a}) is caught alongside a repeated sibling.
|
return err
|
||||||
|
}
|
||||||
|
head, ok := fields[a.key]
|
||||||
|
if !ok {
|
||||||
|
if a.key == "" {
|
||||||
|
return fmt.Errorf("a name is never empty, so this token can name no field")
|
||||||
|
}
|
||||||
|
if isOption(a.key) {
|
||||||
|
return fmt.Errorf("%q is an option and can never be a field", a.key)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("no field %q", a.key)
|
||||||
|
}
|
||||||
|
if err := checkPath(head, a.tail, a.key); err != nil {
|
||||||
|
return fmt.Errorf("field %q: %w", a.key, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenOperands lists the fields one {token} body reads as operands, empty for a
|
||||||
|
// field token or a builtin that reads none.
|
||||||
|
func tokenOperands(body string) []string {
|
||||||
|
name, args, ok := funcCall(body)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b, known := builtins[name]
|
||||||
|
if !known || b.operands == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return b.operands(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// operandTokens lists every operand the builtins in a format read.
|
||||||
|
func operandTokens(format string) []string {
|
||||||
|
var names []string
|
||||||
|
_ = eachToken(format, func(t ftoken) error {
|
||||||
|
if t.kind == 'b' {
|
||||||
|
names = append(names, tokenOperands(t.body)...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkNoRepeatedArm rejects {a|a|b}: an alternation picks its arms evenly, so a
|
||||||
|
// repeated one is a second spelling of weight. The error names the spelling that
|
||||||
|
// does skew a pick.
|
||||||
func checkNoRepeatedArm(body string, names []string) error {
|
func checkNoRepeatedArm(body string, names []string) error {
|
||||||
if len(names) < 2 {
|
if len(names) < 2 {
|
||||||
return nil
|
return nil
|
||||||
@@ -198,10 +229,11 @@ func fieldTokens(format string) []string {
|
|||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
// arm is one alternative of a {a|b} token, split into the key naming the node in
|
// arm is one alternative of a {a|b} token or one operand, split into the key
|
||||||
// a template's fields (a sibling field, or the whole "..path" string a reference is
|
// naming the node in a template's fields (a sibling field, or the "..path" head a
|
||||||
// bound under) and the tail of a dotted path into it. A non-empty tail is what makes
|
// reference is bound under) and the tail of a dotted path into it. A non-empty
|
||||||
// the arm a bound draw: its head is drawn once per expansion (see compileOps).
|
// tail is what makes the arm a bound draw: its head is drawn once per expansion
|
||||||
|
// (see compileOps).
|
||||||
type arm struct {
|
type arm struct {
|
||||||
name string // as written, and the key a bound draw's value is held under
|
name string // as written, and the key a bound draw's value is held under
|
||||||
key string
|
key string
|
||||||
@@ -209,32 +241,38 @@ type arm struct {
|
|||||||
steps []string // key per level passed through; the head and leaf hold their own
|
steps []string // key per level passed through; the head and leaf hold their own
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitArm splits one token alternative into key and tail. A reference keeps its
|
// splitArm splits one name into key and tail. refs maps a reference to the head
|
||||||
// dots — linkRefs binds it whole — so only a sibling name reads as a path.
|
// linkRefs bound it under; before linking, a reference is whole.
|
||||||
func splitArm(name string) arm {
|
func splitArm(name string, refs map[string]string) arm {
|
||||||
if isRef(name) {
|
if isRef(name) {
|
||||||
return arm{name: name, key: name}
|
key, bound := refs[name]
|
||||||
|
if !bound || key == name {
|
||||||
|
return arm{name: name, key: name}
|
||||||
|
}
|
||||||
|
return pathArm(name, key, strings.Split(name[len(key)+1:], "."))
|
||||||
}
|
}
|
||||||
head, tail, dotted := strings.Cut(name, ".")
|
head, tail, dotted := strings.Cut(name, ".")
|
||||||
if !dotted {
|
if !dotted {
|
||||||
return arm{name: name, key: name}
|
return arm{name: name, key: name}
|
||||||
}
|
}
|
||||||
segs := strings.Split(tail, ".")
|
return pathArm(name, head, strings.Split(tail, "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathArm(name, key string, segs []string) arm {
|
||||||
var steps []string
|
var steps []string
|
||||||
for i := 0; i < len(segs)-1; i++ { // every level except the leaf's own
|
for i := 0; i < len(segs)-1; i++ { // every level except the leaf's own
|
||||||
steps = append(steps, head+"."+strings.Join(segs[:i+1], "."))
|
steps = append(steps, key+"."+strings.Join(segs[:i+1], "."))
|
||||||
}
|
}
|
||||||
return arm{name: name, key: head, tail: segs, steps: steps}
|
return arm{name: name, key: key, tail: segs, steps: steps}
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkNoOverlap rejects a format that both renders a level and reads a path into
|
// checkNoOverlap rejects a format that both renders a level and reads a path into
|
||||||
// it — {p} beside {p.first}, or {p.addr} beside {p.addr.city}. The two spell one
|
// it — {p} beside {p.first}, or {p.addr} beside {p.addr.city}. The path reads the
|
||||||
// draw two ways: the path reads the level's held draw, while rendering the level
|
// level's held draw while rendering the level expands it afresh, so their values
|
||||||
// expands it afresh, so their values disagree. One spelling, so there is nothing
|
// would disagree. Names are compared in sorted order, so which pair is reported
|
||||||
// to get wrong. Names are compared in sorted order, so which pair is reported
|
|
||||||
// does not depend on where the tokens sit.
|
// does not depend on where the tokens sit.
|
||||||
func checkNoOverlap(format string, bound map[string]string) error {
|
func checkNoOverlap(format string, bound map[string]string, refs map[string]string) error {
|
||||||
names := boundReaders(format, bound)
|
names := boundReaders(format, bound, refs)
|
||||||
// Stable over one format-order scan, so two readers of one name (a token and a
|
// Stable over one format-order scan, so two readers of one name (a token and a
|
||||||
// calc operand both naming "p") are reported as the format writes them.
|
// calc operand both naming "p") are reported as the format writes them.
|
||||||
sort.SliceStable(names, func(i, j int) bool { return names[i].name < names[j].name })
|
sort.SliceStable(names, func(i, j int) bool { return names[i].name < names[j].name })
|
||||||
@@ -252,23 +290,24 @@ func checkNoOverlap(format string, bound map[string]string) error {
|
|||||||
type reader struct{ name, label string }
|
type reader struct{ name, label string }
|
||||||
|
|
||||||
// boundReaders lists every way a format reaches a bound field, in the order the
|
// boundReaders lists every way a format reaches a bound field, in the order the
|
||||||
// format writes them. A calc operand renders its field, so it names a level exactly
|
// format writes them. An operand renders its field, so it names a level exactly
|
||||||
// as a token does; one scan finds both, which is what puts them in one order.
|
// as a token does; one scan finds both, which is what puts them in one order.
|
||||||
func boundReaders(format string, bound map[string]string) []reader {
|
func boundReaders(format string, bound map[string]string, refs map[string]string) []reader {
|
||||||
var names []reader
|
var names []reader
|
||||||
_ = eachToken(format, func(t ftoken) error {
|
_ = eachToken(format, func(t ftoken) error {
|
||||||
if t.kind != 'b' {
|
if t.kind != 'b' {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if _, _, isFunc := funcCall(t.body); isFunc {
|
if fn, _, isFunc := funcCall(t.body); isFunc {
|
||||||
for _, operand := range calcTokenOperands(t.body) {
|
for _, operand := range tokenOperands(t.body) {
|
||||||
if _, isBound := bound[operand]; isBound {
|
a := splitArm(operand, refs)
|
||||||
names = append(names, reader{operand, fmt.Sprintf("calc operand %q", operand)})
|
if _, isBound := bound[a.key]; isBound {
|
||||||
|
names = append(names, reader{a.name, fmt.Sprintf("%s operand %q", fn, operand)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, a := range splitArms(t.body) {
|
for _, a := range splitArms(t.body, refs) {
|
||||||
if _, isBound := bound[a.key]; isBound {
|
if _, isBound := bound[a.key]; isBound {
|
||||||
names = append(names, reader{a.name, "token {" + a.name + "}"})
|
names = append(names, reader{a.name, "token {" + a.name + "}"})
|
||||||
}
|
}
|
||||||
@@ -297,11 +336,11 @@ func checkSegments(a arm) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// splitArms splits a token body's '|' alternatives.
|
// splitArms splits a token body's '|' alternatives.
|
||||||
func splitArms(body string) []arm {
|
func splitArms(body string, refs map[string]string) []arm {
|
||||||
parts := strings.Split(body, "|")
|
parts := strings.Split(body, "|")
|
||||||
arms := make([]arm, len(parts))
|
arms := make([]arm, len(parts))
|
||||||
for i, p := range parts {
|
for i, p := range parts {
|
||||||
arms[i] = splitArm(p)
|
arms[i] = splitArm(p, refs)
|
||||||
}
|
}
|
||||||
return arms
|
return arms
|
||||||
}
|
}
|
||||||
@@ -311,39 +350,46 @@ func splitArms(body string) []arm {
|
|||||||
// values of the operands it named, which expand read for it.
|
// values of the operands it named, which expand read for it.
|
||||||
type callFn func(s *session, emitted string, operands []string) string
|
type callFn func(s *session, emitted string, operands []string) string
|
||||||
|
|
||||||
// op is one compiled unit of a format string: a literal run, a class char, a field
|
// op is one compiled unit of a format string: a literal run, a field alternation,
|
||||||
// alternation, or a builtin already bound to its args. compile builds these so
|
// or a builtin already bound to its args. compile builds these so render never
|
||||||
// render never re-scans the format.
|
// re-scans the format.
|
||||||
type op struct {
|
type op struct {
|
||||||
kind byte // 'l' literal run, 'c' class char, 'f' field alternation, 'b' builtin
|
kind byte // 'l' literal run, 'f' field alternation, 'b' builtin
|
||||||
lit string // kind 'l'
|
lit string // kind 'l'
|
||||||
r rune // kind 'c'
|
|
||||||
arms []arm // kind 'f': the '|' alternatives, split into key and path once
|
arms []arm // kind 'f': the '|' alternatives, split into key and path once
|
||||||
call callFn
|
call callFn
|
||||||
// operands names the sibling fields a {calc()} reads, in the order calcVars
|
// operands are the fields the builtin reads, in the order its operands func
|
||||||
// fixed; expand reads them before the call. nil for every other builtin.
|
// fixed; expand reads them before the call. nil for a builtin that reads none.
|
||||||
operands []string
|
operands []arm
|
||||||
}
|
}
|
||||||
|
|
||||||
// compileOps turns a format string into ops, and returns the smallest output it can
|
// compileOps turns a format string into ops, and returns the size of its literal
|
||||||
// produce (literals plus one byte per class char) to size the render buffer, plus
|
// text to size the render buffer, plus two sets. bound is the levels the format
|
||||||
// two sets. bound is the levels the format addresses by dotted path, each mapped to
|
// addresses by dotted path, each mapped to the first path reading it, which is what
|
||||||
// the first path reading it, which is what the overlap fences name. held is every
|
// the overlap fences name. held is every name drawn once per expansion — those
|
||||||
// name drawn once per expansion — those levels, plus the siblings a {calc()} reads,
|
// levels, plus the fields an operand reads, so an operand shown is the operand
|
||||||
// so an operand shown is the operand computed. Both are nil when the format needs
|
// computed. Both are nil when the format needs neither, so data that uses neither
|
||||||
// neither, so data that uses neither carries no render-time cost. Call checkTokens
|
// carries no render-time cost. Call checkTokens first: it is what proves the scan
|
||||||
// first: it is what proves the scan and every token are valid.
|
// and every token are valid.
|
||||||
func compileOps(format string) ([]op, int, map[string]string, map[string]bool) {
|
func compileOps(format string, refs map[string]string) ([]op, int, map[string]string, map[string]bool) {
|
||||||
var ops []op
|
var ops []op
|
||||||
var lit strings.Builder
|
var lit strings.Builder
|
||||||
var bound map[string]string
|
var bound map[string]string
|
||||||
var held map[string]bool
|
var held map[string]bool
|
||||||
grow := 0
|
grow := 0
|
||||||
hold := func(name string) {
|
hold := func(a arm) {
|
||||||
if held == nil {
|
if held == nil {
|
||||||
held = map[string]bool{}
|
held = map[string]bool{}
|
||||||
}
|
}
|
||||||
held[name] = true
|
held[a.key] = true
|
||||||
|
if len(a.tail) > 0 {
|
||||||
|
if bound == nil {
|
||||||
|
bound = map[string]string{}
|
||||||
|
}
|
||||||
|
if _, named := bound[a.key]; !named {
|
||||||
|
bound[a.key] = a.name // the first path reading it, for error messages
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
flush := func() {
|
flush := func() {
|
||||||
if lit.Len() > 0 {
|
if lit.Len() > 0 {
|
||||||
@@ -356,29 +402,21 @@ func compileOps(format string) ([]op, int, map[string]string, map[string]bool) {
|
|||||||
switch t.kind {
|
switch t.kind {
|
||||||
case 'l':
|
case 'l':
|
||||||
lit.WriteRune(t.r)
|
lit.WriteRune(t.r)
|
||||||
case 'c':
|
|
||||||
flush()
|
|
||||||
grow++
|
|
||||||
ops = append(ops, op{kind: 'c', r: t.r})
|
|
||||||
case 'b':
|
case 'b':
|
||||||
flush()
|
flush()
|
||||||
if name, args, ok := funcCall(t.body); ok {
|
if name, args, ok := funcCall(t.body); ok {
|
||||||
operands := calcTokenOperands(t.body)
|
var operands []arm
|
||||||
for _, operand := range operands {
|
for _, operand := range tokenOperands(t.body) {
|
||||||
hold(operand) // a calc renders its operand, so the expansion holds that draw
|
a := splitArm(operand, refs)
|
||||||
|
hold(a) // the builtin renders its operand, so the expansion holds that draw
|
||||||
|
operands = append(operands, a)
|
||||||
}
|
}
|
||||||
ops = append(ops, op{kind: 'b', call: builtins[name].prep(args), operands: operands})
|
ops = append(ops, op{kind: 'b', call: builtins[name].prep(args), operands: operands})
|
||||||
} else {
|
} else {
|
||||||
arms := splitArms(t.body)
|
arms := splitArms(t.body, refs)
|
||||||
for _, a := range arms {
|
for _, a := range arms {
|
||||||
if len(a.tail) > 0 {
|
if len(a.tail) > 0 {
|
||||||
if bound == nil {
|
hold(a)
|
||||||
bound = map[string]string{}
|
|
||||||
}
|
|
||||||
if _, named := bound[a.key]; !named {
|
|
||||||
bound[a.key] = a.name // the first path reading it, for error messages
|
|
||||||
}
|
|
||||||
hold(a.key)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ops = append(ops, op{kind: 'f', arms: arms})
|
ops = append(ops, op{kind: 'f', arms: arms})
|
||||||
|
|||||||
+11
-11
@@ -7,25 +7,25 @@ import "testing"
|
|||||||
// cannot quietly shift the rng stream that reproducibility depends on.
|
// cannot quietly shift the rng stream that reproducibility depends on.
|
||||||
func TestSeededOutputIsStable(t *testing.T) {
|
func TestSeededOutputIsStable(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"alt": `{"format":"{a|b}","a":["A"],"b":["B"]}`,
|
"alt": `{"format":"{a|b}","a":"A","b":"B"}`,
|
||||||
"calc": `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99","5.00","100.00"],"qty":["2","3","7"]}`,
|
"calc": `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99","5.00","100.00"],"qty":["2","3","7"]}`,
|
||||||
"classes": `{"format":"00-11-AA-aa"}`,
|
"classes": `"{digits(2)}-{int(1,9)}{int(1,9)}-{upper(2)}-{lower(2)}"`,
|
||||||
"escapes": `{"format":"#0#1#A#a##{x}","x":["!"]}`,
|
"escapes": `{"format":"01Aa#{x}","x":"!"}`,
|
||||||
"funcs": `{"format":"{hex(6)} {int(10,99)} {float(0,1,3)} {nanoid(5)} {seq()}"}`,
|
"funcs": `"{hex(6)} {int(10,99)} {float(0,1,3)} {nanoid(5)} {seq()}"`,
|
||||||
"nested": `{"format":"{outer}","outer":[{"format":"{inner}-00","inner":["i"]}]}`,
|
"nested": `{"format":"{outer}","outer":{"format":"{inner}-{digits(2)}","inner":"i"}}`,
|
||||||
"ref": `{"format":"see {..alt}"}`,
|
"ref": `"see {..alt}"`,
|
||||||
"repeat": `{"format":"{w}","repeat":4,"separator":",","w":["x","y","z"]}`,
|
"repeat": `{"format":"{w}","repeat":4,"separator":",","w":["x","y","z"]}`,
|
||||||
"sums": `{"format":"9{d}{luhn()} {e}{ean()} {m}{mod11()}","d":["012345678901234"],"e":["123456789012"],"m":["12345678"]}`,
|
"sums": `{"format":"9{d}{luhn()} {e}{ean()} {m}{mod11()}","d":"012345678901234","e":"123456789012","m":"12345678"}`,
|
||||||
"weights": `[{"format":"big","weight":9},{"format":"tiny","weight":1}]`,
|
"weights": `[{"format":"big","weight":9},"tiny"]`,
|
||||||
})
|
})
|
||||||
want := map[string][]string{
|
want := map[string][]string{
|
||||||
"alt": {"A", "B", "A", "A"},
|
"alt": {"A", "A", "B", "A"},
|
||||||
"calc": {"100.00 x 3 = 300.00", "100.00 x 3 = 300.00", "100.00 x 7 = 700.00", "5.00 x 3 = 15.00"},
|
"calc": {"100.00 x 3 = 300.00", "100.00 x 3 = 300.00", "100.00 x 7 = 700.00", "5.00 x 3 = 15.00"},
|
||||||
"classes": {"49-74-VY-gj", "38-68-RP-xo", "47-68-IB-hs", "91-89-LP-gk"},
|
"classes": {"49-74-VY-gj", "38-68-RP-xo", "47-68-IB-hs", "91-89-LP-gk"},
|
||||||
"escapes": {"01Aa#!", "01Aa#!", "01Aa#!", "01Aa#!"},
|
"escapes": {"01Aa#!", "01Aa#!", "01Aa#!", "01Aa#!"},
|
||||||
"funcs": {"0016a2 33 0.649 k4Kwj 1", "a00c07 84 0.175 6UjGv 2", "f70eb8 12 0.390 p5p6g 3", "9048a3 90 0.531 I3Aqv 4"},
|
"funcs": {"0016a2 33 0.649 k4Kwj 1", "a00c07 84 0.175 6UjGv 2", "f70eb8 12 0.390 p5p6g 3", "9048a3 90 0.531 I3Aqv 4"},
|
||||||
"nested": {"i-89", "i-67", "i-47", "i-27"},
|
"nested": {"i-73", "i-23", "i-67", "i-85"},
|
||||||
"ref": {"see A", "see A", "see B", "see A"},
|
"ref": {"see A", "see A", "see A", "see B"},
|
||||||
"repeat": {"z,y,z,y", "z,z,y,y", "z,z,x,z", "x,z,y,y"},
|
"repeat": {"z,y,z,y", "z,z,y,y", "z,z,x,z", "x,z,y,y"},
|
||||||
"sums": {"90123456789012348 1234567890124 123456780", "90123456789012348 1234567890124 123456780", "90123456789012348 1234567890124 123456780", "90123456789012348 1234567890124 123456780"},
|
"sums": {"90123456789012348 1234567890124 123456780", "90123456789012348 1234567890124 123456780", "90123456789012348 1234567890124 123456780", "90123456789012348 1234567890124 123456780"},
|
||||||
"weights": {"big", "big", "big", "big"},
|
"weights": {"big", "big", "big", "big"},
|
||||||
|
|||||||
+108
-58
@@ -35,19 +35,33 @@ func mustRender(t *testing.T, f *Generator, s string) string {
|
|||||||
return render(f.rand, compiled(t, s))
|
return render(f.rand, compiled(t, s))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLiteralStringIsVerbatim(t *testing.T) {
|
func TestStringIsAFormat(t *testing.T) {
|
||||||
// A bare string is a literal, never formatted: 'a'/'A' must survive.
|
f := engine(1)
|
||||||
if got := mustRender(t, engine(1), `"Malmö"`); got != "Malmö" {
|
for src, want := range map[string]string{
|
||||||
t.Fatalf("literal = %q, want Malmö", got)
|
`"Malmö"`: "Malmö",
|
||||||
|
`"100 Main St, Apt 1A #0"`: "100 Main St, Apt 1A #0",
|
||||||
|
`"{{x}}"`: "{x}",
|
||||||
|
} {
|
||||||
|
if got := mustRender(t, f, src); got != want {
|
||||||
|
t.Errorf("render(%s) = %q, want %q", src, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := mustRender(t, f, `"{digits(3)}"`); !regexp.MustCompile(`^[0-9]{3}$`).MatchString(got) {
|
||||||
|
t.Errorf(`render("{digits(3)}") = %q, want three digits`, got)
|
||||||
|
}
|
||||||
|
if _, err := compile(parse(t, `"{x}"`)); err == nil || !strings.Contains(err.Error(), `no field "x"`) {
|
||||||
|
t.Errorf(`compile("{x}") = %v, want a no-field error`, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCharacterClasses(t *testing.T) {
|
func TestClassBuiltins(t *testing.T) {
|
||||||
cases := map[string]*regexp.Regexp{
|
cases := map[string]*regexp.Regexp{
|
||||||
`{"format":"0"}`: regexp.MustCompile(`^[0-9]$`),
|
`"{digits(1)}"`: regexp.MustCompile(`^[0-9]$`),
|
||||||
`{"format":"1"}`: regexp.MustCompile(`^[1-9]$`),
|
`"{digits(3)}"`: regexp.MustCompile(`^[0-9]{3}$`),
|
||||||
`{"format":"A"}`: regexp.MustCompile(`^[A-Z]$`),
|
`"{int(1,9)}"`: regexp.MustCompile(`^[1-9]$`),
|
||||||
`{"format":"a"}`: regexp.MustCompile(`^[a-z]$`),
|
`"{upper(1)}"`: regexp.MustCompile(`^[A-Z]$`),
|
||||||
|
`"{lower(1)}"`: regexp.MustCompile(`^[a-z]$`),
|
||||||
|
`"{upper(2)}{lower(2)}"`: regexp.MustCompile(`^[A-Z]{2}[a-z]{2}$`),
|
||||||
}
|
}
|
||||||
f := engine(7)
|
f := engine(7)
|
||||||
for tmpl, re := range cases {
|
for tmpl, re := range cases {
|
||||||
@@ -59,15 +73,39 @@ func TestCharacterClasses(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEscapeAndLiteralChars(t *testing.T) {
|
func TestTextIsLiteral(t *testing.T) {
|
||||||
// '#' escapes the next char; non-class chars (7, x, -) are literal.
|
if got := mustRender(t, engine(1), `{"format":"100 Main St #1 {x}","x":"A"}`); got != "100 Main St #1 A" {
|
||||||
if got := mustRender(t, engine(1), `{"format":"#0#1#A#a## x7-z"}`); got != "01Aa# x7-z" {
|
t.Fatalf("text = %q, want it verbatim", got)
|
||||||
t.Fatalf("escape = %q, want \"01Aa# x7-z\"", got)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBraceEscapes(t *testing.T) {
|
||||||
|
f := engine(1)
|
||||||
|
for src, want := range map[string]string{
|
||||||
|
`{"format":"{{","x":"v"}`: "{",
|
||||||
|
`{"format":"}}","x":"v"}`: "}",
|
||||||
|
`{"format":"{{x}}","x":"v"}`: "{x}",
|
||||||
|
`{"format":"{{{x}}}","x":"v"}`: "{v}",
|
||||||
|
`{"format":"a{{{{b}}}}","x":"v"}`: "a{{b}}",
|
||||||
|
} {
|
||||||
|
if got := mustRender(t, f, src); got != want {
|
||||||
|
t.Errorf("render(%s) = %q, want %q", src, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for src, want := range map[string]string{
|
||||||
|
`"x}y"`: "}}",
|
||||||
|
`"}"`: "}}",
|
||||||
|
`{"format":"{a{b}","a":"Q"}`: "'{'",
|
||||||
|
`"{x"`: "unterminated",
|
||||||
|
} {
|
||||||
|
if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) {
|
||||||
|
t.Errorf("compile(%s) = %v, want an error mentioning %s", src, err, want)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTokenSubstitution(t *testing.T) {
|
func TestTokenSubstitution(t *testing.T) {
|
||||||
if got := mustRender(t, engine(1), `{"format":"{x}sson","x":["Erik"]}`); got != "Eriksson" {
|
if got := mustRender(t, engine(1), `{"format":"{x}sson","x":"Erik"}`); got != "Eriksson" {
|
||||||
t.Fatalf("token = %q, want Eriksson", got)
|
t.Fatalf("token = %q, want Eriksson", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,7 +114,7 @@ func TestAlternationPicksOneField(t *testing.T) {
|
|||||||
f := engine(3)
|
f := engine(3)
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
seen[mustRender(t, f, `{"format":"{a|b}","a":["A"],"b":["B"]}`)] = true
|
seen[mustRender(t, f, `{"format":"{a|b}","a":"A","b":"B"}`)] = true
|
||||||
}
|
}
|
||||||
if !seen["A"] || !seen["B"] || len(seen) != 2 {
|
if !seen["A"] || !seen["B"] || len(seen) != 2 {
|
||||||
t.Fatalf("alternation produced %v, want both A and B", seen)
|
t.Fatalf("alternation produced %v, want both A and B", seen)
|
||||||
@@ -96,7 +134,7 @@ func TestWeightSkewsDistribution(t *testing.T) {
|
|||||||
f := engine(9)
|
f := engine(9)
|
||||||
heavy := 0
|
heavy := 0
|
||||||
for i := 0; i < 1000; i++ {
|
for i := 0; i < 1000; i++ {
|
||||||
if mustRender(t, f, `[{"format":"H","weight":10},{"format":"L"}]`) == "H" {
|
if mustRender(t, f, `[{"format":"H","weight":10},"L"]`) == "H" {
|
||||||
heavy++
|
heavy++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,7 +156,7 @@ func TestRepeatRendersFormatNTimes(t *testing.T) {
|
|||||||
f, re := engine(7), regexp.MustCompile(`^[0-9]{4}$`)
|
f, re := engine(7), regexp.MustCompile(`^[0-9]{4}$`)
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for i := 0; i < 50; i++ {
|
for i := 0; i < 50; i++ {
|
||||||
got := mustRender(t, f, `{"format":"0","repeat":4}`)
|
got := mustRender(t, f, `{"format":"{digits(1)}","repeat":4}`)
|
||||||
if !re.MatchString(got) {
|
if !re.MatchString(got) {
|
||||||
t.Fatalf("repeat-4 = %q, want 4 digits", got)
|
t.Fatalf("repeat-4 = %q, want 4 digits", got)
|
||||||
}
|
}
|
||||||
@@ -135,10 +173,10 @@ func TestFunctionTokenLuhn(t *testing.T) {
|
|||||||
// buffer, so a value is never re-rendered. Bodies are escaped to fix input.
|
// buffer, so a value is never re-rendered. Bodies are escaped to fix input.
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
`{"format":"#8#1#1#2#1#8#9#8#7{luhn()}"}`: "8112189876", // personnummer body
|
`"811218987{luhn()}"`: "8112189876", // personnummer body
|
||||||
`{"format":"#7#9#9#2#7#3#9#8#7#1{luhn()}"}`: "79927398713", // classic Luhn vector
|
`"7992739871{luhn()}"`: "79927398713", // classic Luhn vector
|
||||||
`{"format":"#8#1#1#2#1#8-#9#8#7{luhn()}"}`: "811218-9876", // '-' skipped, kept
|
`"811218-987{luhn()}"`: "811218-9876", // '-' skipped, kept
|
||||||
`{"format":"{n}{luhn()}","n":["811218987"]}`: "8112189876", // over a rendered token
|
`{"format":"{n}{luhn()}","n":"811218987"}`: "8112189876", // over a rendered token
|
||||||
}
|
}
|
||||||
for tmpl, want := range cases {
|
for tmpl, want := range cases {
|
||||||
if got := mustRender(t, f, tmpl); got != want {
|
if got := mustRender(t, f, tmpl); got != want {
|
||||||
@@ -151,7 +189,7 @@ func TestRecursionHasNoDepthLimit(t *testing.T) {
|
|||||||
// Build {format:{a}, a:[{format:{a}, a:[ ... "deep" ]]}} 50 levels deep.
|
// Build {format:{a}, a:[{format:{a}, a:[ ... "deep" ]]}} 50 levels deep.
|
||||||
tmpl := `"deep"`
|
tmpl := `"deep"`
|
||||||
for i := 0; i < 50; i++ {
|
for i := 0; i < 50; i++ {
|
||||||
tmpl = `{"format":"{a}","a":[` + tmpl + `]}`
|
tmpl = `{"format":"{a}","a":` + tmpl + `}`
|
||||||
}
|
}
|
||||||
if got := mustRender(t, engine(1), tmpl); got != "deep" {
|
if got := mustRender(t, engine(1), tmpl); got != "deep" {
|
||||||
t.Fatalf("deep recursion = %q, want deep", got)
|
t.Fatalf("deep recursion = %q, want deep", got)
|
||||||
@@ -162,45 +200,56 @@ func TestCompileErrors(t *testing.T) {
|
|||||||
// Every structural problem is caught up front, at compile/New time, never
|
// Every structural problem is caught up front, at compile/New time, never
|
||||||
// deferred to a random render that happens to hit the bad branch.
|
// deferred to a random render that happens to hit the bad branch.
|
||||||
for _, bad := range []string{
|
for _, bad := range []string{
|
||||||
`{"x":["Q"]}`, // object without "format"
|
`{"x":"Q"}`, // object without "format"
|
||||||
`{"format":"{y}","x":1}`, // a field is a bare number
|
`{"format":"{y}","x":1}`, // a field is a bare number
|
||||||
`[1, 2]`, // a choice of numbers
|
`[1, 2]`, // a choice of numbers
|
||||||
`5`, // unsupported node type
|
`5`, // unsupported node type
|
||||||
`[]`, // empty choice
|
`[]`, // empty choice
|
||||||
`{"format":"{x"}`, // unterminated brace
|
`"{x"`, // unterminated brace
|
||||||
`{"format":"{y}","x":["Q"]}`, // token names a missing field
|
`"x}y"`, // a lone } must be written }}
|
||||||
`{"format":"{}"}`, // empty token name
|
`["x"]`, // a one-item choice is its item
|
||||||
`{"format":"{a|}","a":["Q"]}`, // empty alternation segment
|
`["a","a"]`, // a repeated item is a weight
|
||||||
`[{"format":"A","weight":-1},{"format":"B"}]`, // negative weight
|
`{"format":"x"}`, // an object holding only a format is a string
|
||||||
|
`[{"format":"a","weight":1},"b"]`, // weight 1 is the default
|
||||||
|
`{"format":"{x}","x":"v","repeat":1}`, // repeat 1 is the default
|
||||||
|
`{"format":"{a{b}","a":"Q"}`, // a brace inside a token
|
||||||
|
`"{x}"`, // a bare string has no fields to name
|
||||||
|
`"{digits(0)}"`, // count must be positive
|
||||||
|
`"{upper(x)}"`, // count must be an integer
|
||||||
|
`"{lower()}"`, // wrong arity
|
||||||
|
`{"format":"{y}","x":"Q"}`, // token names a missing field
|
||||||
|
`"{}"`, // empty token name
|
||||||
|
`{"format":"{a|}","a":"Q"}`, // empty alternation segment
|
||||||
|
`[{"format":"A","weight":-1},"B"]`, // negative weight
|
||||||
`[{"format":"A","weight":0},{"format":"B","weight":0}]`, // weights sum to zero
|
`[{"format":"A","weight":0},{"format":"B","weight":0}]`, // weights sum to zero
|
||||||
`[{"format":"A","weight":1e308},{"format":"B","weight":1e308}]`, // weights overflow to +Inf
|
`[{"format":"A","weight":1e308},{"format":"B","weight":1e308}]`, // weights overflow to +Inf
|
||||||
`[{"format":"A","weight":"heavy"}]`, // non-numeric weight
|
`{"format":"A","weight":"heavy"}`, // non-numeric weight
|
||||||
`{"format":"x","repeat":0}`, // repeat below 1
|
`{"format":"x","repeat":0}`, // repeat below 1
|
||||||
`{"format":"x","repeat":-2}`, // negative repeat
|
`{"format":"x","repeat":-2}`, // negative repeat
|
||||||
`{"format":"x","repeat":1.5}`, // non-integer repeat
|
`{"format":"x","repeat":1.5}`, // non-integer repeat
|
||||||
`{"format":"x","repeat":"two"}`, // non-numeric repeat
|
`{"format":"x","repeat":"two"}`, // non-numeric repeat
|
||||||
`{"format":"x","repeat":2,"separator":5}`, // non-string separator
|
`{"format":"x","repeat":2,"separator":5}`, // non-string separator
|
||||||
`{"format":"{nope()}"}`, // unknown function
|
`"{nope()}"`, // unknown function
|
||||||
`{"format":"{luhn(x)}"}`, // function given args it takes none of
|
`"{luhn(x)}"`, // function given args it takes none of
|
||||||
`{"format":"{luhn(}"}`, // malformed function token
|
`"{luhn(}"`, // malformed function token
|
||||||
`{"format":"{int(1)}"}`, // wrong arity
|
`"{int(1)}"`, // wrong arity
|
||||||
`{"format":"{int(a,b)}"}`, // non-integer args
|
`"{int(a,b)}"`, // non-integer args
|
||||||
`{"format":"{int(5,1)}"}`, // min > max
|
`"{int(5,1)}"`, // min > max
|
||||||
`{"format":"{hex(0)}"}`, // count must be positive
|
`"{hex(0)}"`, // count must be positive
|
||||||
`{"format":"{nanoid(-1)}"}`, // negative count
|
`"{nanoid(-1)}"`, // negative count
|
||||||
`{"format":"{base64(0)}"}`, // count must be positive
|
`"{base64(0)}"`, // count must be positive
|
||||||
`{"format":"{float(1,2)}"}`, // wrong arity
|
`"{float(1,2)}"`, // wrong arity
|
||||||
`{"format":"{float(1,2,-1)}"}`, // negative decimals
|
`"{float(1,2,-1)}"`, // negative decimals
|
||||||
`{"format":"{iban(US)}"}`, // unsupported country
|
`"{iban(US)}"`, // unsupported country
|
||||||
`{"format":"{seq(a,b)}"}`, // seq takes at most one name
|
`"{seq(a,b)}"`, // seq takes at most one name
|
||||||
`{"format":"{calc()}"}`, // calc needs an expression
|
`"{calc()}"`, // calc needs an expression
|
||||||
`{"format":"{calc(1 +)}"}`, // dangling operator
|
`"{calc(1 +)}"`, // dangling operator
|
||||||
`{"format":"{calc((1 + 2)}"}`, // unbalanced parenthesis
|
`"{calc((1 + 2)}"`, // unbalanced parenthesis
|
||||||
`{"format":"{calc(1 2)}"}`, // two operands, no operator
|
`"{calc(1 2)}"`, // two operands, no operator
|
||||||
`{"format":"{calc(price)}"}`, // operand names no field
|
`"{calc(price)}"`, // operand names no field
|
||||||
`{"format":"{calc(1, 2, 3)}"}`, // too many args
|
`"{calc(1, 2, 3)}"`, // too many args
|
||||||
`{"format":"{calc(1, x)}"}`, // decimals arg not an integer
|
`"{calc(1, x)}"`, // decimals arg not an integer
|
||||||
`{"format":"{calc(1, -1)}"}`, // decimals negative
|
`"{calc(1, -1)}"`, // decimals negative
|
||||||
} {
|
} {
|
||||||
if _, err := compile(parse(t, bad)); err == nil {
|
if _, err := compile(parse(t, bad)); err == nil {
|
||||||
t.Errorf("compile(%s) = nil error, want error", bad)
|
t.Errorf("compile(%s) = nil error, want error", bad)
|
||||||
@@ -211,7 +260,7 @@ func TestCompileErrors(t *testing.T) {
|
|||||||
func TestFakePathNavigation(t *testing.T) {
|
func TestFakePathNavigation(t *testing.T) {
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
f.categories = map[string]node{
|
f.categories = map[string]node{
|
||||||
"addr": compiled(t, `[{"format":"{street}","street":["Main"]}]`),
|
"addr": compiled(t, `{"format":"{street}","street":"Main"}`),
|
||||||
}
|
}
|
||||||
if got, err := f.Fake("addr"); err != nil || !strings.Contains(got, "Main") {
|
if got, err := f.Fake("addr"); err != nil || !strings.Contains(got, "Main") {
|
||||||
t.Fatalf("Fake(addr) = %q, %v", got, err)
|
t.Fatalf("Fake(addr) = %q, %v", got, err)
|
||||||
@@ -235,15 +284,16 @@ func TestGrowIsALowerBound(t *testing.T) {
|
|||||||
for _, format := range []string{
|
for _, format := range []string{
|
||||||
"",
|
"",
|
||||||
"plain literal",
|
"plain literal",
|
||||||
"00-11-AA-aa",
|
"{digits(2)}-{int(1,9)}{int(1,9)}-{upper(2)}-{lower(2)}",
|
||||||
"#0#1#A#a## literal",
|
"01Aa# literal",
|
||||||
|
"{{}} {{{x}}}",
|
||||||
"Ö dag åäö 日本語",
|
"Ö dag åäö 日本語",
|
||||||
"{x}{x}{x}",
|
"{x}{x}{x}",
|
||||||
"{hex(8)}-{int(10,99)}-{nanoid(5)}",
|
"{hex(8)}-{int(10,99)}-{nanoid(5)}",
|
||||||
"9{d}{luhn()}",
|
"9{d}{luhn()}",
|
||||||
"{a|b} and {a|b}",
|
"{a|b} and {a|b}",
|
||||||
} {
|
} {
|
||||||
src := `{"format":` + quote(format) + `,"x":["1"],"a":["A"],"b":["B"],"d":["012345678901234"]}`
|
src := `{"format":` + quote(format) + `,"x":"1","a":"A","b":"B","d":"012345678901234"}`
|
||||||
tmpl, ok := compiled(t, src).(*template)
|
tmpl, ok := compiled(t, src).(*template)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("format %q did not compile to a template", format)
|
t.Fatalf("format %q did not compile to a template", format)
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package fejkdata
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestTransforms(t *testing.T) {
|
||||||
|
f := engine(1)
|
||||||
|
for src, want := range map[string]string{
|
||||||
|
`{"format":"{uppercase(x)}|{lowercase(x)}|{ascii(x)}","x":"Åsa Ödegård-Nuñez"}`: "ÅSA ÖDEGÅRD-NUÑEZ|åsa ödegård-nuñez|Asa Odegard-Nunez",
|
||||||
|
`{"format":"{lowercase(ascii(x))}","x":"Åsa"}`: "asa",
|
||||||
|
`{"format":"{ascii(x)}","x":"Ærø ß 日本"}`: "AEro ss ",
|
||||||
|
} {
|
||||||
|
if got := mustRender(t, f, src); got != want {
|
||||||
|
t.Errorf("render(%s) = %q, want %q", src, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransformReadsTheHeldDraw(t *testing.T) {
|
||||||
|
f := engine(2)
|
||||||
|
src := `{"format":"{p.first}={lowercase(p.first)}","p":[{"format":"{first}","first":"Anna"},{"format":"{first}","first":"Bo"}]}`
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
if got := mustRender(t, f, src); got != "Anna=anna" && got != "Bo=bo" {
|
||||||
|
t.Fatalf("render = %q, want the transform over the same draw", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransformOverAReference(t *testing.T) {
|
||||||
|
dir := writeData(t, map[string]string{
|
||||||
|
"person": `[{"format":"{first} {last}","first":"Åsa","last":"Öberg"},{"format":"{first} {last}","first":"Bo","last":"Ek"}]`,
|
||||||
|
"email": `"{..person.first} {..person.last} <{lowercase(ascii(..person.first))}.{lowercase(ascii(..person.last))}@example.com>"`,
|
||||||
|
})
|
||||||
|
f := newGenerator(t, dir, WithSeed(5))
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
if got := fake(t, f, "email"); got != "Åsa Öberg <asa.oberg@example.com>" && got != "Bo Ek <bo.ek@example.com>" {
|
||||||
|
t.Fatalf("email = %q, want a record from one draw", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransformArgs(t *testing.T) {
|
||||||
|
for _, bad := range []string{
|
||||||
|
`{"format":"{lowercase()}","x":"v"}`,
|
||||||
|
`{"format":"{lowercase(nope)}","x":"v"}`,
|
||||||
|
`{"format":"{ascii(x,y)}","x":"v","y":"w"}`,
|
||||||
|
`{"format":"{lowercase(hex(2))}","x":"v"}`,
|
||||||
|
`{"format":"{lowercase(x)} {x.a}","x":{"format":"{a}","a":"1"}}`,
|
||||||
|
} {
|
||||||
|
if _, err := compile(parse(t, bad)); err == nil {
|
||||||
|
t.Errorf("compile(%s) = nil error, want it rejected", bad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user