Files
crank/crates/crank-import/src/rest/schema.rs
T
2026-06-23 21:03:03 +00:00

204 lines
5.8 KiB
Rust

use std::collections::BTreeMap;
use crank_schema::{Schema, SchemaKind};
use serde_json::Value;
pub fn any_object(description: Option<String>) -> 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<String>,
) -> 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::<Vec<_>>();
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<String>, fields: BTreeMap<String, Schema>) -> 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<String>) -> Schema {
let required_fields = value
.get("required")
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.filter_map(Value::as_str)
.collect::<std::collections::BTreeSet<_>>()
})
.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<String>) -> 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<String> {
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()
}