use super::data::{Input, Output};
use super::description::{
Parameters, Property, PropertyType, PropertyTypeDef, ServerDescription, ToolDescription,
};
use super::err::{ContentType, DeserializationError};
use super::parser::json_value::{LocatedString, LocatedValue};
use linked_hash_map::LinkedHashMap;
use smol_str::SmolStr;
use std::collections::{HashMap, HashSet};
#[expect(clippy::needless_pass_by_value, reason = "Better interface.")]
pub(crate) fn server_description_from_json_value(
json_value: LocatedValue,
) -> Result<ServerDescription, DeserializationError> {
if json_value.is_object() {
match json_value.get("result") {
Some(result) => {
let tools_json = result.get("tools").ok_or_else(|| {
DeserializationError::missing_attribute(result, "tools", Vec::new())
})?;
let tools = tools_json.get_array().ok_or_else(|| DeserializationError::unexpected_type(
tools_json,
"Expected `tools` attribute of MCP tool_list response to be an array of MCP tool descriptions.",
ContentType::ServerDescription
))?;
Ok(ServerDescription::new(
tools
.iter()
.map(tool_description_from_json_value_inner)
.collect::<Result<Vec<_>, _>>()?
.into_iter(),
typedefs_from_json_value(result.get("$defs"), ContentType::ToolParameters)?,
))
}
None => Ok(ServerDescription::new(
std::iter::once(tool_description_from_json_value_inner(&json_value)?),
HashMap::new(),
)),
}
} else {
let tools = json_value.get_array().ok_or_else(|| DeserializationError::unexpected_type(
&json_value,
"Expected either a JSON object containing an MCP list_tool response, a JSON array of tool descriptions, or a JSON object describing a single MCP tool.",
ContentType::ServerDescription
))?;
Ok(ServerDescription::new(
tools
.iter()
.map(tool_description_from_json_value_inner)
.collect::<Result<Vec<_>, _>>()?
.into_iter(),
HashMap::new(),
))
}
}
#[expect(clippy::needless_pass_by_value, reason = "Better interface.")]
pub(crate) fn tool_description_from_json_value(
json_value: LocatedValue,
) -> Result<ToolDescription, DeserializationError> {
tool_description_from_json_value_inner(&json_value)
}
fn tool_description_from_json_value_inner(
json_value: &LocatedValue,
) -> Result<ToolDescription, DeserializationError> {
let tool_obj = json_value.get_object().ok_or_else(|| {
DeserializationError::unexpected_type(
json_value,
"Expected a JSON object containing an MCP tool description.",
ContentType::ToolDescription,
)
})?;
let name = tool_obj
.get("name")
.ok_or_else(|| DeserializationError::missing_attribute(json_value, "name", Vec::new()))?;
let name = name.get_smolstr().ok_or_else(|| {
DeserializationError::unexpected_type(
name,
"Expected `name` attribute of a MCP tool description to be a String.",
ContentType::ToolDescription,
)
})?;
let inputs = get_value_from_map(tool_obj, &["parameters", "inputSchema"]).ok_or_else(|| {
DeserializationError::missing_attribute(
json_value,
"parameters",
vec!["inputSchema".to_string()],
)
})?;
let inputs = parameters_from_json_value(inputs)?;
let outputs = tool_obj
.get("outputSchema")
.map(parameters_from_json_value)
.transpose()?
.unwrap_or_else(|| Parameters::new(Vec::new(), HashMap::new()));
let type_defs = typedefs_from_json_value(tool_obj.get("$defs"), ContentType::ToolParameters)?;
let description = tool_obj
.get("description")
.map(|json| {
json.get_string().ok_or_else(|| {
DeserializationError::unexpected_type(
json,
"Expected `description` attribute of a MCP Tool Description to be a string.",
ContentType::ToolDescription,
)
})
})
.transpose()?;
Ok(ToolDescription::new(
name,
inputs,
outputs,
type_defs,
description,
))
}
fn parameters_from_json_value(
json_value: &LocatedValue,
) -> Result<Parameters, DeserializationError> {
let json_value = json_value.get("json").unwrap_or(json_value);
let params_obj = json_value.get_object().ok_or_else(|| {
DeserializationError::unexpected_type(
json_value,
"Expected Input/Output schema of a MCP Tool Description to be a JSON object.",
ContentType::ToolParameters,
)
})?;
let type_defs = typedefs_from_json_value(params_obj.get("$defs"), ContentType::ToolParameters)?;
let required =
required_from_json_value(params_obj.get("required"), ContentType::ToolParameters)?;
let properties = properties_from_json_value(
params_obj.get("properties"),
&required,
ContentType::ToolParameters,
)?;
Ok(Parameters::new(properties, type_defs))
}
fn typedefs_from_json_value(
json_value: Option<&LocatedValue>,
content_type: ContentType,
) -> Result<HashMap<SmolStr, PropertyTypeDef>, DeserializationError> {
let type_defs = json_value.map(|json_value| {
let defs = json_value.get_object().ok_or_else(|| DeserializationError::unexpected_type(
json_value,
"Expected attribute `$defs` to be a JSON object mapping type names to JSON Type Schemas.",
content_type
))?;
defs.iter()
.map(|(name, val)| {
let name = name.to_smolstr();
let description = val.get("description").and_then(|desc| desc.get_string());
property_type_from_json_value(val).map(|ptype| {
(name.clone(), PropertyTypeDef::new(name, ptype, description))
})
})
.collect::<Result<_, _>>()
}).unwrap_or_else(|| Ok(HashMap::new()))?;
typedefs_are_well_founded(&type_defs)?;
Ok(type_defs)
}
fn required_from_json_value(
json_value: Option<&LocatedValue>,
content_type: ContentType,
) -> Result<HashSet<SmolStr>, DeserializationError> {
json_value
.map(|json_value| {
if let Some(reqs) = json_value.get_array() {
reqs.iter()
.map(|req| {
req.get_smolstr().ok_or_else(|| {
DeserializationError::unexpected_type(
req,
"Expected element of `required` to be a string.",
content_type,
)
})
})
.collect::<Result<_, _>>()
} else if json_value.get_bool() == Some(false) {
Ok(HashSet::new())
} else {
Err(DeserializationError::unexpected_type(
json_value,
"Expected `required` attribute to be a JSON array of strings.",
content_type,
))
}
})
.unwrap_or_else(|| Ok(HashSet::new()))
}
fn properties_from_json_value(
json_value: Option<&LocatedValue>,
required: &HashSet<SmolStr>,
content_type: ContentType,
) -> Result<Vec<Property>, DeserializationError> {
json_value
.map(|json_value| {
if let Some(props_obj) = json_value.get_object() {
props_obj
.iter()
.map(|(name, ptype_json)| {
let name = name.to_smolstr();
let required = required.contains(&name);
property_from_json_value(ptype_json, name, required)
})
.collect::<Result<_, _>>()
} else if json_value.get_bool() == Some(false) {
Ok(Vec::new())
} else {
Err(DeserializationError::unexpected_type(
json_value,
"Expected `properties` attribute to be a JSON object.",
content_type,
))
}
})
.unwrap_or_else(|| Ok(Vec::new()))
}
fn property_from_json_value(
json_value: &LocatedValue,
name: SmolStr,
required: bool,
) -> Result<Property, DeserializationError> {
let description = json_value
.get("description")
.map(|desc_json| {
desc_json.get_string().ok_or_else(|| {
DeserializationError::unexpected_type(
desc_json,
"Expected `description` attribute to be a string.",
ContentType::Property,
)
})
})
.transpose()?;
Ok(Property::new(
name,
required,
property_type_from_json_value(json_value)?,
description,
))
}
fn property_type_from_json_value(
json_value: &LocatedValue,
) -> Result<PropertyType, DeserializationError> {
let ptype_obj = json_value.get_object().ok_or_else(|| {
DeserializationError::unexpected_type(
json_value,
"Expected Property Type Schema to be a JSON object.",
ContentType::Property,
)
})?;
if let Some(type_json) = ptype_obj.get("type") {
match type_json.get_str() {
Some("boolean") => Ok(PropertyType::Bool),
Some("integer") => Ok(PropertyType::Integer),
Some("float") => Ok(PropertyType::Float),
Some("number") => Ok(PropertyType::Number),
Some("string") => {
if let Some(enum_json) = ptype_obj.get("enum") {
enum_from_json_value(enum_json)
} else if let Some(format_json) = ptype_obj.get("format") {
property_type_of_format(format_json)
} else {
Ok(PropertyType::String)
}
}
Some("null") => Ok(PropertyType::Null),
Some("array") => property_type_from_json_array_def(ptype_obj),
Some("object") => {
let required = required_from_json_value(ptype_obj.get("required"), ContentType::ToolParameters)?;
let properties = properties_from_json_value(ptype_obj.get("properties"), &required, ContentType::ToolParameters)?;
let additional_properties = additional_properties_from_map(ptype_obj)?;
Ok(PropertyType::Object { properties, additional_properties })
}
Some(_) => Err(DeserializationError::unexpected_value(
type_json,
"Expected one of: `boolean`, `integer`, `float`, `number`, `string`, `null`, `array`, `object`.",
ContentType::PropertyType
)),
None => union_type_of_json_value(type_json, ptype_obj)
}
} else if let Some(union_json) = get_value_from_map(ptype_obj, &["anyOf", "oneOf"]) {
let typ_arr = union_json.get_array().ok_or_else(|| {
DeserializationError::unexpected_type(
json_value,
"Expected `anyOf` or `oneOf` attribute to be an array of JSON Schemas.",
ContentType::PropertyType,
)
})?;
let types = typ_arr
.iter()
.map(property_type_from_json_value)
.collect::<Result<_, _>>()?;
Ok(PropertyType::Union { types })
} else if let Some(ref_json) = ptype_obj.get("$ref") {
let s = ref_json.get_str().ok_or_else(|| {
DeserializationError::unexpected_type(
ref_json,
"Expected `$ref` attribute to be a string.",
ContentType::PropertyType,
)
})?;
let s = s.strip_prefix("#/$defs/").ok_or_else(|| {
DeserializationError::unexpected_value(
ref_json,
"Expected `$ref` attribute to begin with `#/$defs/`",
ContentType::Property,
)
})?;
Ok(PropertyType::Ref { name: s.into() })
} else {
Ok(PropertyType::Unknown)
}
}
fn additional_properties_from_map(
map: &LinkedHashMap<LocatedString, LocatedValue>,
) -> Result<Option<Box<PropertyType>>, DeserializationError> {
match map.get("additionalProperties") {
Some(json) if json.is_bool() || json.is_null() => Ok(None),
Some(json) => Ok(Some(Box::new(property_type_from_json_value(json)?))),
None => Ok(None),
}
}
fn property_type_from_json_array_def(
ptype_obj: &LinkedHashMap<LocatedString, LocatedValue>,
) -> Result<PropertyType, DeserializationError> {
let prefix_items = ptype_obj.get("prefixItems").and_then(|v| v.get_array());
let items_json = ptype_obj.get("items");
if let Some(prefix) = prefix_items {
let prefix_types: Vec<PropertyType> = prefix
.iter()
.map(property_type_from_json_value)
.collect::<Result<_, _>>()?;
match items_json {
Some(items) if items.get_bool() == Some(false) => Ok(PropertyType::Tuple {
types: prefix_types,
}),
Some(items) if items.is_object() => {
let items_type = property_type_from_json_value(items)?;
if prefix_types.iter().all(|t| *t == items_type) {
Ok(PropertyType::Array {
element_ty: Box::new(items_type),
})
} else {
Ok(PropertyType::Array {
element_ty: Box::new(PropertyType::Unknown),
})
}
}
_ => Ok(PropertyType::Array {
element_ty: Box::new(PropertyType::Unknown),
}),
}
} else {
items_json.map(|items_json| {
if items_json.is_object() {
let items_type = property_type_from_json_value(items_json)?;
Ok(PropertyType::Array { element_ty: Box::new(items_type) })
} else if items_json.is_bool() || items_json.is_null() {
Ok(PropertyType::Array { element_ty: Box::new(PropertyType::Unknown) })
} else {
Err(DeserializationError::unexpected_type(
items_json,
"Expected `items` attribute to be a JSON Schema (object) describing the type of array items.",
ContentType::PropertyType
))
}
}).unwrap_or_else(|| Ok(PropertyType::Array { element_ty: Box::new(PropertyType::Unknown) }))
}
}
fn enum_from_json_value(enum_json: &LocatedValue) -> Result<PropertyType, DeserializationError> {
let enum_variants = enum_json.get_array().ok_or_else(|| {
DeserializationError::unexpected_type(
enum_json,
"Expected `enum` attributed to be a JSON array of strings.",
ContentType::PropertyType,
)
})?;
let variants = enum_variants
.iter()
.map(|variant| {
variant.get_smolstr().ok_or_else(|| {
DeserializationError::unexpected_type(
variant,
"Expected element of `enum` attribute to be a string.",
ContentType::PropertyType,
)
})
})
.collect::<Result<Vec<_>, _>>()?;
if variants.is_empty() {
Err(DeserializationError::unexpected_value(
enum_json,
"Expected non-empty list of variants for `enum` attribute.",
ContentType::PropertyType,
))
} else {
Ok(PropertyType::Enum { variants })
}
}
fn property_type_of_format(
format_json: &LocatedValue,
) -> Result<PropertyType, DeserializationError> {
match format_json.get_str() {
Some("date") => Ok(PropertyType::Datetime),
Some("date-time") => Ok(PropertyType::Datetime),
Some("duration") => Ok(PropertyType::Duration),
Some("ipv4") => Ok(PropertyType::IpAddr),
Some("ipv6") => Ok(PropertyType::IpAddr),
Some("decimal") => Ok(PropertyType::Decimal),
Some(_) => Ok(PropertyType::String),
None => Err(DeserializationError::unexpected_type(
format_json,
"Expected `format` attribute to be a string.",
ContentType::PropertyType,
)),
}
}
fn union_type_of_json_value(
type_json: &LocatedValue,
top_typ: &LinkedHashMap<LocatedString, LocatedValue>,
) -> Result<PropertyType, DeserializationError> {
if let Some(types_json) = type_json.get_array() {
let types = types_json
.iter()
.map(|ty_json| tuple_type_element_of_json_value_array_element(ty_json, top_typ))
.collect::<Result<_, _>>()?;
Ok(PropertyType::Union { types })
} else if type_json.is_bool() || type_json.is_null() {
Ok(PropertyType::Unknown)
} else {
Err(DeserializationError::unexpected_type(
type_json,
"Expected `type` attribute to be a string.",
ContentType::PropertyType,
))
}
}
fn tuple_type_element_of_json_value_array_element(
ty_json: &LocatedValue,
top_typ: &LinkedHashMap<LocatedString, LocatedValue>,
) -> Result<PropertyType, DeserializationError> {
match ty_json.get_str() {
Some("boolean") => Ok(PropertyType::Bool),
Some("integer") => Ok(PropertyType::Integer),
Some("float") => Ok(PropertyType::Float),
Some("number") => Ok(PropertyType::Number),
Some("string") => Ok(PropertyType::String),
Some("null") => Ok(PropertyType::Null),
Some("object") => {
let required = required_from_json_value(top_typ.get("required"), ContentType::ToolParameters)?;
let properties = properties_from_json_value(top_typ.get("properties"), &required, ContentType::ToolParameters)?;
let additional_properties = additional_properties_from_map(top_typ)?;
Ok(PropertyType::Object { properties, additional_properties })
}
Some("array") => property_type_from_json_array_def(top_typ),
Some(_) => Err(DeserializationError::unexpected_value(
ty_json,
"Expected one of: `boolean`, `integer`, `float`, `number`, `string`, `null`, `array`, `object`.",
ContentType::PropertyType
)),
None => property_type_from_json_value(ty_json)
}
}
fn get_value_from_map<'a, T: AsRef<str>>(
map: &'a LinkedHashMap<LocatedString, LocatedValue>,
key_aliases: &[T],
) -> Option<&'a LocatedValue> {
key_aliases.iter().find_map(|key| map.get(key.as_ref()))
}
pub(crate) fn mcp_tool_input_from_json_value(
json_value: &LocatedValue,
) -> Result<Input, DeserializationError> {
let obj = json_value.get_object().ok_or_else(|| {
DeserializationError::unexpected_type(
json_value,
"MCP `tools/call` request should be an object",
ContentType::ToolInputRequest,
)
})?;
let params = obj
.get("params")
.ok_or_else(|| DeserializationError::missing_attribute(json_value, "params", vec![]))?;
let params_obj = params.get_object().ok_or_else(|| {
DeserializationError::unexpected_type(
json_value,
"MCP `tools/call` request \"params\" attribute should be an object",
ContentType::ToolInputRequest,
)
})?;
let tool = params_obj
.get("tool")
.ok_or_else(|| DeserializationError::missing_attribute(params, "tool", vec![]))?;
let tool = tool.get_smolstr().ok_or_else(|| {
DeserializationError::unexpected_type(
tool,
"Expected \"tool\" attribute to be a string",
ContentType::ToolInputRequest,
)
})?;
let args = params_obj
.get("args")
.ok_or_else(|| DeserializationError::missing_attribute(params, "args", vec![]))?;
let args = args.get_object().ok_or_else(|| {
DeserializationError::unexpected_type(
args,
"Expected \"args\" attribute to be an object",
ContentType::ToolInputRequest,
)
})?;
let args = args
.iter()
.map(|(k, v)| (k.to_smolstr(), v.clone()))
.collect();
Ok(Input { name: tool, args })
}
pub(crate) fn mcp_tool_output_from_json_value(
json_value: &LocatedValue,
) -> Result<Output, DeserializationError> {
let obj = json_value.get_object().ok_or_else(|| {
DeserializationError::unexpected_type(
json_value,
"MCP `tools/call` response should be an object",
ContentType::ToolOutputResponse,
)
})?;
let result = obj
.get("result")
.ok_or_else(|| DeserializationError::missing_attribute(json_value, "result", vec![]))?;
let result_obj = result.get_object().ok_or_else(|| {
DeserializationError::unexpected_type(
result,
"MCP `tools/call` response \"result\" attribute should be an object",
ContentType::ToolOutputResponse,
)
})?;
let content = result_obj.get("structuredContent").ok_or_else(|| {
DeserializationError::missing_attribute(result, "structuredContent", vec![])
})?;
let results = content.get_object().ok_or_else(|| {
DeserializationError::unexpected_type(
content,
"MCP `tools/call` response `\"structuredContent\"` is expected to be an object",
ContentType::ToolOutputResponse,
)
})?;
let results = results
.iter()
.map(|(k, v)| (k.to_smolstr(), v.clone()))
.collect();
Ok(Output { results })
}
fn typedefs_are_well_founded(
type_defs: &HashMap<SmolStr, PropertyTypeDef>,
) -> Result<(), DeserializationError> {
for (name, ty_def) in type_defs {
let mut cycle = vec![name.clone()];
let mut ty_def = ty_def;
while let PropertyType::Ref { name } = ty_def.property_type() {
if cycle.contains(name) {
cycle.push(name.clone());
return Err(DeserializationError::type_definition_cycle(cycle));
}
match type_defs.get(name) {
Some(tdef) => {
cycle.push(name.clone());
ty_def = tdef
}
_ => break,
}
}
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use crate::parser::json_parser::JsonParser;
use cool_asserts::assert_matches;
fn parse_property_type(json: &str) -> Result<PropertyType, DeserializationError> {
let mut parser = JsonParser::new(json);
let value = parser.get_value().expect("JSON should parse");
property_type_from_json_value(&value)
}
#[test]
fn test_property_type_simple_object() {
let result = parse_property_type(
r#"{"type": "object", "properties": {"name": {"type": "string"}}}"#,
);
assert_matches!(result, Ok(PropertyType::Object { .. }));
}
#[test]
fn test_property_type_simple_array() {
let result = parse_property_type(r#"{"type": "array", "items": {"type": "integer"}}"#);
assert_matches!(
result,
Ok(PropertyType::Array { element_ty }) if matches!(*element_ty, PropertyType::Integer)
);
}
#[test]
fn test_property_type_primitive_type_array() {
let result = parse_property_type(r#"{"type": ["null", "string"]}"#);
assert_matches!(result, Ok(PropertyType::Union { types }) if types.len() == 2);
}
#[test]
fn test_property_tuple() {
let tuple_cases = vec![
(
r#"{"type": "array", "prefixItems": [], "items": false}"#,
PropertyType::Tuple { types: vec![] },
),
(
r#"{"type": "array", "prefixItems": [{"type": "string"}], "items": false}"#,
PropertyType::Tuple {
types: vec![PropertyType::String],
},
),
(
r#"{"type": "array", "prefixItems": [{"type": "integer"}, {"type": "string"}], "items": false}"#,
PropertyType::Tuple {
types: vec![PropertyType::Integer, PropertyType::String],
},
),
(
r#"{"type": "array", "prefixItems": [{"type": "null"}, {"type": "string"}], "items": false}"#,
PropertyType::Tuple {
types: vec![PropertyType::Null, PropertyType::String],
},
),
(
r#"{"type": "array", "prefixItems": [{"type": "string"}, {"type": "object", "properties": {"x": {"type": "integer"}}}], "items": false}"#,
PropertyType::Tuple {
types: vec![
PropertyType::String,
PropertyType::Object {
properties: vec![Property::new(
"x".into(),
false,
PropertyType::Integer,
None,
)],
additional_properties: None,
},
],
},
),
(
r#"{"type": "array", "prefixItems": [{"type": "array", "items": {"type": "boolean"}}], "items": false}"#,
PropertyType::Tuple {
types: vec![PropertyType::Array {
element_ty: Box::new(PropertyType::Bool),
}],
},
),
(
r#"{"type": "array", "prefixItems": [{"type": "integer"}, {"type": "integer"}, {"type": "integer"}], "items": false}"#,
PropertyType::Tuple {
types: vec![
PropertyType::Integer,
PropertyType::Integer,
PropertyType::Integer,
],
},
),
(
r#"{"type": "array", "prefixItems": [{"type": "string", "enum": ["a", "b"]}, {"type": "number"}], "items": false}"#,
PropertyType::Tuple {
types: vec![
PropertyType::Enum {
variants: vec!["a".into(), "b".into()],
},
PropertyType::Number,
],
},
),
];
let non_tuple_cases = vec![
(
r#"{"type": "array", "prefixItems": [{"type": "null"}, {"type": "string"}]}"#,
PropertyType::Array {
element_ty: Box::new(PropertyType::Unknown),
},
),
(
r#"{"type": "array", "prefixItems": [{"type": "integer"}], "items": true}"#,
PropertyType::Array {
element_ty: Box::new(PropertyType::Unknown),
},
),
(
r#"{"type": "array", "items": {"type": "string"}}"#,
PropertyType::Array {
element_ty: Box::new(PropertyType::String),
},
),
(
r#"{"type": "array", "prefixItems": [{"type": "integer"}], "items": {"type": "string"}}"#,
PropertyType::Array {
element_ty: Box::new(PropertyType::Unknown),
},
),
(
r#"{"type": "array", "prefixItems": [{"type": "string"}, {"type": "string"}], "items": {"type": "string"}}"#,
PropertyType::Array {
element_ty: Box::new(PropertyType::String),
},
),
];
for (json, expected) in tuple_cases.iter().chain(non_tuple_cases.iter()) {
let result = parse_property_type(json).unwrap();
assert_eq!(result, *expected, "Failed for input: {json}");
}
}
#[test]
fn test_property_type_string_with_enum() {
let result = parse_property_type(r#"{"type": "string", "enum": ["a", "b", "c"]}"#);
assert_matches!(result, Ok(PropertyType::Enum { variants }) if variants.len() == 3);
}
#[test]
fn test_property_type_anyof_union() {
let result = parse_property_type(r#"{"anyOf": [{"type": "string"}, {"type": "integer"}]}"#);
assert_matches!(result, Ok(PropertyType::Union { types }) if types.len() == 2);
}
#[test]
fn test_property_type_ref() {
let result = parse_property_type(r##"{"$ref": "#/$defs/MyType"}"##);
assert_matches!(result, Ok(PropertyType::Ref { name }) if name == "MyType");
}
#[test]
fn test_union_type_array_with_nested_objects() {
let json = r#"{
"type": ["null", "object"],
"properties": {
"enabled": {
"type": ["null", "boolean"],
"description": "Whether the alert is currently active"
},
"groupByKeys": {
"type": ["null", "array"],
"items": { "type": "string" }
},
"phantomMode": {
"type": ["null", "boolean"]
},
"activeOn": {
"type": ["null", "object"],
"properties": {
"dayOfWeek": { "type": ["null", "array"], "items": {"type": "string"} },
"startTime": {
"type": ["null", "object"],
"properties": {
"hours": { "type": ["null", "integer"] },
"minutes": { "type": ["null", "integer"] }
}
}
}
}
}
}"#;
let mut parser = JsonParser::new(json);
let value = parser.get_value().expect("JSON should parse");
let result = property_type_from_json_value(&value);
assert!(result.is_ok(), "Expected successful parse, got: {result:?}");
}
#[test]
fn test_additional_properties_boolean_false() {
let result = parse_property_type(
r#"{"type": "object", "properties": {"x": {"type": "string"}}, "additionalProperties": false}"#,
);
assert_matches!(
result,
Ok(PropertyType::Object {
additional_properties: None,
..
})
);
}
#[test]
fn test_additional_properties_boolean_true() {
let result = parse_property_type(
r#"{"type": "object", "properties": {}, "additionalProperties": true}"#,
);
assert_matches!(
result,
Ok(PropertyType::Object {
additional_properties: None,
..
})
);
}
#[test]
fn test_additional_properties_valid_schema() {
let result = parse_property_type(
r#"{"type": "object", "properties": {}, "additionalProperties": {"type": "integer"}}"#,
);
assert_matches!(
result,
Ok(PropertyType::Object { additional_properties: Some(ty), .. }) if matches!(*ty, PropertyType::Integer)
);
}
#[test]
fn test_additional_properties_malformed_schema_errors() {
let result = parse_property_type(
r#"{"type": "object", "properties": {}, "additionalProperties": {"type": "bogus"}}"#,
);
assert_matches!(result, Err(_));
}
#[test]
fn test_additional_properties_absent() {
let result =
parse_property_type(r#"{"type": "object", "properties": {"x": {"type": "string"}}}"#);
assert_matches!(
result,
Ok(PropertyType::Object {
additional_properties: None,
..
})
);
}
#[test]
fn test_type_array_union_with_tuple() {
let result = parse_property_type(
r#"{"type": ["null", "array"], "prefixItems": [{"type": "string"}, {"type": "integer"}], "items": false}"#,
);
assert_matches!(
result,
Ok(PropertyType::Union { types }) if types.len() == 2
&& matches!(types[0], PropertyType::Null)
&& matches!(types[1], PropertyType::Tuple { ref types } if types.len() == 2)
);
}
}