204 lines
5.8 KiB
Rust
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()
|
|
}
|