use proc_macro2::Span;
use quote::quote;
use serde_json::Value;
use syn::visit_mut::VisitMut;
use typify::{TypeSpace, TypeSpaceImpl, TypeSpaceSettings};
use super::GenerateError;
type Definition = (String, Value);
const ENGINE: &str = "regress";
pub(super) fn emit(
api: &openapiv3::OpenAPI,
header: &str,
replacements: &[(String, String)],
) -> Result<String, GenerateError> {
let definitions = definitions(api)?;
let mut settings = TypeSpaceSettings::default();
settings.with_derive("PartialEq".to_owned());
for (format, rust) in replacements {
for shape in shapes_declaring(&definitions, format) {
let shape: schemars::schema::SchemaObject =
serde_json::from_value(shape).map_err(|source| {
GenerateError::Unsupported(format!(
"a `format: {format}` shape typify does not accept: {source}"
))
})?;
settings.with_conversion(
shape,
rust,
[TypeSpaceImpl::Display, TypeSpaceImpl::FromStr].into_iter(),
);
}
}
let mut space = TypeSpace::new(&settings);
space
.add_ref_types(schemas(definitions)?)
.map_err(GenerateError::Typify)?;
let mut file: syn::File =
syn::parse2(space.to_stream()).map_err(|source| GenerateError::NotRust {
file: "types.rs",
source,
})?;
ThroughThisCrate.visit_file_mut(&mut file);
let mut displays = display_impls(&file)?;
file.items.append(&mut displays);
Ok(format!("{header}{}", prettyplease::unparse(&file)))
}
fn display_impls(file: &syn::File) -> Result<Vec<syn::Item>, GenerateError> {
file.items
.iter()
.filter_map(string_newtype)
.map(|name| {
syn::parse2(quote! {
impl ::std::fmt::Display for #name {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
::std::fmt::Display::fmt(&self.0, f)
}
}
})
.map(syn::Item::Impl)
.map_err(|source| GenerateError::NotRust {
file: "types.rs",
source,
})
})
.collect()
}
fn string_newtype(item: &syn::Item) -> Option<&syn::Ident> {
let syn::Item::Struct(item) = item else {
return None;
};
let syn::Fields::Unnamed(fields) = &item.fields else {
return None;
};
if !item.generics.params.is_empty() || fields.unnamed.len() != 1 {
return None;
}
let syn::Type::Path(wrapped) = &fields.unnamed.first()?.ty else {
return None;
};
(wrapped.qself.is_none() && wrapped.path.segments.last()?.ident == "String")
.then_some(&item.ident)
}
struct ThroughThisCrate;
impl VisitMut for ThroughThisCrate {
fn visit_path_mut(&mut self, path: &mut syn::Path) {
syn::visit_mut::visit_path_mut(self, path);
if path.leading_colon.is_some()
&& path.segments.first().is_some_and(|it| it.ident == ENGINE)
{
path.segments.insert(
0,
syn::Ident::new("typed_openapi", Span::call_site()).into(),
);
}
}
}
fn definitions(api: &openapiv3::OpenAPI) -> Result<Vec<Definition>, GenerateError> {
let components = api.components.as_ref().ok_or_else(|| {
GenerateError::Unsupported("the overlaid document has no components".to_owned())
})?;
components
.schemas
.iter()
.map(|(name, schema)| {
let value = serde_json::to_value(schema).map_err(|source| GenerateError::Schema {
name: name.clone(),
source,
})?;
Ok((name.clone(), as_json_schema(value)))
})
.collect()
}
fn schemas(
definitions: Vec<Definition>,
) -> Result<Vec<(String, schemars::schema::Schema)>, GenerateError> {
definitions
.into_iter()
.map(|(name, value)| {
let schema: schemars::schema::Schema =
serde_json::from_value(value).map_err(|source| {
GenerateError::Unsupported(format!(
"schema `{name}` is not a JSON Schema typify accepts: {source}"
))
})?;
Ok((name, schema))
})
.collect()
}
fn shapes_declaring(definitions: &[Definition], format: &str) -> Vec<Value> {
let mut found = Vec::new();
for (_, schema) in definitions {
collect(schema, format, &mut found);
}
found
}
fn collect(value: &Value, format: &str, found: &mut Vec<Value>) {
let Value::Object(fields) = value else {
if let Value::Array(items) = value {
for item in items {
collect(item, format, found);
}
}
return;
};
if fields.get("format") == Some(&Value::String(format.to_owned())) && !found.contains(value) {
found.push(value.clone());
}
for nested in fields.values() {
collect(nested, format, found);
}
}
fn as_json_schema(value: Value) -> Value {
match value {
Value::Object(fields) => Value::Object(
fields
.into_iter()
.map(|(key, value)| {
let value = match (key.as_str(), &value) {
("$ref", Value::String(target)) => {
Value::String(target.replace("#/components/schemas/", "#/definitions/"))
}
_ => as_json_schema(value),
};
(key, value)
})
.collect(),
),
Value::Array(items) => Value::Array(items.into_iter().map(as_json_schema).collect()),
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => value,
}
}