///|
/// Non-fatal interpretation diagnostic required by RFC 9457 consumer semantics.
pub(all) enum ProblemDiagnosticKind {
IgnoredMemberWrongType
} derive(Eq, Debug)
///|
pub struct ProblemDiagnostic {
kind : ProblemDiagnosticKind
member_name : String
expected : String
actual : String
} derive(Eq, Debug)
///|
/// Successful parse plus non-fatal diagnostics.
pub struct ProblemParseResult {
problem : ProblemDetails
diagnostics : Array[ProblemDiagnostic]
} derive(Eq, Debug)
///|
pub fn ProblemDiagnostic::kind(
self : ProblemDiagnostic,
) -> ProblemDiagnosticKind {
self.kind
}
///|
pub fn ProblemDiagnostic::member_name(self : ProblemDiagnostic) -> String {
self.member_name
}
///|
pub fn ProblemDiagnostic::expected(self : ProblemDiagnostic) -> String {
self.expected
}
///|
pub fn ProblemDiagnostic::actual(self : ProblemDiagnostic) -> String {
self.actual
}
///|
pub fn ProblemParseResult::problem(self : ProblemParseResult) -> ProblemDetails {
self.problem
}
///|
pub fn ProblemParseResult::diagnostics(
self : ProblemParseResult,
) -> Array[ProblemDiagnostic] {
self.diagnostics.copy()
}
///|
pub fn parse_problem_json(
input : String,
) -> Result[ProblemParseResult, ProblemError] {
parse_problem_json_with_limits(input, Limits::default())
}
///|
/// Byte-oriented entry point for network payloads and truncation testing.
pub fn parse_problem_json_bytes(
input : Bytes,
limits? : Limits = Limits::default(),
) -> Result[ProblemParseResult, ProblemError] {
if input.length() > limits.max_input_bytes() {
return Err(
problem_error(Input, LimitExceeded, "input exceeds max_input_bytes"),
)
}
let text = @utf8.decode(input) catch {
_ =>
return Err(problem_error(Input, InvalidUtf8, "input is not valid UTF-8"))
}
parse_problem_json_with_limits(text, limits)
}
///|
/// Parse JSON using core's parser, then interpret RFC 9457 members.
pub fn parse_problem_json_with_limits(
input : String,
limits : Limits,
) -> Result[ProblemParseResult, ProblemError] {
let input_bytes = utf8_length(input)
if input_bytes > limits.max_input_bytes() {
return Err(
problem_error(Input, LimitExceeded, "input exceeds max_input_bytes"),
)
}
let max_nesting_depth = limits.max_nesting_depth()
let root = @json.parse(input, max_nesting_depth~) catch {
_ => return Err(problem_error(Json, InvalidJson, "invalid JSON document"))
}
let members = match json_object(root) {
Some(value) => value
None =>
return Err(
problem_error(
Json,
RootNotObject,
"top-level JSON value must be an object",
),
)
}
if members.length() > limits.max_members() {
return Err(
problem_error(Limit, LimitExceeded, "object exceeds max_members"),
)
}
let diagnostics : Array[ProblemDiagnostic] = []
let mut type_raw : String? = None
let mut title : String? = None
let mut status : Int? = None
let mut detail : String? = None
let mut instance : String? = None
let extensions : Array[ExtensionMember] = []
let keys = map_keys(members)
keys.sort()
for key in keys {
let value = members[key]
match key {
"type" =>
match json_string(value) {
Some(text) => type_raw = Some(text)
None => diagnostics.push(wrong_type(key, "string", value))
}
"title" =>
match json_string(value) {
Some(text) => title = Some(text)
None => diagnostics.push(wrong_type(key, "string", value))
}
"status" =>
match json_int(value) {
Some(code) => status = Some(code)
None => diagnostics.push(wrong_type(key, "integer number", value))
}
"detail" =>
match json_string(value) {
Some(text) => detail = Some(text)
None => diagnostics.push(wrong_type(key, "string", value))
}
"instance" =>
match json_string(value) {
Some(text) => instance = Some(text)
None => diagnostics.push(wrong_type(key, "string", value))
}
_ => extensions.push(extension_member(key, value))
}
}
if extensions.length() > limits.max_extension_members() {
return Err(
problem_error(
Limit,
LimitExceeded,
"object exceeds max_extension_members",
),
)
}
diagnostics.sort_by(fn(a, b) { lexical_compare(a.member_name, b.member_name) })
let parsed = problem_details(
type_raw, title, status, detail, instance, extensions,
)
check_parsed_limits(parsed, limits) catch {
error => return Err(unwrap_problem_error(error))
}
Ok({ problem: parsed, diagnostics })
}
///|
/// Convenience API preserving errors while discarding diagnostics.
pub fn parse_problem(input : String) -> Result[ProblemDetails, ProblemError] {
match parse_problem_json(input) {
Ok(parsed) => Ok(parsed.problem())
Err(error) => Err(error)
}
}
///|
fn wrong_type(
member_name : String,
expected : String,
value : Json,
) -> ProblemDiagnostic {
{
kind: IgnoredMemberWrongType,
member_name,
expected,
actual: json_kind(value),
}
}
///|
/// Bounded traversal after parse protects strings and aggregate shape.
fn check_parsed_limits(
problem : ProblemDetails,
limits : Limits,
) -> Unit raise ProblemError {
match problem.title() {
Some(text) if utf8_length(text) > limits.max_title_bytes() =>
raise problem_error(Limit, LimitExceeded, "title exceeds max_title_bytes")
_ => ()
}
match problem.detail() {
Some(text) if utf8_length(text) > limits.max_detail_bytes() =>
raise problem_error(
Limit,
LimitExceeded,
"detail exceeds max_detail_bytes",
)
_ => ()
}
match problem.type_raw() {
Some(text) => check_string_limit(text, limits)
None => ()
}
match problem.instance() {
Some(text) => check_string_limit(text, limits)
None => ()
}
for extension in problem.extensions() {
check_string_limit(extension.name(), limits)
check_json_value(extension.value(), limits, 1)
}
}
///|
fn check_string_limit(
text : String,
limits : Limits,
) -> Unit raise ProblemError {
if utf8_length(text) > limits.max_string_bytes() {
raise problem_error(Limit, LimitExceeded, "string exceeds max_string_bytes")
}
}
///|
fn check_json_value(
value : Json,
limits : Limits,
depth : Int,
) -> Unit raise ProblemError {
if depth > limits.max_nesting_depth() {
raise problem_error(
Limit,
LimitExceeded,
"extension exceeds max_nesting_depth",
)
}
match value {
String(text) => check_string_limit(text, limits)
Array(values) => {
if values.length() > limits.max_members() {
raise problem_error(Limit, LimitExceeded, "array exceeds max_members")
}
for child in values {
check_json_value(child, limits, depth + 1)
}
}
Object(members) => {
if members.length() > limits.max_members() {
raise problem_error(Limit, LimitExceeded, "object exceeds max_members")
}
for key in members.keys() {
check_string_limit(key, limits)
check_json_value(members[key], limits, depth + 1)
}
}
_ => ()
}
}