Skip to content

The engine (Rust)

Today, PDL schemas are compiled and run by phony-core, a Rust workspace. (A CLI and language libraries — PHP, JS, Python — are planned and will read the same portable artifacts; this page documents the engine you can use now.)

phony-core has two crates:

CrateRoleAPI
phony-pdlthe composition layer — envelope, registry, all 7 generators, PEL, the entity layer, the locale resolverphony-pdl API
ngram-corethe N-gram engine — the Model generator (train + generate + the .ngram format)ngram-core API

phony-pdl calls into ngram-core for model-type generation.

Install

Until published to crates.io, use a path dependency:

toml
# Cargo.toml
phony-pdl  = { path = "../phony-core/crates/pdl" }
ngram-core = { path = "../phony-core/crates/ngram-core" }
serde_json = "1"

Quick start — run a scenario

Compile a PDL schema and generate tables:

rust
use std::sync::Arc;
use phony_pdl::{Dataset, InMemoryAssets, Registry};
use ngram_core::NgramModel;
use serde_json::json;

// 1. Assets — the models/lists the `model`/`list` generators look up by name.
let assets = InMemoryAssets::new()
    .with_model("person.first_names", NgramModel::train_words(&["mehmet","ayse","mustafa","zeynep"], 3));

// 2. Registry — built-in generators + assets + (optionally) a frozen clock.
let registry = Registry::with_builtins()
    .with_assets(Arc::new(assets))
    .with_reference_time(1_704_067_200); // 2024-01-01T00:00:00Z

// 3. A PDL schema (see "The PDL Language" for the syntax).
let schema = json!({
  "generators": {
    "id":         { "type": "logic", "algorithm": "uuid_v7" },
    "first_name": { "type": "model", "source": "person.first_names",
                    "generation": { "mode": "word" } },
    "email":      { "type": "template",
                    "pattern": "{{lowercase(first_name)}}@example.com", "unique": true }
  },
  "entities": { "User": { "fields": {
    "id":         { "generator": "id", "primary_key": true },
    "first_name": { "generator": "first_name" },
    "email":      { "generator": "email" }
  }}},
  "scenarios": { "dev": { "User": 100 } }
});

// 4. Compile + generate — byte-identical for the same seed.
let dataset = Dataset::from_pdl(&schema).unwrap();
let tables  = dataset.generate_scenario(&registry, /* seed */ 2026, "dev").unwrap();

// tables: BTreeMap<String, Vec<serde_json::Value>>
let users = &tables["User"]; // 100 row objects

Entry points

Pick the smallest one that fits:

FunctionGeneratesReferences resolve?
generate_one(def, registry, seed, field, row)one value from a standalone envelopeinline only
generate_field(schema, name, registry, seed, scope, row)one named generator{{ref}} against the schema
Dataset::generate_table(registry, seed, entity, count, parents)one entity's rowsrow consistency
Dataset::generate_scenario(registry, seed, scenario)all tables in a scenariofull consistency + FKs

Assets

Generators never embed data — they resolve it from an AssetProvider:

rust
pub trait AssetProvider {
    fn model(&self, name: &str) -> Option<Arc<NgramModel>>;
    fn list(&self,  name: &str) -> Option<Arc<serde_json::Value>>;
}

The frozen clock

now()/today()/age() and relative date ranges resolve against an injected reference instant, never the wall clock:

rust
let registry = Registry::with_builtins().with_reference_time(1_704_067_200);

So (schema, seed, reference-time) fully determines the output — see How generation works.

Build & test (contributing)

bash
cargo test                  # whole workspace
cargo clippy -p phony-pdl
cargo bench -p ngram-core   # engine throughput

Phony Cloud — Documentation & Specification