///|
pub(all) enum Spec {
Value(Provider)
Object(Array[(String, Spec)])
List(Spec, Int, Int)
Nullable(Spec, Int)
Unique(Spec)
Reference(String)
Foreign(String, String)
Join(Array[Spec], String)
} derive(Debug, Eq, ToJson)
///|
fn scalar_spec(spec : Spec) -> Bool {
match spec {
Value(_) => true
Nullable(child, _) => scalar_spec(child)
_ => false
}
}
///|
fn validate_spec(
spec : Spec,
path : String,
depth : Int,
unique_paths : Map[String, Bool],
counter : Ref[Int],
) -> Unit raise FakeError {
counter.val += 1
if depth > 16 || counter.val > 1024 {
raise Invalid("schema depth/node limit")
}
match spec {
Value(provider) => provider.validate()
Object(fields) => {
if fields.length() > 128 {
raise Invalid("object field limit")
}
let names : Map[String, Bool] = Map([])
for i, (name, child) in fields {
if name.is_empty() || name.length() > 80 || names.contains(name) {
raise Invalid("invalid or duplicate field: " + name)
}
names[name] = true
validate_spec(
child,
path + "/" + i.to_string(),
depth + 1,
unique_paths,
counter,
)
}
}
List(child, low, high) => {
if low < 0 || low > high || high > 1000 {
raise Invalid("array length 0..1000 required")
}
validate_spec(child, path + "/items", depth + 1, unique_paths, counter)
}
Nullable(child, percent) => {
if percent < 0 || percent > 100 {
raise Invalid("null percentage")
}
validate_spec(child, path + "/nullable", depth + 1, unique_paths, counter)
}
Unique(child) => {
if !scalar_spec(child) {
raise Invalid("unique requires a scalar provider, optionally nullable")
}
unique_paths[path] = true
validate_spec(child, path + "/unique", depth + 1, unique_paths, counter)
}
Reference(pointer) => ignore(pointer_parts(pointer))
Foreign(table, pointer) => {
if table.is_empty() || table.length() > 80 {
raise Invalid("invalid table name")
}
ignore(pointer_parts(pointer))
}
Join(parts, separator) => {
if parts.length() > 128 || separator.length() > 1024 {
raise Invalid("join dimensions")
}
for i, child in parts {
validate_spec(
child,
path + "/join" + i.to_string(),
depth + 1,
unique_paths,
counter,
)
}
}
}
}
///|
fn pointer_parts(pointer : String) -> Array[String] raise FakeError {
if pointer.length() > 1024 ||
(!pointer.is_empty() && !pointer.has_prefix("/")) {
raise Invalid("reference must be a JSON pointer")
}
if pointer.is_empty() {
return []
}
let parts = []
for fragment in pointer[1:].split("/") {
let cs = fragment.to_owned().to_array()
let out = StringBuilder()
let mut i = 0
while i < cs.length() {
if cs[i] == '~' {
if i + 1 >= cs.length() || (cs[i + 1] != '0' && cs[i + 1] != '1') {
raise Invalid("invalid JSON pointer escape")
}
out.write_char(if cs[i + 1] == '0' { '~' } else { '/' })
i += 2
} else {
out.write_char(cs[i])
i += 1
}
}
parts.push(out.to_string())
}
parts
}
///|
fn pointer_escape(text : String) -> String {
text.replace_all(old="~", new="~0").replace_all(old="/", new="~1")
}
///|
fn at_pointer(value : Json, parts : ArrayView[String]) -> Json raise FakeError {
let mut current = value
for part in parts {
current = match current {
Object(fields) =>
match fields.get(part) {
Some(v) => v
None => raise Invalid("reference field missing: " + part)
}
Array(values) => {
let mut index = 0
if part.is_empty() || (part.length() > 1 && part.has_prefix("0")) {
raise Invalid("invalid reference index")
}
for c in part {
if c < '0' || c > '9' || index > 1000000 {
raise Invalid("invalid reference index")
}
index = index * 10 + c.to_int() - 48
}
match values.get(index) {
Some(v) => v
None => raise Invalid("reference index out of range")
}
}
_ => raise Invalid("reference traverses a scalar")
}
}
current
}
///|
fn resolve_reference(
published : Map[String, Json],
pointer : String,
) -> Json raise FakeError {
if published.get(pointer) is Some(value) {
return value
}
let parts = pointer_parts(pointer)
let mut path = ""
for i, part in parts {
path += "/" + pointer_escape(part)
if published.get(path) is Some(value) {
return at_pointer(value, parts[i + 1:])
}
}
raise Invalid("reference requires an earlier completed field: " + pointer)
}
///|
fn parse_spec_inner(value : Json, depth : Int) -> Spec raise FakeError {
if depth > 16 {
raise Invalid("schema depth limit")
}
let fields = object(value)
if fields.contains("provider") {
let column = parse_column(value)
let mut spec = Value(column.provider)
if column.null_percent != 0 {
spec = Nullable(spec, column.null_percent)
}
if column.unique {
spec = Unique(spec)
}
return spec
}
let kind = text_option(fields, "type", "")
match kind {
"object" => {
reject_unknown(fields, ["type", "fields", "name"])
guard required(fields, "fields") is Array(entries) else {
raise Invalid("fields array required")
}
if entries.length() > 128 {
raise Invalid("object field limit")
}
Object(
entries.map(entry => {
let field = object(entry)
(text_option(field, "name", ""), parse_spec_inner(entry, depth + 1))
}),
)
}
"array" => {
reject_unknown(fields, ["type", "items", "min", "max", "name"])
List(
parse_spec_inner(required(fields, "items"), depth + 1),
int_option(fields, "min", 0),
int_option(fields, "max", 3),
)
}
"ref" => {
reject_unknown(fields, ["type", "path", "name"])
Reference(text_option(fields, "path", ""))
}
"foreign" => {
reject_unknown(fields, ["type", "table", "path", "name"])
Foreign(text_option(fields, "table", ""), text_option(fields, "path", ""))
}
"nullable" => {
reject_unknown(fields, ["type", "value", "percent", "name"])
Nullable(
parse_spec_inner(required(fields, "value"), depth + 1),
int_option(fields, "percent", 50),
)
}
"unique" => {
reject_unknown(fields, ["type", "value", "name"])
Unique(parse_spec_inner(required(fields, "value"), depth + 1))
}
"join" => {
reject_unknown(fields, ["type", "parts", "separator", "name"])
guard required(fields, "parts") is Array(parts) else {
raise Invalid("join parts array required")
}
if parts.length() > 128 {
raise Invalid("join part limit")
}
Join(
parts.map(p => parse_spec_inner(p, depth + 1)),
text_option(fields, "separator", ""),
)
}
_ => raise Invalid("unknown schema type: " + kind)
}
}
///|
pub fn parse_spec(value : Json) -> Spec raise FakeError {
let spec = parse_spec_inner(value, 0)
validate_spec(spec, "", 0, Map([]), Ref(0))
spec
}
///|
fn owned_spec(spec : Spec) -> Spec {
match spec {
Value(Choice(values)) => Value(Choice(values.copy()))
Value(WeightedChoice(values, weights)) =>
Value(WeightedChoice(values.copy(), weights.copy()))
Object(fields) => Object(fields.map(pair => (pair.0, owned_spec(pair.1))))
List(child, low, high) => List(owned_spec(child), low, high)
Nullable(child, percent) => Nullable(owned_spec(child), percent)
Unique(child) => Unique(owned_spec(child))
Join(parts, separator) => Join(parts.map(owned_spec), separator)
other => other
}
}