///|
pub(all) enum Provider {
Name
Address
Email
Username
Url
Uuid4
Ipv4
Ipv6
MacAddress
HexColor
Company
Job
Word
Sentence(Int)
Boolean(Int)
Integer(Int, Int)
Decimal(Int, Int, Int)
DateBetween(String, String)
Ean13
Isbn13
Template(String)
Choice(Array[String])
WeightedChoice(Array[String], Array[Int])
Constant(Json)
Sequence(Int, Int)
Localized(String, String)
} derive(Debug, Eq, ToJson)
///|
pub(all) struct Column {
name : String
provider : Provider
unique : Bool
null_percent : Int
} derive(Debug, Eq)
///|
pub fn Column::new(
name : String,
provider : Provider,
unique? : Bool = false,
null_percent? : Int = 0,
) -> Column {
{ name, provider, unique, null_percent, }
}
///|
fn bounds(min : Int, max : Int) -> Unit raise FakeError {
if min > max || min < -1000000000 || max > 1000000000 {
raise Invalid("integer bounds")
}
}
///|
fn scalar(value : Json) -> Json raise FakeError {
match value {
String(s) => {
if s.length() > 4096 {
raise Invalid("scalar string length")
}
Json::string(s)
}
Number(n, ..) => {
if n.is_nan() || n.is_inf() {
raise Invalid("non-finite JSON number")
}
Json::number(n)
}
Null | True | False => value
_ => raise Invalid("only scalar constant values supported")
}
}
///|
fn choice_values(values : Array[String]) -> Unit raise FakeError {
if values.is_empty() || values.length() > 10000 {
raise Invalid("choice count 1..10000 required")
}
let mut total = 0
for value in values {
total += value.length()
if value.length() > 4096 || total > 1000000 {
raise Invalid("choice text budget")
}
}
}
///|
fn sequence_value(start : Int, step : Int, index : Int) -> Int raise FakeError {
let value = start.to_int64() + step.to_int64() * index.to_int64()
if value < -1000000000L || value > 1000000000L {
raise Invalid("sequence bounds")
}
value.to_int()
}
///|
fn Provider::validate(self : Provider) -> Unit raise FakeError {
match self {
Localized(locale, provider_name) => {
ignore(locale_table(locale, provider_name))
if provider_name.has_prefix("postcode_set_") {
raise Invalid("internal locale table")
}
}
Integer(min, max) => bounds(min, max)
Decimal(min, max, places) => {
bounds(min, max)
if places < 0 || places > 6 {
raise Invalid("decimal places")
}
}
DateBetween(first, last) =>
if ordinal(first) > ordinal(last) {
raise Invalid("reversed date range")
}
Sentence(n) => if n < 1 || n > 100 { raise Invalid("sentence words") }
Boolean(n) => if n < 0 || n > 100 { raise Invalid("boolean percentage") }
Choice(values) => choice_values(values)
WeightedChoice(values, weights) => {
choice_values(values)
if values.length() != weights.length() {
raise Invalid("choice weight length")
}
ignore(weight_total(weights))
}
Template(template) => {
if template.length() > 4096 {
raise Invalid("template length")
}
let mut escaped = false
for c in template {
if escaped {
escaped = false
} else if c == '\\' {
escaped = true
}
}
if escaped {
raise Invalid("trailing template escape")
}
}
Constant(value) => ignore(scalar(value))
Sequence(start, step) => {
bounds(start, start)
bounds(step, step)
}
_ => ()
}
}
///|
fn Generator::provide(
self : Generator,
provider : Provider,
index : Int,
) -> Json raise FakeError {
match provider {
Localized(locale, provider_name) =>
Json::string(self.localized(locale, provider_name))
Name => Json::string(self.name())
Address => Json::string(self.profile(index).address)
Email => Json::string(self.email())
Username => Json::string(self.username())
Url => Json::string(self.url())
Uuid4 => Json::string(self.uuid4())
Ipv4 => Json::string(self.ipv4())
Ipv6 => Json::string(self.ipv6())
MacAddress => Json::string(self.mac_address())
HexColor => Json::string(self.hex_color())
Company => Json::string(self.company())
Job => Json::string(self.job())
Word => Json::string(self.word())
Sentence(n) => Json::string(self.sentence(words=n))
Boolean(percent) => Json::boolean(self.boolean(percent~))
Integer(min, max) => self.integer(min, max).to_json()
Decimal(min, max, places) => Json::string(self.decimal(min, max, places))
DateBetween(first, last) => Json::string(self.date_between(first, last))
Ean13 => Json::string(self.ean13())
Isbn13 => Json::string(self.isbn13())
Template(template) => Json::string(self.bothify(template))
Choice(values) => Json::string(self.choose(values))
WeightedChoice(values, weights) =>
Json::string(values[self.weighted_index(weights)])
Constant(value) => scalar(value)
Sequence(start, step) => sequence_value(start, step, index).to_json()
}
}
///|
/// Generate one provider value. Sequence index is explicit and does not consume RNG.
pub fn Generator::value(
self : Generator,
provider : Provider,
index? : Int = 0,
) -> Json raise FakeError {
if index < 0 || index > 1000000000 {
raise Invalid("provider index")
}
provider.validate()
self.provide(provider, index)
}
///|
/// Flat scalar records. Per-column uniqueness includes null and has bounded retries.
pub fn records(
seed : UInt,
columns : Array[Column],
count : Int,
) -> Array[Json] raise FakeError {
if count < 0 ||
count > 10000 ||
columns.is_empty() ||
columns.length() > 32 ||
count * columns.length() > 100000 {
raise Invalid("record dimensions exceed budget")
}
let names : Map[String, Bool] = Map([])
let seen : Array[Map[String, Bool]] = []
for column in columns {
if column.name.is_empty() ||
column.name.length() > 80 ||
names.contains(column.name) {
raise Invalid("invalid or duplicate column name")
}
names[column.name] = true
if column.null_percent < 0 || column.null_percent > 100 {
raise Invalid("null percentage")
}
column.provider.validate()
if column.provider is Sequence(start, step) {
ignore(sequence_value(start, step, if count > 0 { count - 1 } else { 0 }))
}
seen.push(Map([]))
}
let generator = Generator::new(seed)
let rows = []
let mut attempts = 0
let mut size = 0
for index in 0.. 1000000 {
raise Invalid("generation attempt budget")
}
let value = if column.null_percent > 0 &&
generator.boolean(percent=column.null_percent) {
Json::null()
} else {
generator.provide(column.provider, index)
}
let key = value.stringify()
if column.unique && seen[c].contains(key) {
continue
}
if column.unique {
seen[c][key] = true
}
row[column.name] = value
accepted = true
break
}
if !accepted {
raise Invalid("unique retry budget for column " + column.name)
}
}
let value = Json::object(row)
size += value.stringify().length()
if size > 8000000 {
raise Invalid("record output budget")
}
rows.push(value)
}
rows
}
///|
pub fn render_records(
columns : Array[Column],
rows : Array[Json],
format : String,
) -> String raise FakeError {
if !["json", "jsonl", "csv"].contains(format) {
raise Invalid("format must be json, jsonl or csv")
}
if rows.length() > 10000 ||
columns.is_empty() ||
columns.length() > 32 ||
rows.length() * columns.length() > 100000 {
raise Invalid("render dimensions exceed budget")
}
let seen : Map[String, Bool] = Map([])
for column in columns {
if column.name.is_empty() ||
column.name.length() > 80 ||
seen.contains(column.name) {
raise Invalid("invalid or duplicate column name")
}
seen[column.name] = true
}
let canonical = []
let mut size = 0
for row in rows {
let source = object(row)
if source.length() != columns.length() {
raise Invalid("row column count mismatch")
}
let clean : Map[String, Json] = Map([])
for column in columns {
clean[column.name] = scalar(required(source, column.name))
}
let value = Json::object(clean)
size += value.stringify().length()
if size > 8000000 {
raise Invalid("render output budget")
}
canonical.push(value)
}
if format == "json" {
return Json::array(canonical).stringify()
}
if format == "jsonl" {
return if canonical.is_empty() {
""
} else {
canonical.map(row => row.stringify()).join("\n") + "\n"
}
}
fn quote(s : String) -> String {
"\"" + s.replace_all(old="\"", new="\"\"") + "\""
}
let lines = [columns.map(c => quote(c.name)).join(",")]
for row in canonical {
let object = match row {
Object(object) => object
_ => raise Invalid("CSV row must be an object")
}
let cells = []
for column in columns {
let value = match object.get(column.name) {
Some(v) => scalar(v)
None => raise Invalid("missing CSV column")
}
cells.push(
quote(
match value {
String(s) => s
Null => ""
_ => value.stringify()
},
),
)
}
lines.push(cells.join(","))
}
lines.join("\r\n") + "\r\n"
}