phony-pdl API
The composition layer: it compiles PDL schemas and generates values, rows, and tables. This page is the Rust API reference. For the language, see The PDL Language.
Generating data
Dataset
The entry point for whole schemas.
impl Dataset {
fn from_pdl(pdl: &serde_json::Value) -> Result<Dataset, String>;
fn generate_scenario(&self, registry: &Registry, root_seed: u64, scenario: &str)
-> Result<Tables, GenError>;
fn generate_table(&self, registry: &Registry, root_seed: u64,
entity: &str, count: usize, parents: &Tables)
-> Result<Vec<Value>, GenError>;
fn schema(&self) -> &Schema;
}
// Tables = BTreeMap<String, Vec<serde_json::Value>> (entity → rows)generate_scenario orders entities by foreign key, generates each table, and returns them keyed by entity name. Each row is a serde_json::Value object.
Single values & fields
fn generate_one(def: &GeneratorDef, registry: &Registry,
root_seed: u64, field_path: &str, row: u64) -> Result<Value, GenError>;
fn generate_field(schema: &Schema, name: &str, registry: &Registry,
root_seed: u64, scope: &str, row: u64) -> Result<Value, GenError>;generate_oneruns a standalone envelope (inline{{ }}works;{{ref}}won't resolve — there's no schema).generate_fieldruns a named generator from aSchema, so references resolve.
The envelope
Every generator invocation is a GeneratorDef:
struct GeneratorDef {
use_ref: String, // serialized as "use" — e.g. "@phony/core:logic.int_between"
params: Value,
constraints: Value,
modifiers: Modifiers,
}
struct Modifiers {
unique: Option<bool>,
nullable: Option<f64>, // probability
transform: Option<String>, // a PEL expression over `value`
}compile_generator(pdl: &Value) -> Result<GeneratorDef, String> maps a PDL per-type definition ({ "type": "logic", "algorithm": …, "params": … }) onto this envelope.
A generator's manifest describes its accepted parameters so an envelope can be checked without the implementation:
struct Manifest { name: String, version: String, summary: String,
params: BTreeMap<String, ParamSpec>, output: String, deterministic: bool }
fn validate(def: &GeneratorDef, registry: &Registry) -> Result<(), Vec<String>>;The registry
An open lookup from namespaced reference to generator, plus the asset provider and reference instant used during generation.
impl Registry {
fn new() -> Self; // empty
fn with_builtins() -> Self; // all @phony/core:* generators
fn register(&mut self, g: Arc<dyn Generator>) -> Option<Arc<dyn Generator>>;
fn resolve(&self, reference: &str) -> Option<&Arc<dyn Generator>>;
fn names(&self) -> Vec<&str>;
fn with_assets(self, assets: Arc<dyn AssetProvider>) -> Self;
fn set_assets(&mut self, assets: Arc<dyn AssetProvider>);
fn with_reference_time(self, epoch: i64) -> Self;
fn set_reference_time(&mut self, epoch: i64);
}Custom generators
Implement Generator and register it under a namespaced name. execute must be a pure function of ctx.seed, params, and constraints.
trait Generator: Send + Sync {
fn manifest(&self) -> &Manifest;
fn execute(&self, ctx: &GenContext, params: &Value, constraints: &Value)
-> Result<Value, GenError>;
}
struct GenContext<'a> {
seed: u64, // derived for this exact cell
field_path: &'a str,
row: u64,
registry: &'a Registry,
assets: &'a dyn AssetProvider,
schema: &'a Schema,
depth: u32,
row_scope: Option<&'a RowScope<'a>>,
reference_time: i64,
}Draw randomness only from SplitMix64::new(ctx.seed) — never the wall clock or a global RNG.
Schema & PEL
struct Schema { generators: BTreeMap<String, GeneratorDef> }
mod pel {
fn render_template(src: &str, ctx: &GenContext) -> Result<String, GenError>;
fn eval_expression(src: &str, ctx: &GenContext, bound: Option<Value>) -> Result<Value, GenError>;
fn referenced_names_in_template(src: &str) -> Vec<String>;
fn referenced_names_in_expr(src: &str) -> Vec<String>;
}render_template renders a {{ … }} template; eval_expression evaluates one expression (with an optional bound value, used by transform). The referenced_names_* helpers power the entity layer's topological ordering. The function set is documented in Expressions (PEL).
Assets & locales
trait AssetProvider: Send + Sync {
fn model(&self, name: &str) -> Option<Arc<NgramModel>>;
fn list(&self, name: &str) -> Option<Arc<Value>>;
}In-memory:
let assets = InMemoryAssets::new()
.with_model("person.first_names", model)
.with_list("geo.cities", json!(["Istanbul", "Ankara"]));Locale-resolving — packages contribute (asset, locale) data; resolution walks a chain (most-specific first) with a * fallback:
let assets = LocaleAssets::default()
.contribute_list("coin.flip", "tr_TR", json!(["Yazı", "Tura"]))
.contribute_list("coin.flip", "en_US", json!(["Heads", "Tails"]))
.contribute_list("http.method", LOCALE_INDEPENDENT, json!(["GET", "POST"]))
.with_chain(vec!["tr_TR".into(), "base".into()]);Switching the chain switches the resolved data; the generator is unchanged. See Locales & the contribution model.
Determinism helpers
fn derive_seed(root_seed: u64, field_path: &str, row: u64) -> u64;
const DEFAULT_REFERENCE_TIME: i64; // 2024-01-01T00:00:00Zderive_seed is the exact recipe (mix(root ^ fnv1a(field_path) ^ rotate(row))) a non-Rust runtime reproduces. See How generation works.
Errors
enum GenError {
Unknown(String), // no such generator/asset/reference
BadParams(String), // params didn't satisfy the generator
}