///|
/// Raise a decode error when `value` is outside `min..=max`.
fn check_int_range(
value : Int,
path : @json.JsonPath,
min~ : Int,
max~ : Int,
) -> Unit raise @json.JsonDecodeError {
guard value >= min && value <= max else {
raise @json.JsonDecodeError(
(path, "expected an integer in \{min}..=\{max}, got \{value}"),
)
}
}
///|
/// Raise a decode error when `value` is below `min`.
fn check_int_min(
value : Int,
path : @json.JsonPath,
min~ : Int,
) -> Unit raise @json.JsonDecodeError {
guard value >= min else {
raise @json.JsonDecodeError(
(path, "expected an integer >= \{min}, got \{value}"),
)
}
}
///|
/// Decode a required array member that the schema marks `minItems: 1`.
fn[T : @json.FromJson] non_empty_array_field(
obj : Map[String, Json],
key : String,
path : @json.JsonPath,
) -> Array[T] raise @json.JsonDecodeError {
let items : Array[T] = required_field(obj, key, path)
guard items.length() > 0 else {
raise @json.JsonDecodeError(
(path.add_key(key), "expected at least one item"),
)
}
items
}
///|
/// Decode a required integer member with a schema minimum.
fn int_field_min(
obj : Map[String, Json],
key : String,
path : @json.JsonPath,
min~ : Int,
) -> Int raise @json.JsonDecodeError {
let value : Int = required_field(obj, key, path)
check_int_min(value, path.add_key(key), min~)
value
}
///|
/// Decode an optional integer member with a schema minimum.
fn optional_int_field_min(
obj : Map[String, Json],
key : String,
path : @json.JsonPath,
min~ : Int,
) -> Int? raise @json.JsonDecodeError {
let value : Int? = optional_field(obj, key, path)
if value is Some(v) {
check_int_min(v, path.add_key(key), min~)
}
value
}
///|
/// Raise unless `json` is an object; return its members.
fn expect_object(
json : Json,
path : @json.JsonPath,
type_name : String,
) -> Map[String, Json] raise @json.JsonDecodeError {
guard json is Object(obj) else {
raise @json.JsonDecodeError(
(path, "\{type_name}::from_json: expected object"),
)
}
obj
}
///|
/// Decode the object member `key`, or raise when it is absent.
fn[T : @json.FromJson] required_field(
obj : Map[String, Json],
key : String,
path : @json.JsonPath,
) -> T raise @json.JsonDecodeError {
guard obj.get(key) is Some(value) else {
raise @json.JsonDecodeError((path, "Missing field \{key}"))
}
@json.from_json(value, path=path.add_key(key))
}
///|
/// Decode the object member `key`; absent or `null` gives `None`.
fn[T : @json.FromJson] optional_field(
obj : Map[String, Json],
key : String,
path : @json.JsonPath,
) -> T? raise @json.JsonDecodeError {
match obj.get(key) {
None | Some(Null) => None
Some(value) => Some(@json.from_json(value, path=path.add_key(key)))
}
}
///|
/// Decode an optional `argumentIndex` (schema range 1..=2).
fn argument_index_field(
obj : Map[String, Json],
path : @json.JsonPath,
) -> Int? raise @json.JsonDecodeError {
let index : Int? = optional_field(obj, "argumentIndex", path)
if index is Some(i) {
check_int_range(i, path.add_key("argumentIndex"), min=1, max=2)
}
index
}
///|
fn decode_fails(label : String, f : () -> Unit raise) -> Unit raise {
let failed = try f() catch {
_ => true
} noraise {
_ => false
}
assert_true(failed, msg="accepted \{label}")
}
///|
test "schema minimums and minItems are enforced" {
let cases : Array[(String, () -> Unit raise)] = [
(
"Location.line 0",
() => ignore((@json.from_json(@json.parse("{\"line\":0}")) : Location)),
),
(
"Location.column 0",
() => {
ignore(
(@json.from_json(@json.parse("{\"line\":1,\"column\":0}")) : Location),
)
},
),
(
"Group.start -1",
() => ignore((@json.from_json(@json.parse("{\"start\":-1}")) : Group)),
),
(
"TestCaseStarted.attempt -1",
() => {
ignore(
(
@json.from_json(
@json.parse(
"{\"attempt\":-1,\"id\":\"i\",\"testCaseId\":\"t\",\"timestamp\":{\"seconds\":1,\"nanos\":0}}",
),
) : TestCaseStarted),
)
},
),
(
"ParameterType.regularExpressions []",
() => {
ignore(
(
@json.from_json(
@json.parse(
"{\"id\":\"i\",\"name\":\"n\",\"preferForRegularExpressionMatch\":false,\"regularExpressions\":[],\"useForSnippets\":true}",
),
) : ParameterType),
)
},
),
(
"Pickle.astNodeIds []",
() => {
ignore(
(
@json.from_json(
@json.parse(
"{\"id\":\"p\",\"uri\":\"u\",\"name\":\"n\",\"language\":\"en\",\"steps\":[],\"tags\":[],\"astNodeIds\":[]}",
),
) : Pickle),
)
},
),
(
"PickleStep.astNodeIds []",
() => {
ignore(
(
@json.from_json(
@json.parse("{\"id\":\"s\",\"text\":\"t\",\"astNodeIds\":[]}"),
) : PickleStep),
)
},
),
(
"PickleTableRow.cells []",
() => {
ignore(
(@json.from_json(@json.parse("{\"cells\":[]}")) : PickleTableRow),
)
},
),
(
"Suggestion.snippets []",
() => {
ignore(
(
@json.from_json(
@json.parse(
"{\"id\":\"i\",\"pickleStepId\":\"p\",\"snippets\":[]}",
),
) : Suggestion),
)
},
),
]
for case_ in cases {
decode_fails(case_.0, case_.1)
}
}