use std::collections::BTreeMap; use crank_schema::{Schema, SchemaKind}; use serde_json::Value; pub fn any_object(description: Option) -> Schema { Schema { kind: SchemaKind::Object, description, required: true, nullable: false, default_value: None, fields: BTreeMap::new(), items: None, enum_values: Vec::new(), variants: Vec::new(), } } pub fn field_count(schema: &Schema) -> usize { match schema.kind { SchemaKind::Object => schema.fields.len(), _ => 0, } } pub fn schema_from_openapi( value: Option<&Value>, required: bool, description: Option, ) -> Schema { let Some(value) = value else { return primitive(SchemaKind::String, required, description); }; let resolved = collapse_composition(value); if let Some(values) = resolved.get("enum").and_then(Value::as_array) { let enum_values = values .iter() .filter_map(Value::as_str) .map(ToOwned::to_owned) .collect::>(); if !enum_values.is_empty() { return Schema { kind: SchemaKind::Enum, description: description.or_else(|| text(&resolved, "description")), required, nullable: nullable(&resolved), default_value: resolved.get("default").cloned(), fields: BTreeMap::new(), items: None, enum_values, variants: Vec::new(), }; } } match type_name(&resolved).unwrap_or("object") { "object" => object_schema(&resolved, required, description), "array" => Schema { kind: SchemaKind::Array, description: description.or_else(|| text(&resolved, "description")), required, nullable: nullable(&resolved), default_value: resolved.get("default").cloned(), fields: BTreeMap::new(), items: Some(Box::new(schema_from_openapi( resolved.get("items"), true, None, ))), enum_values: Vec::new(), variants: Vec::new(), }, "integer" => primitive( SchemaKind::Integer, required, description.or_else(|| text(&resolved, "description")), ), "number" => primitive( SchemaKind::Number, required, description.or_else(|| text(&resolved, "description")), ), "boolean" => primitive( SchemaKind::Boolean, required, description.or_else(|| text(&resolved, "description")), ), "null" => primitive( SchemaKind::Null, required, description.or_else(|| text(&resolved, "description")), ), _ => primitive( SchemaKind::String, required, description.or_else(|| text(&resolved, "description")), ), } } pub fn object_with_fields(description: Option, fields: BTreeMap) -> Schema { Schema { kind: SchemaKind::Object, description, required: true, nullable: false, default_value: None, fields, items: None, enum_values: Vec::new(), variants: Vec::new(), } } fn object_schema(value: &Value, required: bool, description: Option) -> Schema { let required_fields = value .get("required") .and_then(Value::as_array) .map(|items| { items .iter() .filter_map(Value::as_str) .collect::>() }) .unwrap_or_default(); let mut fields = BTreeMap::new(); if let Some(properties) = value.get("properties").and_then(Value::as_object) { for (name, schema) in properties { fields.insert( name.clone(), schema_from_openapi( Some(schema), required_fields.contains(name.as_str()), text(schema, "description"), ), ); } } Schema { kind: SchemaKind::Object, description: description.or_else(|| text(value, "description")), required, nullable: nullable(value), default_value: value.get("default").cloned(), fields, items: None, enum_values: Vec::new(), variants: Vec::new(), } } fn primitive(kind: SchemaKind, required: bool, description: Option) -> Schema { Schema { kind, description, required, nullable: false, default_value: None, fields: BTreeMap::new(), items: None, enum_values: Vec::new(), variants: Vec::new(), } } fn type_name(value: &Value) -> Option<&str> { match value.get("type") { Some(Value::String(value)) => Some(value.as_str()), Some(Value::Array(values)) => values.iter().find_map(Value::as_str), _ if value.get("properties").is_some() => Some("object"), _ if value.get("items").is_some() => Some("array"), _ => None, } } fn nullable(value: &Value) -> bool { value .get("nullable") .and_then(Value::as_bool) .unwrap_or(false) } fn text(value: &Value, key: &str) -> Option { value .get(key) .and_then(Value::as_str) .map(ToOwned::to_owned) } fn collapse_composition(value: &Value) -> Value { for key in ["allOf", "oneOf", "anyOf"] { if let Some(items) = value.get(key).and_then(Value::as_array) { if let Some(first) = items.first() { return first.clone(); } } } value.clone() }