///|
/// Resource limits for validating untrusted JSON values and recursive schemas.
pub(all) struct ValidationOptions {
max_errors : Int
max_depth : Int
max_ref_depth : Int
max_nodes : Int
} derive(Eq, Debug)
///|
pub fn ValidationOptions::default() -> ValidationOptions {
{ max_errors: 100, max_depth: 256, max_ref_depth: 64, max_nodes: 1000000, }
}
///|
pub fn ValidationOptions::new(
max_errors? : Int = 100,
max_depth? : Int = 256,
max_ref_depth? : Int = 64,
max_nodes? : Int = 1000000,
) -> ValidationOptions {
{ max_errors, max_depth, max_ref_depth, max_nodes, }
}
///|
pub fn ValidationOptions::max_errors(self : ValidationOptions) -> Int {
self.max_errors
}
///|
pub fn ValidationOptions::max_depth(self : ValidationOptions) -> Int {
self.max_depth
}
///|
pub fn ValidationOptions::max_ref_depth(self : ValidationOptions) -> Int {
self.max_ref_depth
}
///|
pub fn ValidationOptions::max_nodes(self : ValidationOptions) -> Int {
self.max_nodes
}
///|
/// Complete outcome of one validation pass.
pub(all) struct ValidationReport {
errors : Array[Diagnostic]
visited_nodes : Int
truncated : Bool
} derive(Debug)
///|
pub fn ValidationReport::errors(self : ValidationReport) -> Array[Diagnostic] {
self.errors.copy()
}
///|
pub fn ValidationReport::is_valid(self : ValidationReport) -> Bool {
self.errors.is_empty()
}
///|
pub fn ValidationReport::visited_nodes(self : ValidationReport) -> Int {
self.visited_nodes
}
///|
pub fn ValidationReport::is_truncated(self : ValidationReport) -> Bool {
self.truncated
}
///|
priv struct ValidationState {
options : ValidationOptions
errors : Array[Diagnostic]
nodes : Ref[Int]
truncated : Ref[Bool]
}
///|
fn ValidationState::new(options : ValidationOptions) -> ValidationState {
{ options, errors: [], nodes: Ref(0), truncated: Ref(false), }
}
///|
fn ValidationState::full(self : ValidationState) -> Bool {
if self.errors.length() >= self.options.max_errors {
self.truncated.val = true
true
} else {
self.truncated.val
}
}
///|
fn ValidationState::push(
self : ValidationState,
code : DiagnosticCode,
message : String,
instance_path : JsonPointer,
schema_path : JsonPointer,
) -> Unit {
if self.errors.length() < self.options.max_errors {
self.errors.push(
Diagnostic::new(code, message, instance_path~, schema_path~),
)
} else {
self.truncated.val = true
}
}
///|
fn ValidationState::enter(
self : ValidationState,
depth : Int,
instance_path : JsonPointer,
schema_path : JsonPointer,
) -> Bool {
if self.full() {
return false
}
if depth > self.options.max_depth {
self.push(
ResourceLimitExceeded,
"instance depth exceeds configured limit " +
self.options.max_depth.to_string(),
instance_path,
schema_path,
)
self.truncated.val = true
return false
}
self.nodes.val += 1
if self.nodes.val > self.options.max_nodes {
self.push(
ResourceLimitExceeded,
"visited node count exceeds configured limit " +
self.options.max_nodes.to_string(),
instance_path,
schema_path,
)
self.truncated.val = true
return false
}
true
}
///|
fn contains_string(values : Array[String], target : String) -> Bool {
for value in values {
if value == target {
return true
}
}
false
}
///|
fn validate_type_form(
kind : JtdType,
value : Json,
instance_path : JsonPointer,
schema_path : JsonPointer,
state : ValidationState,
) -> Unit {
if !scalar_accepts(kind, value) {
state.push(
ValidationMismatch,
"expected " + scalar_expectation(kind) + ", found " + json_kind(value),
instance_path,
schema_path.property("type"),
)
}
}
///|
fn validate_enum_form(
values : Array[String],
value : Json,
instance_path : JsonPointer,
schema_path : JsonPointer,
state : ValidationState,
) -> Unit {
match value {
String(text) =>
if !contains_string(values, text) {
state.push(
ValidationMismatch,
"string '" + text + "' is not an allowed enum value",
instance_path,
schema_path.property("enum"),
)
}
_ =>
state.push(
ValidationMismatch,
"expected enum string, found " + json_kind(value),
instance_path,
schema_path.property("enum"),
)
}
}
///|
fn validate_elements_form(
document : SchemaDocument,
element : Schema,
value : Json,
instance_path : JsonPointer,
schema_path : JsonPointer,
depth : Int,
ref_depth : Int,
state : ValidationState,
) -> Unit {
guard value is Array(items) else {
state.push(
ValidationMismatch,
"expected array, found " + json_kind(value),
instance_path,
schema_path.property("elements"),
)
return
}
for index, item in items {
if state.full() {
return
}
validate_node(
document,
element,
item,
instance_path.index(index),
schema_path.property("elements"),
depth + 1,
ref_depth,
state,
)
}
}
///|
fn validate_values_form(
document : SchemaDocument,
value_schema : Schema,
value : Json,
instance_path : JsonPointer,
schema_path : JsonPointer,
depth : Int,
ref_depth : Int,
state : ValidationState,
) -> Unit {
guard value is Object(object) else {
state.push(
ValidationMismatch,
"expected object, found " + json_kind(value),
instance_path,
schema_path.property("values"),
)
return
}
for name, item in object {
if state.full() {
return
}
validate_node(
document,
value_schema,
item,
instance_path.property(name),
schema_path.property("values"),
depth + 1,
ref_depth,
state,
)
}
}
///|
fn validate_required_properties(
document : SchemaDocument,
required : Map[String, Schema],
object : Map[String, Json],
instance_path : JsonPointer,
schema_path : JsonPointer,
depth : Int,
ref_depth : Int,
state : ValidationState,
) -> Unit {
for name, property_schema in required {
if state.full() {
return
}
match object.get(name) {
None =>
state.push(
MissingProperty,
"missing required property '" + name + "'",
instance_path,
schema_path.property("properties").property(name),
)
Some(property_value) =>
validate_node(
document,
property_schema,
property_value,
instance_path.property(name),
schema_path.property("properties").property(name),
depth + 1,
ref_depth,
state,
)
}
}
}
///|
fn validate_optional_properties(
document : SchemaDocument,
optional : Map[String, Schema],
object : Map[String, Json],
instance_path : JsonPointer,
schema_path : JsonPointer,
depth : Int,
ref_depth : Int,
state : ValidationState,
) -> Unit {
for name, property_schema in optional {
if state.full() {
return
}
match object.get(name) {
None => ()
Some(property_value) =>
validate_node(
document,
property_schema,
property_value,
instance_path.property(name),
schema_path.property("optionalProperties").property(name),
depth + 1,
ref_depth,
state,
)
}
}
}
///|
fn validate_additional_properties(
required : Map[String, Schema],
optional : Map[String, Schema],
object : Map[String, Json],
ignored_property : String?,
instance_path : JsonPointer,
schema_path : JsonPointer,
state : ValidationState,
) -> Unit {
for name, _ in object {
if state.full() {
return
}
let ignored = match ignored_property {
Some(value) => value == name
None => false
}
if !ignored && !required.contains(name) && !optional.contains(name) {
state.push(
AdditionalProperty,
"additional property '" + name + "' is not allowed",
instance_path.property(name),
schema_path,
)
}
}
}
///|
fn validate_properties_object(
document : SchemaDocument,
required : Map[String, Schema],
optional : Map[String, Schema],
additional : Bool,
object : Map[String, Json],
ignored_property : String?,
instance_path : JsonPointer,
schema_path : JsonPointer,
depth : Int,
ref_depth : Int,
state : ValidationState,
) -> Unit {
validate_required_properties(
document, required, object, instance_path, schema_path, depth, ref_depth, state,
)
validate_optional_properties(
document, optional, object, instance_path, schema_path, depth, ref_depth, state,
)
if !additional {
validate_additional_properties(
required, optional, object, ignored_property, instance_path, schema_path, state,
)
}
}
///|
fn validate_properties_form(
document : SchemaDocument,
required : Map[String, Schema],
optional : Map[String, Schema],
additional : Bool,
value : Json,
instance_path : JsonPointer,
schema_path : JsonPointer,
depth : Int,
ref_depth : Int,
state : ValidationState,
) -> Unit {
guard value is Object(object) else {
let form_path = if !required.is_empty() || optional.is_empty() {
schema_path.property("properties")
} else {
schema_path.property("optionalProperties")
}
state.push(
ValidationMismatch,
"expected object, found " + json_kind(value),
instance_path,
form_path,
)
return
}
validate_properties_object(
document,
required,
optional,
additional,
object,
None,
instance_path,
schema_path,
depth,
ref_depth,
state,
)
}
///|
fn validate_discriminator_form(
document : SchemaDocument,
tag : String,
mapping : Map[String, Schema],
value : Json,
instance_path : JsonPointer,
schema_path : JsonPointer,
depth : Int,
ref_depth : Int,
state : ValidationState,
) -> Unit {
guard value is Object(object) else {
state.push(
ValidationMismatch,
"expected object for discriminator form, found " + json_kind(value),
instance_path,
schema_path.property("discriminator"),
)
return
}
let tag_value = match object.get(tag) {
None => {
state.push(
InvalidDiscriminatorTag,
"missing discriminator property '" + tag + "'",
instance_path,
schema_path.property("discriminator"),
)
return
}
Some(String(text)) => text
Some(other) => {
state.push(
InvalidDiscriminatorTag,
"discriminator property '" +
tag +
"' must be a string, found " +
json_kind(other),
instance_path.property(tag),
schema_path.property("discriminator"),
)
return
}
}
let branch = match mapping.get(tag_value) {
None => {
state.push(
UnknownDiscriminatorValue,
"unknown discriminator value '" + tag_value + "'",
instance_path.property(tag),
schema_path.property("mapping"),
)
return
}
Some(value) => value
}
match branch.form() {
PropertiesForm(required, optional, additional) =>
validate_properties_object(
document,
required,
optional,
additional,
object,
Some(tag),
instance_path,
schema_path.property("mapping").property(tag_value),
depth,
ref_depth,
state,
)
_ =>
state.push(
InvalidDiscriminatorMapping,
"selected discriminator mapping is not a properties schema",
instance_path,
schema_path.property("mapping").property(tag_value),
)
}
}
///|
fn validate_reference(
document : SchemaDocument,
name : String,
value : Json,
instance_path : JsonPointer,
depth : Int,
ref_depth : Int,
state : ValidationState,
) -> Unit {
if ref_depth >= state.options.max_ref_depth {
state.push(
ResourceLimitExceeded,
"reference depth exceeds configured limit " +
state.options.max_ref_depth.to_string(),
instance_path,
JsonPointer::root().property("definitions").property(name),
)
state.truncated.val = true
return
}
match document.definition(name) {
None =>
state.push(
UnknownReference,
"reference '" + name + "' is not defined",
instance_path,
JsonPointer::root().property("ref"),
)
Some(target) =>
validate_node(
document,
target,
value,
instance_path,
JsonPointer::root().property("definitions").property(name),
depth,
ref_depth + 1,
state,
)
}
}
///|
fn validate_node(
document : SchemaDocument,
schema : Schema,
value : Json,
instance_path : JsonPointer,
schema_path : JsonPointer,
depth : Int,
ref_depth : Int,
state : ValidationState,
) -> Unit {
if !state.enter(depth, instance_path, schema_path) {
return
}
if value is Null && schema.is_nullable() {
return
}
match schema.form() {
EmptyForm => ()
RefForm(name) =>
validate_reference(
document, name, value, instance_path, depth, ref_depth, state,
)
TypeForm(kind) =>
validate_type_form(kind, value, instance_path, schema_path, state)
EnumForm(values) =>
validate_enum_form(values, value, instance_path, schema_path, state)
ElementsForm(element) =>
validate_elements_form(
document, element, value, instance_path, schema_path, depth, ref_depth, state,
)
ValuesForm(value_schema) =>
validate_values_form(
document, value_schema, value, instance_path, schema_path, depth, ref_depth,
state,
)
PropertiesForm(required, optional, additional) =>
validate_properties_form(
document, required, optional, additional, value, instance_path, schema_path,
depth, ref_depth, state,
)
DiscriminatorForm(tag, mapping) =>
validate_discriminator_form(
document, tag, mapping, value, instance_path, schema_path, depth, ref_depth,
state,
)
}
}
///|
pub fn validate_with(
document : SchemaDocument,
value : Json,
options : ValidationOptions,
) -> ValidationReport {
if options.max_errors < 1 ||
options.max_depth < 0 ||
options.max_ref_depth < 1 ||
options.max_nodes < 1 {
let error = Diagnostic::new(
ResourceLimitExceeded,
"validation limits are invalid",
)
return { errors: [error], visited_nodes: 0, truncated: true, }
}
let state = ValidationState::new(options)
validate_node(
document,
document.root(),
value,
JsonPointer::root(),
JsonPointer::root(),
0,
0,
state,
)
{
errors: state.errors,
visited_nodes: state.nodes.val,
truncated: state.truncated.val,
}
}
///|
pub fn validate(document : SchemaDocument, value : Json) -> Array[Diagnostic] {
validate_with(document, value, ValidationOptions::default()).errors()
}
///|
pub fn validate_json_text(
document : SchemaDocument,
text : StringView,
) -> Result[Array[Diagnostic], Diagnostic] {
let value = @json.parse(text) catch {
error =>
return Err(
Diagnostic::new(
InvalidJson,
"invalid JSON instance: " + error.to_string(),
),
)
}
Ok(validate(document, value))
}