Replace locale dirs with a data-folder namespace model: New([]paths), folder dot-paths, last-wins merge

This commit is contained in:
lilleman
2026-06-08 23:52:37 +02:00
parent acf7571334
commit 8db65ac598
46 changed files with 423 additions and 278 deletions
+51 -39
View File
@@ -7,10 +7,11 @@ lacked the locale coverage and format control we needed.
- **Standards first** — formats, names and structures follow international and
local standards first and foremost.
- **Locale-aware** — names, addresses, postal codes and phone numbers follow
per-locale data and formats. Locales are always full language + territory
tags (`sv_SE`, never `sv`).
per-locale data and formats. The shipped data is organised by full locale tag
(`sv_SE`), but the engine treats folders as plain namespaces — name yours
anything.
- **Data lives in JSON** — all source data is recursive JSON on disk, read when
you create a faker and then served from memory. Add or change locales without
you create a faker and then served from memory. Add or change data without
touching the library. Behavior belongs in data too: the engine grows a
built-in function only for what data can't express (a checksum, a time-based
id), never for what character classes and choices already do.
@@ -23,25 +24,27 @@ lacked the locale coverage and format control we needed.
## CLI
Install the `fakes` command, then point it at a locale directory and a category
path — it prints one value to stdout. The first path segment names a category (a
JSON file); deeper dotted segments descend into it.
Install the `fakes` command, then point it at one or more data directories and a
path — it prints one value to stdout. Each dot segment descends one level:
folders, then the category (a JSON file), then fields inside it.
```sh
go install github.com/Timewave-AB/fakes/cmd/fakes@latest
fakes ./locales/sv_SE person # Sara Eriksson
fakes ./locales/sv_SE person.last # Eriksson (dotted path into a category)
fakes -seed 42 ./locales/sv_SE address
fakes ./data/sv_SE person # Sara Eriksson
fakes ./data/sv_SE person.last # Eriksson (dotted path into a category)
fakes ./data sv_SE.person # point at the tree; the folder is a segment
fakes ./data/sv_SE ./mydata word # layer dirs; the last wins a name clash
fakes -seed 42 ./data/sv_SE address
```
Without installing, run it from a checkout with `go run ./cmd/fakes …`. Exit
codes: `0` success, `1` runtime error (bad locale, unknown path), `2` misuse.
codes: `0` success, `1` runtime error (missing dir, unknown path), `2` misuse.
### Generating a file from a custom template
A category is just a JSON file in the locale directory, so you can drop in your
own and render it — no code change. Save this as `locales/sv_SE/sql.json`:
A category is just a JSON file in a data directory, so you can drop in your
own and render it — no code change. Save this as `data/sv_SE/sql.json`:
```json
{
@@ -58,10 +61,10 @@ own and render it — no code change. Save this as `locales/sv_SE/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 [Locale data format](#locale-data-format).)
letter token — see [Data format](#data-format).)
```sh
fakes -seed 1 ./locales/sv_SE sql
fakes -seed 1 ./data/sv_SE sql
# INSERT INTO users VALUES('zoom'),('wahoo'),('blip');
```
@@ -69,7 +72,7 @@ Raise `repeat` for more rows per statement, or loop in the shell to build a
whole seed file:
```sh
for _ in $(seq 100); do fakes ./locales/sv_SE sql; done > seed.sql
for _ in $(seq 100); do fakes ./data/sv_SE sql; done > seed.sql
```
## Library
@@ -78,9 +81,9 @@ for _ in $(seq 100); do fakes ./locales/sv_SE sql; done > seed.sql
go get github.com/Timewave-AB/fakes # requires Go 1.22+ (for math/rand/v2)
```
Point `New` at a locale directory, then generate values by category path with
`Fake`. The first path segment names a category (a JSON file); deeper dotted
segments descend into it.
Point `New` at one or more data directories, then generate values by path with
`Fake`. Each dot segment descends one level: folders, then the category (a JSON
file), then fields inside it.
```go
package main
@@ -93,7 +96,7 @@ import (
)
func main() {
f, err := fakes.New("./locales/sv_SE")
f, err := fakes.New([]string{"./data/sv_SE"})
if err != nil {
log.Fatal(err)
}
@@ -120,8 +123,8 @@ Seed a faker for reproducible output — same seed + locale yields an identical
sequence, handy for stable tests:
```go
a, _ := fakes.New("./locales/sv_SE", fakes.WithSeed(42))
b, _ := fakes.New("./locales/sv_SE", fakes.WithSeed(42))
a, _ := fakes.New([]string{"./data/sv_SE"}, fakes.WithSeed(42))
b, _ := fakes.New([]string{"./data/sv_SE"}, fakes.WithSeed(42))
av, _ := a.Fake("person")
bv, _ := b.Fake("person")
av == bv // true
@@ -129,25 +132,34 @@ av == bv // true
A `*Fakes` is **not** safe for concurrent use — create one per goroutine.
## Locales
## Data
The library ships a ready-to-use set under [`locales/`](locales) (`en_US`,
`sv_SE`). Point either tool at one of them, a copy, or your own directory —
anywhere on disk.
The library ships a ready-to-use set under [`data/`](data), organised by locale
(`en_US`, `sv_SE`). Point either tool at the whole tree, a single locale, a
copy, or your own directory — anywhere on disk; no naming rules.
Each ships 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`, `uuid`,
`version`, `word`.
A directory is just a namespace. Each JSON file is a category named after the
file; each subdirectory is a dot-path segment — folders nest exactly like JSON
objects do. So `data/sv_SE/person.json` is `Fake("person")` when you point at
`data/sv_SE`, or `Fake("sv_SE.person")` when you point at `data`.
The directory's name is the locale and must be a full tag (`sv_SE`, never
`sv`). Any casing or separator is accepted and canonicalised (`sv-se`,
`SV_SE``sv_SE`); a non-full name returns an error.
Pass several directories and they merge, left to right: matching folders combine
by their children, and any other clash is won by the last directory loaded. That
lets you layer your own data over the built-ins without copying them:
## Locale data format
```go
fakes.New([]string{"./data/sv_SE", "./mydata"}) // mydata overrides on a clash
```
Each JSON file in a locale directory is a **category** named after the file
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`,
`uuid`, `version`, `word`.
## Data format
Each JSON file in a data directory is a **category** named after the file
(`address.json``address`), rendered by `Fake("address")`. Drop in a new file
or folder — no code change, no recompile.
@@ -297,13 +309,13 @@ docker compose run --rm test # latest
```
fakes.go Fakes, New, options, seeding
template.go Fake, the recursive renderer (choices, format strings, paths)
locale.go locale loading + tag parsing
data.go data loading: folders/files -> namespace tree, multi-path merge
cmd/fakes/ the `fakes` CLI (New + Fake over stdout)
locales/ shipped locale data (JSON)
data/ shipped data (JSON), organised by locale
```
To add a category, drop a JSON file into a locale directory; to add a locale,
add a directory named with its full tag.
To add a category, drop a JSON file into a data directory; to add a locale, add
a subdirectory of JSON files.
## License