Skip to content

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.

rust
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

rust
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_one runs a standalone envelope (inline {{ }} works; {{ref}} won't resolve — there's no schema).
  • generate_field runs a named generator from a Schema, so references resolve.

The envelope

Every generator invocation is a GeneratorDef:

rust
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:

rust
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.

rust
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.

rust
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

rust
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

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

In-memory:

rust
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:

rust
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

rust
fn derive_seed(root_seed: u64, field_path: &str, row: u64) -> u64;
const DEFAULT_REFERENCE_TIME: i64; // 2024-01-01T00:00:00Z

derive_seed is the exact recipe (mix(root ^ fnv1a(field_path) ^ rotate(row))) a non-Rust runtime reproduces. See How generation works.

Errors

rust
enum GenError {
    Unknown(String),    // no such generator/asset/reference
    BadParams(String),  // params didn't satisfy the generator
}

Phony Cloud — Documentation & Specification