///|
pub fn Generator::to_json(self : Generator) -> Json {
let fields : Map[String, Json] = Map([])
fn kind(name : String) -> Unit {
fields["kind"] = Json::string(name)
}
fn text(name : String, value : String) -> Unit {
fields[name] = Json::string(value)
}
fn number(name : String, value : Int) -> Unit {
fields[name] = Json::number(value.to_double())
}
match self {
Constant(value) => {
kind("constant")
fields["value"] = value.to_json()
}
Sequence(start, step) => {
kind("sequence")
number("start", start)
number("step", step)
}
IntegerRange(min, max) => {
kind("integer")
number("min", min)
number("max", max)
}
BooleanChance(chance) => {
kind("boolean")
number("chance", chance)
}
DateOffset(min, max) => {
kind("date_offset")
number("min", min)
number("max", max)
}
Choice(values) => {
kind("choice")
fields["values"] = Json::array(values.map(v => v.to_json()))
}
WeightedChoice(values) => {
kind("weighted_choice")
fields["values"] = Json::array(
values.map(pair => {
Json::object(
Map([
("value", pair.0.to_json()),
("weight", Json::number(pair.1.to_double())),
]),
)
}),
)
}
Pattern(alphabet, length) => {
kind("pattern")
text("alphabet", alphabet)
number("length", length)
}
Reference(entity, key) => {
kind("reference")
text("entity", entity)
text("key", key)
}
Copy(field) => {
kind("copy")
text("field", field)
}
Add(left, right) | Multiply(left, right) => {
kind(if self is Add(_, _) { "add" } else { "multiply" })
text("left", left)
text("right", right)
}
Concat(names, separator) => {
kind("concat")
fields["fields"] = Json::array(names.map(Json::string))
text("separator", separator)
}
Lookup(field, entity, key, value) => {
kind("lookup")
text("field", field)
text("entity", entity)
text("key", key)
text("value", value)
}
}
Json::object(fields)
}
///|
pub fn Field::to_json(self : Field) -> Json {
Json::object(
Map([
("name", Json::string(self.name)),
("generator", self.generator.to_json()),
("unique", Json::boolean(self.unique)),
("primary", Json::boolean(self.primary)),
("null_per_mille", Json::number(self.null_per_mille.to_double())),
]),
)
}
///|
pub fn Model::to_json(self : Model) -> Json {
Json::object(
Map([
("name", Json::string(self.name)),
(
"entities",
Json::array(
self.entities.map(entity => {
Json::object(
Map([
("name", Json::string(entity.name)),
("count", Json::number(entity.count.to_double())),
(
"fields",
Json::array(entity.fields.map(field => field.to_json())),
),
]),
)
}),
),
),
]),
)
}
///|
/// Canonical object-key ordering is explicit, independent of hash-map iteration.
fn canonical_json(value : Json) -> String {
match value {
Object(fields) => {
let names : Array[String] = []
for name, _ in fields {
names.push(name)
}
names.sort()
"{" +
names
.map(name => {
Json::string(name).stringify() +
":" +
canonical_json(fields.get(name).unwrap())
})
.join(",") +
"}"
}
Array(values) => "[" + values.map(canonical_json).join(",") + "]"
_ => value.stringify()
}
}
///|
pub fn Model::to_json_text(self : Model) -> String {
canonical_json(self.to_json())
}
///|
/// Non-cryptographic identity for regression reports, not an integrity signature.
pub fn Model::fingerprint(self : Model) -> String {
mix_name(2166136261U, self.to_json_text()).to_string()
}
///|
pub fn Limits::to_json(self : Limits) -> Json {
Json::object(
Map([
("max_entities", Json::number(self.max_entities.to_double())),
("max_fields", Json::number(self.max_fields.to_double())),
("max_rows", Json::number(self.max_rows.to_double())),
("max_cells", Json::number(self.max_cells.to_double())),
("max_attempts", Json::number(self.max_attempts.to_double())),
("max_text_units", Json::number(self.max_text_units.to_double())),
]),
)
}
///|
fn decode_limits(value : Json, path : String) -> Limits raise ConfigError {
let fields = object_fields(value, path)
reject_unknown(
fields,
[
"max_entities", "max_fields", "max_rows", "max_cells", "max_attempts", "max_text_units",
],
path,
)
fn number(name : String) -> Int raise ConfigError {
integer_value(required(fields, name, path), path + "." + name)
}
{
max_entities: number("max_entities"),
max_fields: number("max_fields"),
max_rows: number("max_rows"),
max_cells: number("max_cells"),
max_attempts: number("max_attempts"),
max_text_units: number("max_text_units"),
}
}
///|
fn unsigned_decimal(text : String, path : String) -> UInt raise ConfigError {
if text.is_empty() || text.length() > 10 {
raise InvalidConfig(path, "Expected an unsigned 32-bit decimal string")
}
let mut result = 0UL
for c in text.iter() {
if c < '0' || c > '9' {
raise InvalidConfig(path, "Expected decimal digits")
}
result = result * 10UL + (c.to_int() - 48).to_uint64()
if result > 4294967295UL {
raise InvalidConfig(path, "Seed exceeds UInt32")
}
}
result.to_uint()
}
///|
/// Full input manifest; replay also checks algorithm identity and resource limits.
pub fn Plan::replay_json(self : Plan) -> String {
canonical_json(
Json::object(
Map([
("format", Json::string("moonfixture.replay.v1")),
("algorithm", Json::string(algorithm_version())),
("seed", Json::string(self.context.seed.to_string())),
("reference_time", Json::string(self.context.reference_time)),
("limits", self.context.limits.to_json()),
("model", self.model.to_json()),
]),
),
)
}
///|
pub fn replay(
text : String,
max_units? : Int = 1048576,
) -> Result[Dataset, Array[Issue]] {
if guard_json(text, max_units, "replay") is Err(error) {
return Err([error])
}
if max_units <= 0 || text.length() > max_units {
return Err([issue("config_limit", "replay", "Replay exceeds input limit")])
}
try {
let fields = object_fields(@json.parse(text), "replay")
reject_unknown(
fields,
["format", "algorithm", "seed", "reference_time", "limits", "model"],
"replay",
)
fn text(name : String) -> String raise ConfigError {
text_value(required(fields, name, "replay"), "replay." + name)
}
if text("format") != "moonfixture.replay.v1" {
raise InvalidConfig("replay.format", "Unsupported replay format")
}
if text("algorithm") != algorithm_version() {
raise InvalidConfig("replay.algorithm", "Algorithm version mismatch")
}
let context : Context = {
seed: unsigned_decimal(text("seed"), "replay.seed"),
reference_time: text("reference_time"),
limits: decode_limits(
required(fields, "limits", "replay"),
"replay.limits",
),
}
generate(decode_model(required(fields, "model", "replay")), context~)
} catch {
InvalidConfig(path, message) =>
Err([issue("invalid_replay", path, message)])
_ => Err([issue("invalid_json", "replay", "Invalid JSON syntax")])
}
}