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); }; // Only v3 NormalizedIR emits the private marker. Pending legacy-v1 jobs // retain their historical first-branch behavior, while modern oneOf/anyOf // is projected losslessly through crank-schema's existing Oneof shape. if value .get("x-crank-lossless-composition") .and_then(Value::as_bool) == Some(true) && let Some(items) = value .get("oneOf") .or_else(|| value.get("anyOf")) .and_then(Value::as_array) { return Schema { kind: SchemaKind::Oneof, description: description.or_else(|| text(value, "description")), required, nullable: nullable(value), default_value: value.get("default").cloned(), fields: BTreeMap::new(), items: None, enum_values: Vec::new(), variants: items .iter() .map(|item| schema_from_openapi(Some(item), true, None)) .collect(), }; } 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")), ), } } fn collapse_composition(value: &Value) -> Value { for key in ["allOf", "oneOf", "anyOf"] { if let Some(items) = value.get(key).and_then(Value::as_array) && let Some(first) = items.first() { return first.clone(); } } value.clone() } 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) }