pub mod base64;
pub mod error;
pub mod example;
pub mod exts;
pub mod paginate;
pub mod phone_number;
pub mod random;
use std::{collections::BTreeMap, str::FromStr};
use anyhow::Result;
use indexmap::map::IndexMap;
use numeral::Cardinal;
use crate::types::exts::{
ParameterExt, ParameterSchemaOrContentExt, ReferenceOrExt, SchemaRenderExt, StatusCodeExt,
TokenStreamExt,
};
#[derive(Debug, Clone)]
pub struct TypeSpace {
pub types: IndexMap<String, openapiv3::Schema>,
pub spec: openapiv3::OpenAPI,
pub rendered: proc_macro2::TokenStream,
}
pub fn generate_types(spec: &openapiv3::OpenAPI) -> Result<TypeSpace> {
let base64_mod = get_base64_mod()?;
let paginate_mod = get_paginate_mod()?;
let phone_number_mod = get_phone_number_mod()?;
let error_mod = get_error_mod()?;
let mut type_space = TypeSpace {
types: IndexMap::new(),
spec: spec.clone(),
rendered: quote!(
use tabled::Tabled;
#base64_mod
#paginate_mod
#phone_number_mod
#error_mod
),
};
if let Some(components) = &spec.components {
for (name, schema) in &components.schemas {
let schema = schema.get_schema_from_reference(spec, true)?;
type_space.render_schema(name, &schema)?;
}
for (name, parameter) in &components.parameters {
let schema = (¶meter.expand(spec)?).data()?.format.schema()?;
let schema = schema.get_schema_from_reference(spec, true)?;
type_space.render_schema(name, &schema)?;
}
for (name, response) in &components.responses {
type_space.render_response(name, &response.expand(spec)?)?;
}
for (name, request_body) in &components.request_bodies {
type_space.render_request_body(name, &request_body.expand(spec)?)?;
}
}
Ok(type_space)
}
impl TypeSpace {
pub fn render(&self) -> Result<String> {
get_text_fmt(&self.rendered)
}
pub fn add_to_rendered(
&mut self,
t: &proc_macro2::TokenStream,
(name, s): (String, openapiv3::Schema),
) -> Result<()> {
if let Some(item) = self.types.get(&name) {
if &s != item {
anyhow::bail!(
"The schema {} is already defined with a different schema\nnew: {:?}\nold: {:?}",
name,
s,
item
);
}
} else {
self.types.insert(name, s);
let r = &self.rendered;
self.rendered = quote! {
#r
#t
};
}
Ok(())
}
pub fn render_schema(&mut self, name: &str, schema: &openapiv3::Schema) -> Result<()> {
match &schema.schema_kind {
openapiv3::SchemaKind::Type(openapiv3::Type::String(s)) => {
self.render_string_type(name, s, &schema.schema_data)
}
openapiv3::SchemaKind::Type(openapiv3::Type::Number(_n)) => {
Ok(())
}
openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_i)) => {
Ok(())
}
openapiv3::SchemaKind::Type(openapiv3::Type::Object(o)) => {
self.render_object(name, o, &schema.schema_data)
}
openapiv3::SchemaKind::Type(openapiv3::Type::Array(a)) => {
if let Some(openapiv3::ReferenceOr::Item(s)) = &a.items {
return self.render_schema(name, s);
}
Ok(())
}
openapiv3::SchemaKind::Type(openapiv3::Type::Boolean { .. }) => {
Ok(())
}
openapiv3::SchemaKind::OneOf { one_of } => {
self.render_one_of(name, one_of, &schema.schema_data)
}
openapiv3::SchemaKind::AllOf { all_of } => {
self.render_all_of(name, all_of, &schema.schema_data)
}
openapiv3::SchemaKind::AnyOf { any_of } => {
self.render_any_of(name, any_of, &schema.schema_data)
}
openapiv3::SchemaKind::Not { not } => {
anyhow::bail!("XXX not not supported yet: {} => {:?}", name, not);
}
openapiv3::SchemaKind::Any(any) => self.render_any(name, any, &schema.schema_data),
}
}
#[allow(clippy::type_complexity)]
fn get_all_of_properties(
&mut self,
name: &str,
all_ofs: &Vec<openapiv3::ReferenceOr<openapiv3::Schema>>,
) -> Result<(
IndexMap<String, openapiv3::ReferenceOr<Box<openapiv3::Schema>>>,
Vec<String>,
)> {
let mut properties: IndexMap<String, openapiv3::ReferenceOr<Box<openapiv3::Schema>>> =
IndexMap::new();
let mut required: Vec<String> = Vec::new();
for all_of in all_ofs {
let schema = all_of.get_schema_from_reference(&self.spec, true)?;
if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(o)) = &schema.schema_kind {
for (k, v) in o.properties.iter() {
properties.insert(k.clone(), v.clone());
}
required.extend(o.required.iter().cloned());
} else if let openapiv3::SchemaKind::AllOf { all_of } = &schema.schema_kind {
let (p, r) = self.get_all_of_properties(name, all_of)?;
properties.extend(p);
required.extend(r);
} else {
anyhow::bail!(
"The all of {} is not an object, it is a {:?}",
name,
schema.schema_kind
);
}
}
Ok((properties, required))
}
fn render_all_of(
&mut self,
name: &str,
all_ofs: &Vec<openapiv3::ReferenceOr<openapiv3::Schema>>,
data: &openapiv3::SchemaData,
) -> Result<()> {
if all_ofs.len() == 1 {
let first = if let openapiv3::ReferenceOr::Item(i) = &all_ofs[0] {
i.clone()
} else {
all_ofs[0].get_schema_from_reference(&self.spec, true)?
};
return self.render_schema(name, &first);
}
let (properties, required) = match self.get_all_of_properties(name, all_ofs) {
Ok(p) => p,
Err(err) => {
if err.to_string().contains("not an object") {
return self.render_one_of(name, all_ofs, data);
}
return Err(err);
}
};
self.render_object(
name,
&openapiv3::ObjectType {
properties,
required,
..Default::default()
},
data,
)
}
fn render_any_of(
&mut self,
name: &str,
any_ofs: &Vec<openapiv3::ReferenceOr<openapiv3::Schema>>,
data: &openapiv3::SchemaData,
) -> Result<()> {
if any_ofs.len() == 1 {
let first = any_ofs[0].item()?;
return self.render_schema(name, first);
}
let mut properties: IndexMap<String, openapiv3::ReferenceOr<Box<openapiv3::Schema>>> =
IndexMap::new();
for any_of in any_ofs {
let schema = any_of.get_schema_from_reference(&self.spec, true)?;
if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(o)) = &schema.schema_kind {
for (k, v) in o.properties.iter() {
properties.insert(k.clone(), v.clone());
}
} else {
return self.render_one_of(name, any_ofs, data);
}
}
self.render_object(
name,
&openapiv3::ObjectType {
properties,
..Default::default()
},
data,
)
}
fn render_one_of(
&mut self,
name: &str,
one_ofs: &Vec<openapiv3::ReferenceOr<openapiv3::Schema>>,
data: &openapiv3::SchemaData,
) -> Result<()> {
let description = if let Some(d) = &data.description {
quote!(#[doc = #d])
} else {
quote!()
};
let one_of_name = get_type_name(name, data)?;
if one_ofs.len() == 1 {
let first = one_ofs[0].item()?;
return self.render_schema(name, first);
}
let tag_result = get_one_of_tag(one_ofs, &self.spec)?;
let mut serde_options = Vec::new();
if let Some(tag) = &tag_result.tag {
serde_options.push(quote!(tag = #tag))
}
if let Some(content) = &tag_result.content {
serde_options.push(quote!(content = #content))
}
let serde_options = if serde_options.is_empty() {
quote!()
} else {
quote!(#[serde(#(#serde_options),*)] )
};
let (_, values) = self.get_one_of_values(name, one_ofs, &tag_result, true)?;
let rendered = quote! {
#description
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug, Clone, schemars::JsonSchema, tabled::Tabled)]
#serde_options
pub enum #one_of_name {
#values
}
};
self.add_to_rendered(
&rendered,
(
one_of_name.to_string(),
openapiv3::Schema {
schema_data: data.clone(),
schema_kind: openapiv3::SchemaKind::OneOf {
one_of: one_ofs.clone(),
},
},
),
)?;
Ok(())
}
fn render_any(
&mut self,
name: &str,
any: &openapiv3::AnySchema,
data: &openapiv3::SchemaData,
) -> Result<()> {
if !any.properties.is_empty() || any.additional_properties.is_some() {
return self.render_object(
name,
&openapiv3::ObjectType {
properties: any.properties.clone(),
required: any.required.clone(),
additional_properties: any.additional_properties.clone(),
min_properties: any.min_properties,
max_properties: any.max_properties,
},
data,
);
}
Ok(())
}
fn render_object(
&mut self,
name: &str,
o: &openapiv3::ObjectType,
data: &openapiv3::SchemaData,
) -> Result<()> {
if let Some(min_properties) = o.min_properties {
log::warn!(
"min properties not supported for objects: {} => {:?}",
name,
min_properties
);
}
if let Some(max_properties) = o.max_properties {
log::warn!(
"max properties not supported for objects: {} => {:?}",
name,
max_properties
);
}
let description = if let Some(d) = &data.description {
quote!(#[doc = #d])
} else {
quote!()
};
let struct_name = get_type_name(name, data)?;
if o.properties.is_empty() {
if let Some(additional_properties) = &o.additional_properties {
match additional_properties {
openapiv3::AdditionalProperties::Any(_any) => {
}
openapiv3::AdditionalProperties::Schema(schema) => match schema.item() {
Ok(item) => {
return self.render_schema(name, item);
}
Err(_) => {
return Ok(());
}
},
}
}
}
let mut values = quote!();
for (k, v) in &o.properties {
let prop = clean_property_name(k);
let inner_schema = if let openapiv3::ReferenceOr::Item(i) = v {
let s = &**i;
s.clone()
} else {
v.get_schema_from_reference(&self.spec, true)?
};
let prop_desc = if let Some(d) = &inner_schema.schema_data.description {
quote!(#[doc = #d])
} else {
quote!()
};
let mut type_name = if v.should_render()? {
let mut t = if let Some(components) = &self.spec.components {
if components.schemas.contains_key(&proper_name(&prop)) {
proper_name(&format!("{} {}", struct_name, prop))
} else {
proper_name(&prop)
}
} else {
proper_name(&prop)
};
let mut should_render = true;
if let Some(rendered) = self.types.get(&t) {
if rendered.schema_kind != inner_schema.schema_kind
|| rendered.schema_data != inner_schema.schema_data
{
t = proper_name(&format!("{} {}", struct_name, prop));
} else {
should_render = false;
}
}
if should_render {
self.render_schema(&t, &inner_schema)?;
}
get_type_name_for_schema(&t, &inner_schema, &self.spec, true)?
} else if let openapiv3::ReferenceOr::Item(i) = v {
get_type_name_for_schema(&prop, i, &self.spec, true)?
} else {
get_type_name_from_reference(&v.reference()?, &self.spec, true)?
};
if !o.required.contains(k) && !type_name.is_option()? {
type_name = quote!(Option<#type_name>);
}
let prop_ident = format_ident!("{}", prop);
let prop_value = quote!(
pub #prop_ident: #type_name,
);
let mut serde_props = Vec::<proc_macro2::TokenStream>::new();
if &prop != k {
serde_props.push(quote!(
rename = #k
));
}
if type_name.is_option()? {
serde_props.push(quote!(default));
serde_props.push(quote!(skip_serializing_if = "Option::is_none"));
}
let serde_full = if serde_props.is_empty() {
quote!()
} else {
quote!(#[serde(#(#serde_props),*)])
};
values = quote!(
#values
#prop_desc
#serde_full
#prop_value
);
}
let mut pagination = quote!();
let pagination_properties = PaginationProperties::from_object(o, &self.spec)?;
if pagination_properties.can_paginate() {
let page_item = pagination_properties.item_type(true)?;
let item_ident = pagination_properties.item_ident()?;
let next_page_str = pagination_properties.next_page_str()?;
let next_page_ident = format_ident!("{}", next_page_str);
pagination = quote!(
impl crate::types::paginate::Pagination for #struct_name {
type Item = #page_item;
fn has_more_pages(&self) -> bool {
self.next_page.is_some()
}
fn next_page(&self, req: reqwest::Request) -> anyhow::Result<reqwest::Request, crate::types::error::Error> {
let mut req = req.try_clone().ok_or_else(|| crate::types::error::Error::InvalidRequest(format!("failed to clone request: {:?}", req)))?;
req.url_mut().query_pairs_mut()
.append_pair(#next_page_str, self.#next_page_ident.as_deref().unwrap_or(""));
Ok(req)
}
fn items(&self) -> Vec<Self::Item> {
self.#item_ident.clone()
}
}
);
}
let length: proc_macro2::TokenStream = o
.properties
.len()
.to_string()
.parse()
.map_err(|err| anyhow::anyhow!("{}", err))?;
let mut headers = Vec::new();
let mut fields = Vec::new();
for (k, v) in &o.properties {
let prop = clean_property_name(k);
let prop_ident = format_ident!("{}", prop);
headers.push(quote!(#prop.to_string()));
let inner_schema = if let openapiv3::ReferenceOr::Item(i) = v {
let s = &**i;
s.clone()
} else {
v.get_schema_from_reference(&self.spec, true)?
};
let type_name = get_type_name_for_schema(&prop, &inner_schema, &self.spec, true)?;
if o.required.contains(k) && type_name.is_string()? {
fields.push(quote!(
self.#prop_ident.clone()
));
} else if !o.required.contains(k)
&& type_name.rendered()? != "phone_number::PhoneNumber"
{
fields.push(quote!(
if let Some(#prop_ident) = &self.#prop_ident {
format!("{:?}", #prop_ident)
} else {
String::new()
}
));
} else if type_name.rendered()? == "PhoneNumber" {
fields.push(quote!(
self.#prop_ident.to_string()
));
} else {
fields.push(quote!(format!("{:?}", self.#prop_ident)));
}
}
let tabled = quote! {
impl tabled::Tabled for #struct_name {
const LENGTH: usize = #length;
fn fields(&self) -> Vec<String> {
vec![
#(#fields),*
]
}
fn headers() -> Vec<String> {
vec![
#(#headers),*
]
}
}
};
let rendered = quote! {
#description
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug, Clone, schemars::JsonSchema)]
pub struct #struct_name {
#values
}
impl std::fmt::Display for #struct_name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "{}", serde_json::to_string_pretty(self).map_err(|_| std::fmt::Error)?)
}
}
#pagination
#tabled
};
self.add_to_rendered(
&rendered,
(
struct_name.to_string(),
openapiv3::Schema {
schema_data: data.clone(),
schema_kind: openapiv3::SchemaKind::Type(openapiv3::Type::Object(o.clone())),
},
),
)?;
Ok(())
}
fn render_string_type(
&mut self,
name: &str,
s: &openapiv3::StringType,
data: &openapiv3::SchemaData,
) -> Result<()> {
if !s.enumeration.is_empty() {
return self.render_enum(name, s, data);
}
if let Some(ref max_length) = s.max_length {
log::warn!(
"XXX max_length not supported here yet: {} => {:?}",
name,
max_length
);
}
if let Some(ref min_length) = s.min_length {
log::warn!(
"XXX min_length not supported here yet: {} => {:?}",
name,
min_length
);
}
Ok(())
}
fn render_enum(
&mut self,
name: &str,
s: &openapiv3::StringType,
data: &openapiv3::SchemaData,
) -> Result<()> {
if s.enumeration.is_empty() {
anyhow::bail!("Cannot render empty string enumeration: {}", name);
}
let description = if let Some(d) = &data.description {
quote!(#[doc = #d])
} else {
quote!()
};
let enum_name = get_type_name(name, data)?;
let mut values = quote!();
for e in &s.enumeration {
if e.is_none() {
if !data.nullable {
anyhow::bail!("enum `{}` is not nullable, but it has a null value", name);
}
continue;
}
let e = e.as_ref().unwrap().to_string();
let e_name = format_ident!("{}", proper_name(&e));
let mut e_value = quote!(
#e_name,
);
if proper_name(&e) != e {
e_value = quote!(
#[serde(rename = #e)]
#[display(#e)]
#e_value
);
}
values = quote!(
#values
#e_value
);
}
let default = if let Some(default) = &data.default {
let default = default.to_string();
let default = format_ident!("{}", proper_name(&default));
quote!(
impl std::default::Default for #enum_name {
fn default() -> Self {
#enum_name::#default
}
}
)
} else if s.enumeration.len() == 1 {
let default = s.enumeration[0].as_ref().unwrap().to_string();
let default = format_ident!("{}", proper_name(&default));
quote!(
impl std::default::Default for #enum_name {
fn default() -> Self {
#enum_name::#default
}
}
)
} else {
quote!()
};
let rendered = quote! {
#description
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash, Debug, Clone, schemars::JsonSchema, tabled::Tabled, clap::ValueEnum, parse_display::FromStr, parse_display::Display)]
pub enum #enum_name {
#values
}
#default
};
self.add_to_rendered(
&rendered,
(
enum_name.to_string(),
openapiv3::Schema {
schema_data: data.clone(),
schema_kind: openapiv3::SchemaKind::Type(openapiv3::Type::String(s.clone())),
},
),
)?;
Ok(())
}
fn render_response(&mut self, name: &str, response: &openapiv3::Response) -> Result<()> {
for (content_name, content) in &response.content {
if let Some(openapiv3::ReferenceOr::Item(i)) = &content.schema {
self.render_schema(&format!("{}_{}", name, content_name), i)?;
}
}
Ok(())
}
fn render_request_body(
&mut self,
name: &str,
request_body: &openapiv3::RequestBody,
) -> Result<()> {
for (content_name, content) in &request_body.content {
if let Some(openapiv3::ReferenceOr::Item(i)) = &content.schema {
self.render_schema(&format!("{}_{}", name, content_name), i)?;
}
}
Ok(())
}
fn get_one_of_values(
&mut self,
name: &str,
one_ofs: &Vec<openapiv3::ReferenceOr<openapiv3::Schema>>,
tag_result: &TagContent,
should_render: bool,
) -> Result<(
BTreeMap<String, openapiv3::ReferenceOr<openapiv3::Schema>>,
proc_macro2::TokenStream,
)> {
let mut values: BTreeMap<String, openapiv3::ReferenceOr<openapiv3::Schema>> =
Default::default();
let mut rendered_value = quote!();
let mut name = name.to_string();
if let Some(tag) = &tag_result.tag {
for one_of in one_ofs {
let schema = one_of.get_schema_from_reference(&self.spec, true)?;
if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(o)) = &schema.schema_kind
{
let tag_schema = match o.properties.get(tag) {
Some(v) => v,
None => {
anyhow::bail!(
"no property `{}` in object, even through we thought we had a tag",
tag
);
}
};
let inner_schema = if let openapiv3::ReferenceOr::Item(i) = tag_schema {
let s = &**i;
s.clone()
} else {
tag_schema.get_schema_from_reference(&self.spec, true)?
};
let tag_name = if let openapiv3::SchemaKind::Type(openapiv3::Type::String(s)) =
inner_schema.schema_kind
{
if s.enumeration.len() == 1 {
s.enumeration[0]
.as_ref()
.map(|s| s.to_string())
.unwrap_or_default()
} else {
anyhow::bail!("enumeration for tag `{}` is not a single value", tag);
}
} else {
anyhow::bail!("enumeration for tag `{}` is not a string", tag);
};
let p = proper_name(&tag_name);
let n = format_ident!("{}", p);
if let Some(content) = &tag_result.content {
let content_schema = match o.properties.get(content) {
Some(v) => v,
None => {
anyhow::bail!(
"no property `{}` in object, even through we thought we had content",
content
);
}
};
let content_name = if let openapiv3::ReferenceOr::Item(i) = content_schema {
let s = &**i;
get_type_name_for_schema(&name, s, &self.spec, true)?
} else {
get_type_name_from_reference(
&content_schema.reference()?,
&self.spec,
true,
)?
};
values.insert(p.to_string(), one_of.clone());
if p != tag_name {
rendered_value = quote!(
#rendered_value
#[serde(rename = #tag_name)]
#n(#content_name),
);
} else {
rendered_value = quote!(
#rendered_value
#n(#content_name),
);
}
} else {
let content_name =
render_enum_object_internal(&tag_name, o, &self.spec, tag)?;
values.insert(p.to_string(), one_of.clone());
if p != tag_name {
rendered_value = quote!(
#rendered_value
#[serde(rename = #tag_name)]
#content_name,
);
} else {
rendered_value = quote!(
#rendered_value
#content_name,
);
}
}
}
}
return Ok((values, rendered_value));
}
for one_of in one_ofs {
if one_of.should_render()? && should_render {
name = format!("{}_OneOf", name);
self.render_schema(&name, &one_of.item()?.clone())?;
}
let o_type = if let openapiv3::ReferenceOr::Reference { .. } = one_of {
let reference = proper_name(&one_of.reference()?);
let reference_name = format_ident!("{}", reference);
values.insert(reference.to_string(), one_of.clone());
quote!(
#reference_name(#reference_name),
)
} else {
let rendered_type =
get_type_name_for_schema(&name, &one_of.expand(&self.spec)?, &self.spec, true)?;
let n = if let Some(title) = &one_of.expand(&self.spec)?.schema_data.title {
let p = proper_name(title);
p.parse().map_err(|e| anyhow::anyhow!("{}", e))?
} else {
let t = inflector::cases::classcase::to_class_case(
&rendered_type.strip_option()?.strip_vec()?.rendered()?,
);
let t = format_ident!("{}", t);
quote!(#t)
};
let rendered = n.rendered()?;
values.insert(rendered, one_of.clone());
quote!(
#n(#rendered_type),
)
};
rendered_value = quote!(
#rendered_value
#o_type
);
}
Ok((values, rendered_value))
}
}
pub fn get_type_name_for_schema(
name: &str,
schema: &openapiv3::Schema,
spec: &openapiv3::OpenAPI,
in_crate: bool,
) -> Result<proc_macro2::TokenStream> {
let t = match &schema.schema_kind {
openapiv3::SchemaKind::Type(openapiv3::Type::String(s)) => {
get_type_name_for_string(name, s, &schema.schema_data, in_crate)?
}
openapiv3::SchemaKind::Type(openapiv3::Type::Number(n)) => get_type_name_for_number(n)?,
openapiv3::SchemaKind::Type(openapiv3::Type::Integer(i)) => get_type_name_for_integer(i)?,
openapiv3::SchemaKind::Type(openapiv3::Type::Object(o)) => {
get_type_name_for_object(name, o, &schema.schema_data, spec, in_crate)?
}
openapiv3::SchemaKind::Type(openapiv3::Type::Array(a)) => {
get_type_name_for_array(name, a, spec, in_crate)?
}
openapiv3::SchemaKind::Type(openapiv3::Type::Boolean { .. }) => quote!(bool),
openapiv3::SchemaKind::OneOf { one_of } => {
if one_of.len() != 1 {
if name.is_empty() {
anyhow::bail!(
"XXX one of with more than one value not supported yet when name is empty"
);
} else {
let ident = format_ident!("{}", proper_name(name));
let t = if in_crate {
quote!(#ident)
} else {
quote!(crate::types::#ident)
};
return Ok(t);
}
}
let internal_schema = &one_of[0];
match internal_schema {
openapiv3::ReferenceOr::Reference { .. } => {
get_type_name_from_reference(&internal_schema.reference()?, spec, in_crate)?
}
openapiv3::ReferenceOr::Item(s) => {
get_type_name_for_schema(name, s, spec, in_crate)?
}
}
}
openapiv3::SchemaKind::AllOf { all_of } => {
get_type_name_for_all_of(name, all_of, &schema.schema_data, spec, in_crate)?
}
openapiv3::SchemaKind::AnyOf { any_of: _ } => get_type_name_for_object(
name,
&openapiv3::ObjectType::default(),
&schema.schema_data,
spec,
in_crate,
)?,
openapiv3::SchemaKind::Not { not: _ } => {
anyhow::bail!("XXX not not supported yet");
}
openapiv3::SchemaKind::Any(any) => {
if !any.properties.is_empty() || any.additional_properties.is_some() {
let obj = openapiv3::Schema {
schema_data: schema.schema_data.clone(),
schema_kind: openapiv3::SchemaKind::Type(openapiv3::Type::Object(
openapiv3::ObjectType {
properties: any.properties.clone(),
required: any.required.clone(),
additional_properties: any.additional_properties.clone(),
min_properties: any.min_properties,
max_properties: any.max_properties,
},
)),
};
return get_type_name_for_schema(name, &obj, spec, in_crate);
}
log::warn!("got any schema kind `{}`: {:?}", name, any);
quote!(serde_json::Value)
}
};
if schema.schema_data.nullable && !t.is_option()? {
Ok(quote!(Option<#t>))
} else {
Ok(t)
}
}
fn get_type_name_for_string(
name: &str,
s: &openapiv3::StringType,
data: &openapiv3::SchemaData,
in_crate: bool,
) -> Result<proc_macro2::TokenStream> {
if !s.enumeration.is_empty() {
let ident = get_type_name(name, data)?;
let t = if in_crate {
quote!(#ident)
} else {
quote!(crate::types::#ident)
};
return Ok(t);
}
let t = match &s.format {
openapiv3::VariantOrUnknownOrEmpty::Item(openapiv3::StringFormat::DateTime) => {
quote!(chrono::DateTime<chrono::Utc>)
}
openapiv3::VariantOrUnknownOrEmpty::Item(openapiv3::StringFormat::Date) => {
quote!(chrono::NaiveDate)
}
openapiv3::VariantOrUnknownOrEmpty::Item(openapiv3::StringFormat::Password) => {
quote!(String)
}
openapiv3::VariantOrUnknownOrEmpty::Item(openapiv3::StringFormat::Byte) => {
if in_crate {
quote!(base64::Base64Data)
} else {
quote!(crate::types::base64::Base64Data)
}
}
openapiv3::VariantOrUnknownOrEmpty::Item(openapiv3::StringFormat::Binary) => {
quote!(bytes::Bytes)
}
openapiv3::VariantOrUnknownOrEmpty::Empty => quote!(String),
openapiv3::VariantOrUnknownOrEmpty::Unknown(f) => match f.as_str() {
"float" => quote!(f64),
"int64" => quote!(i64),
"uint64" => quote!(u64),
"ipv4" => quote!(std::net::Ipv4Addr),
"ipv6" => quote!(std::net::Ipv6Addr),
"ip" => quote!(std::net::IpAddr),
"uri" => quote!(url::Url),
"uri-template" => quote!(String),
"url" => quote!(url::Url),
"email" => quote!(String),
"phone" => {
if in_crate {
quote!(phone_number::PhoneNumber)
} else {
quote!(crate::types::phone_number::PhoneNumber)
}
}
"uuid" => quote!(uuid::Uuid),
"hostname" => quote!(String),
"time" => quote!(chrono::NaiveTime),
"date" => quote!(chrono::NaiveDate),
"date-time" => quote!(chrono::DateTime<chrono::Utc>),
"partial-date-time" => quote!(chrono::NaiveDateTime),
f => {
anyhow::bail!("XXX unknown string format {}", f)
}
},
};
Ok(t)
}
pub fn get_type_name_from_reference(
name: &str,
spec: &openapiv3::OpenAPI,
in_crate: bool,
) -> Result<proc_macro2::TokenStream> {
let schema = if let Some(components) = &spec.components {
components
.schemas
.get(name)
.ok_or_else(|| anyhow::anyhow!("reference {} not found in components", name))?
.item()?
} else {
anyhow::bail!("no components in spec, cannot get reference");
};
get_type_name_for_schema(name, schema, spec, in_crate)
}
fn get_type_name_for_number(n: &openapiv3::NumberType) -> Result<proc_macro2::TokenStream> {
let t = match &n.format {
openapiv3::VariantOrUnknownOrEmpty::Item(openapiv3::NumberFormat::Float) => {
quote!(f64)
}
openapiv3::VariantOrUnknownOrEmpty::Item(openapiv3::NumberFormat::Double) => {
quote!(f64)
}
openapiv3::VariantOrUnknownOrEmpty::Empty => quote!(f64),
openapiv3::VariantOrUnknownOrEmpty::Unknown(f) => {
let width = match f.as_str() {
"f32" => 32,
"f64" => 64,
"money-usd" => 64,
f => anyhow::bail!("unknown number format {}", f),
};
match width {
32 => quote!(f32),
64 => quote!(f64),
_ => anyhow::bail!("unknown number width {}", width),
}
}
};
Ok(t)
}
fn get_type_name_for_integer(i: &openapiv3::IntegerType) -> Result<proc_macro2::TokenStream> {
let t = match &i.format {
openapiv3::VariantOrUnknownOrEmpty::Item(openapiv3::IntegerFormat::Int32) => {
quote!(i32)
}
openapiv3::VariantOrUnknownOrEmpty::Item(openapiv3::IntegerFormat::Int64) => {
quote!(i64)
}
openapiv3::VariantOrUnknownOrEmpty::Empty => quote!(i64),
openapiv3::VariantOrUnknownOrEmpty::Unknown(f) => {
let uint;
let width;
match f.as_str() {
"uint" | "uint32" => {
uint = true;
width = 32;
}
"uint8" => {
uint = true;
width = 8;
}
"uint16" => {
uint = true;
width = 16;
}
"uint64" => {
uint = true;
width = 64;
}
"int8" => {
uint = false;
width = 8;
}
"int16" => {
uint = false;
width = 16;
}
"duration" => {
uint = false;
width = 64;
}
f => anyhow::bail!("unknown integer format {}", f),
}
if uint {
match width {
8 => quote!(u8),
16 => quote!(u16),
32 => quote!(u32),
64 => quote!(u64),
_ => anyhow::bail!("unknown uint width {}", width),
}
} else {
match width {
8 => quote!(i8),
16 => quote!(i16),
32 => quote!(i32),
64 => quote!(i64),
_ => anyhow::bail!("unknown int width {}", width),
}
}
}
};
Ok(t)
}
fn get_type_name_for_object(
name: &str,
o: &openapiv3::ObjectType,
data: &openapiv3::SchemaData,
spec: &openapiv3::OpenAPI,
in_crate: bool,
) -> Result<proc_macro2::TokenStream> {
if o.properties.is_empty() {
if let Some(additional_properties) = &o.additional_properties {
match additional_properties {
openapiv3::AdditionalProperties::Any(_any) => {
}
openapiv3::AdditionalProperties::Schema(schema) => {
let t = if let Ok(reference) = schema.reference() {
let ident = format_ident!("{}", proper_name(&reference));
if in_crate {
quote!(#ident)
} else {
quote!(crate::types::#ident)
}
} else {
get_type_name_for_schema(name, schema.item()?, spec, in_crate)?
};
return Ok(quote!(std::collections::HashMap<String, #t>));
}
}
}
}
if o == &openapiv3::ObjectType::default()
&& name.is_empty()
&& data == &openapiv3::SchemaData::default()
{
anyhow::bail!("object `{}` has no properties: {:?} => {:?}", name, o, data);
}
let ident = get_type_name(name, data)?;
let t = if in_crate {
quote!(#ident)
} else {
quote!(crate::types::#ident)
};
Ok(t)
}
fn get_type_name_for_all_of(
name: &str,
all_ofs: &Vec<openapiv3::ReferenceOr<openapiv3::Schema>>,
data: &openapiv3::SchemaData,
spec: &openapiv3::OpenAPI,
in_crate: bool,
) -> Result<proc_macro2::TokenStream> {
if all_ofs.len() == 1 {
let internal_schema = &all_ofs[0];
return match internal_schema {
openapiv3::ReferenceOr::Reference { .. } => {
get_type_name_from_reference(&internal_schema.reference()?, spec, in_crate)
}
openapiv3::ReferenceOr::Item(s) => get_type_name_for_schema(name, s, spec, in_crate),
};
}
let ident = get_type_name(name, data)?;
let t = if in_crate {
quote!(#ident)
} else {
quote!(crate::types::#ident)
};
Ok(t)
}
fn get_type_name_for_array(
name: &str,
a: &openapiv3::ArrayType,
spec: &openapiv3::OpenAPI,
in_crate: bool,
) -> Result<proc_macro2::TokenStream> {
let t = if let Some(ref s) = a.items {
if let Ok(r) = s.reference() {
crate::types::get_type_name_from_reference(&r, spec, in_crate)?
} else {
let item = s.item()?;
get_type_name_for_schema(name, item, spec, in_crate)?
}
} else {
anyhow::bail!(
"no items in array, cannot get type name: {} => {:?}",
name,
a
);
};
Ok(quote!(Vec<#t>))
}
fn render_enum_object_internal(
name: &str,
o: &openapiv3::ObjectType,
spec: &openapiv3::OpenAPI,
ignore_key: &str,
) -> Result<proc_macro2::TokenStream> {
let proper_name = proper_name(name);
let struct_name = format_ident!("{}", proper_name);
if let Some(components) = &spec.components {
if let Some(schema) = components.schemas.get(name) {
if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(existing)) =
&schema.expand(spec)?.schema_kind
{
let mut modified_properties = o.properties.clone();
modified_properties.remove(ignore_key);
if modified_properties == existing.properties {
return Ok(quote!(#struct_name(#struct_name)));
}
}
}
}
let mut values = quote!();
for (k, v) in &o.properties {
if k == ignore_key {
continue;
}
let mut type_name = if let openapiv3::ReferenceOr::Item(i) = v {
get_type_name_for_schema(k, i, spec, true)?
} else {
get_type_name_from_reference(&v.reference()?, spec, true)?
};
if !o.required.contains(k) && !type_name.is_option()? {
type_name = quote!(Option<#type_name>);
}
let prop_ident = format_ident!("{}", k);
let prop_value = quote!(
#prop_ident: #type_name,
);
values = quote!(
#values
#prop_value
);
}
let rendered = quote! {
#struct_name {
#values
}
};
Ok(rendered)
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TagContent {
tag: Option<String>,
content: Option<String>,
}
fn get_one_of_tag(
one_ofs: &Vec<openapiv3::ReferenceOr<openapiv3::Schema>>,
spec: &openapiv3::OpenAPI,
) -> Result<TagContent> {
let mut result: TagContent = Default::default();
for one_of in one_ofs {
let schema = one_of.get_schema_from_reference(spec, true)?;
if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(o)) = schema.schema_kind {
for (k, v) in &o.properties {
let inner_schema = if let openapiv3::ReferenceOr::Item(i) = v {
let s = &**i;
s.clone()
} else {
v.get_schema_from_reference(spec, true)?
};
if let openapiv3::SchemaKind::Type(openapiv3::Type::String(s)) =
inner_schema.schema_kind
{
if s.enumeration.len() == 1 {
result.tag = Some(k.to_string());
}
}
}
}
}
let mut has_content = false;
if let Some(tag) = &result.tag {
for one_of in one_ofs {
let schema = one_of.get_schema_from_reference(spec, true)?;
if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(o)) = schema.schema_kind {
if o.properties.len() == 2 {
for (k, _) in &o.properties {
if tag != k {
if has_content {
if Some(k.to_string()) != result.content {
result.content = None;
return Ok(result);
}
} else {
has_content = true;
result.content = Some(k.to_string());
}
}
}
} else {
result.content = None;
return Ok(result);
}
} else {
result.content = None;
return Ok(result);
}
}
}
Ok(result)
}
pub fn clean_property_name(s: &str) -> String {
let mut prop = s.trim().to_string();
if prop == "+1" {
prop = "plus_one".to_string()
} else if prop == "-1" {
prop = "minus_one".to_string()
} else if prop == "_links" {
prop = "underscore_links".to_string()
}
prop = inflector::cases::snakecase::to_snake_case(&prop);
if prop == "ref"
|| prop == "type"
|| prop == "self"
|| prop == "box"
|| prop == "match"
|| prop == "foo"
|| prop == "enum"
|| prop == "const"
|| prop == "use"
|| prop == "async"
{
prop = format!("{}_", prop);
} else if prop == "$ref" || prop == "$type" {
prop = format!("{}_", prop.replace('$', ""));
} else if prop.starts_with('@') {
prop = prop.trim_start_matches('@').to_string();
} else if prop.starts_with('_') {
prop = prop.trim_start_matches('_').to_string();
}
prop
}
pub fn proper_name(s: &str) -> String {
if s.is_empty() {
return "Empty".to_string();
}
let s = if let Ok(num) = s.parse::<i32>() {
num.cardinal()
} else {
s.to_string()
};
let first_char = s.chars().next().unwrap();
let s = if let Ok(num) = first_char.to_string().parse::<i32>() {
if s.len() == 1 {
num.cardinal()
} else if !s.chars().nth(1).unwrap().is_numeric() {
s.replace(first_char, &num.cardinal())
} else {
s
}
} else {
s
};
inflector::cases::pascalcase::to_pascal_case(&s)
.trim_start_matches("CrateTypes")
.trim_start_matches("VecCrateTypes")
.trim_start_matches("OptionCrateTypes")
.replace("V1", "")
}
fn get_type_name(name: &str, data: &openapiv3::SchemaData) -> Result<proc_macro2::Ident> {
let t = format_ident!(
"{}",
if !name.is_empty() {
proper_name(name)
} else if let Some(title) = &data.title {
proper_name(title)
} else {
anyhow::bail!("Cannot get type name without name or title: {:?}", data);
}
);
Ok(t)
}
fn clean_text(s: &str) -> String {
if cfg!(not(windows)) {
let regex = regex::Regex::new(r#"(})(\n\s{0,8}[^} ])"#).unwrap();
regex.replace_all(s, "$1\n$2").to_string()
} else {
let regex = regex::Regex::new(r#"(})(\r\n\s{0,8}[^} ])"#).unwrap();
regex.replace_all(s, "$1\r\n$2").to_string()
}
}
pub fn get_text(output: &proc_macro2::TokenStream) -> Result<String> {
let content = output.to_string();
Ok(clean_text(&content).replace(' ', ""))
}
pub fn get_text_fmt(output: &proc_macro2::TokenStream) -> Result<String> {
let content = rustfmt_wrapper::rustfmt(output).unwrap();
Ok(clean_text(&content))
}
fn get_base64_mod() -> Result<proc_macro2::TokenStream> {
let file = include_str!("base64.rs");
let stream = proc_macro2::TokenStream::from_str(file).map_err(|e| anyhow::anyhow!("{}", e))?;
Ok(quote!(
pub mod base64 {
#stream
}
))
}
fn get_paginate_mod() -> Result<proc_macro2::TokenStream> {
let file = include_str!("paginate.rs");
let stream = proc_macro2::TokenStream::from_str(file).map_err(|e| anyhow::anyhow!("{}", e))?;
Ok(quote!(
pub mod paginate {
#stream
}
))
}
fn get_phone_number_mod() -> Result<proc_macro2::TokenStream> {
let file = include_str!("phone_number.rs");
let stream = proc_macro2::TokenStream::from_str(file).map_err(|e| anyhow::anyhow!("{}", e))?;
Ok(quote!(
pub mod phone_number {
#stream
}
))
}
fn get_error_mod() -> Result<proc_macro2::TokenStream> {
let file = include_str!("error.rs");
let stream = proc_macro2::TokenStream::from_str(file).map_err(|e| anyhow::anyhow!("{}", e))?;
Ok(quote!(
pub mod error {
#stream
}
))
}
#[derive(Debug, Clone, Default)]
pub struct PaginationProperties {
pub next_page: Option<(String, proc_macro2::TokenStream)>,
pub page_param: Option<(String, proc_macro2::TokenStream)>,
pub items: Option<(String, proc_macro2::TokenStream)>,
pub path: Option<String>,
pub method: Option<http::Method>,
}
impl PaginationProperties {
pub fn from_object(o: &openapiv3::ObjectType, spec: &openapiv3::OpenAPI) -> Result<Self> {
let mut properties = PaginationProperties::default();
for (k, v) in &o.properties {
let prop = crate::types::clean_property_name(k);
let inner_schema = if let openapiv3::ReferenceOr::Item(i) = v {
let s = &**i;
s.clone()
} else {
v.get_schema_from_reference(spec, true)?
};
let mut type_name =
crate::types::get_type_name_for_schema(&prop, &inner_schema, spec, true)?;
let type_name_str = crate::types::get_text(&type_name)?;
if !o.required.contains(k) && !type_name.is_option()? {
type_name = quote!(Option<#type_name>);
}
if is_pagination_property_next_page(&prop) {
properties.next_page = Some((prop, type_name));
} else if is_pagination_property_items(&prop, &type_name)? {
let ident = format_ident!(
"{}",
type_name_str
.trim_start_matches("Vec<")
.trim_end_matches('>')
);
properties.items = Some((prop, quote!(#ident)));
}
}
Ok(properties)
}
pub fn from_operation(
name: &str,
method: &http::Method,
op: &openapiv3::Operation,
spec: &openapiv3::OpenAPI,
) -> Result<Self> {
if method != http::Method::GET {
return Ok(PaginationProperties::default());
}
let mut schema = None;
for (status_code, response) in &op.responses.responses {
if status_code.is_success() {
let response = response.expand(spec)?;
for (_name, content) in &response.content {
if let Some(s) = &content.schema {
schema = Some(s.get_schema_from_reference(spec, true)?);
break;
}
}
}
}
let mut page_param = None;
for param in &op.parameters {
let param = param.expand(spec)?;
if let openapiv3::Parameter::Query {
parameter_data,
style: _,
allow_reserved: _,
allow_empty_value: _,
} = param
{
let s = parameter_data.format.schema()?;
let mut t = match s {
openapiv3::ReferenceOr::Reference { .. } => {
crate::types::get_type_name_from_reference(&s.reference()?, spec, true)?
}
openapiv3::ReferenceOr::Item(s) => crate::types::get_type_name_for_schema(
¶meter_data.name,
&s,
spec,
true,
)?,
};
if !parameter_data.required && !t.is_option()? {
t = quote!(Option<#t>);
}
if is_pagination_property_param_page(¶meter_data.name) {
page_param = Some((parameter_data.name.to_string(), t.clone()));
}
}
}
let schema = if let Some(schema) = schema {
schema
} else {
return Ok(PaginationProperties::default());
};
let mut properties =
if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(o)) = &schema.schema_kind {
PaginationProperties::from_object(o, spec)?
} else {
return Ok(PaginationProperties::default());
};
properties.path = Some(name.to_string());
properties.method = Some(method.clone());
properties.page_param = page_param;
Ok(properties)
}
pub fn can_paginate(&self) -> bool {
self.next_page.is_some() && self.items.is_some()
}
pub fn item_type(&self, in_crate: bool) -> Result<proc_macro2::TokenStream> {
if let Some((_k, v)) = &self.items {
if in_crate {
return Ok(v.clone());
} else {
return Ok(quote!(crate::types::#v));
}
}
anyhow::bail!("No item type found")
}
pub fn item_ident(&self) -> Result<proc_macro2::Ident> {
if let Some((k, _v)) = &self.items {
return Ok(format_ident!("{}", k));
}
anyhow::bail!("No item type found")
}
pub fn next_page_str(&self) -> Result<String> {
if let Some((k, _v)) = &self.next_page {
return Ok(k.to_string());
}
anyhow::bail!("No next page property found")
}
pub fn page_param_str(&self) -> Result<String> {
if let Some((k, _v)) = &self.page_param {
return Ok(k.to_string());
}
anyhow::bail!("No next page property found")
}
}
fn is_pagination_property_next_page(s: &str) -> bool {
["next_page", "next"].contains(&s)
}
fn is_pagination_property_param_page(s: &str) -> bool {
["page_token", "page"].contains(&s)
}
fn is_pagination_property_items(s: &str, t: &proc_macro2::TokenStream) -> Result<bool> {
Ok(["items", "data"].contains(&s) && get_text(t)?.starts_with("Vec<"))
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
#[test]
fn test_generate_kittycad_types() {
let result = super::generate_types(
&crate::load_json_spec(include_str!("../../../spec.json")).unwrap(),
)
.unwrap();
expectorate::assert_contents("tests/types/kittycad.rs.gen", &result.render().unwrap());
}
#[test]
#[ignore] fn test_generate_github_types() {
let result = super::generate_types(
&crate::load_json_spec(include_str!("../../tests/api.github.com.json")).unwrap(),
)
.unwrap();
expectorate::assert_contents("tests/types/github.rs.gen", &result.render().unwrap());
}
#[test]
fn test_generate_oxide_types() {
let result = super::generate_types(
&crate::load_json_spec(include_str!("../../tests/oxide.json")).unwrap(),
)
.unwrap();
expectorate::assert_contents("tests/types/oxide.rs.gen", &result.render().unwrap());
}
#[test]
fn test_proper_name_number() {
assert_eq!(super::proper_name("1"), "One");
assert_eq!(super::proper_name("2"), "Two");
assert_eq!(super::proper_name("100"), "OneHundred");
assert_eq!(super::proper_name("2FaDisabled"), "TwoFaDisabled");
}
#[test]
fn test_proper_name_kebab() {
assert_eq!(super::proper_name("kebab-case"), "KebabCase");
assert_eq!(
super::proper_name("webhook-config-insecure-ssl"),
"WebhookConfigInsecureSsl"
);
}
#[test]
fn test_clean_property_name() {
assert_eq!(super::clean_property_name("+1"), "plus_one");
assert_eq!(super::clean_property_name("-1"), "minus_one");
}
#[test]
fn test_schema_parsing_with_refs() {
let schema = include_str!("../../tests/types/input/RouterRoute.json");
let schema = serde_json::from_str::<openapiv3::Schema>(schema).unwrap();
let mut type_space = super::TypeSpace {
types: indexmap::map::IndexMap::new(),
spec: crate::load_json_spec(include_str!("../../tests/oxide.json")).unwrap(),
rendered: quote!(),
};
type_space.render_schema("RouterRoute", &schema).unwrap();
expectorate::assert_contents(
"tests/types/oxide.router-route.rs.gen",
&super::get_text_fmt(&type_space.rendered).unwrap(),
);
}
#[test]
fn test_schema_parsing_one_of_with_titles() {
let schema = r##"{
"oneOf": [
{
"title": "v4",
"allOf": [
{
"$ref": "#/components/schemas/Ipv4Net"
}
]
},
{
"title": "v6",
"allOf": [
{
"$ref": "#/components/schemas/Ipv6Net"
}
]
}
]
}"##;
let schema = serde_json::from_str::<openapiv3::Schema>(schema).unwrap();
let mut type_space = super::TypeSpace {
types: indexmap::map::IndexMap::new(),
spec: crate::load_json_spec(include_str!("../../tests/oxide.json")).unwrap(),
rendered: quote!(),
};
type_space.render_schema("IpNet", &schema).unwrap();
expectorate::assert_contents(
"tests/types/oxide.ip-net.rs.gen",
&super::get_text_fmt(&type_space.rendered).unwrap(),
);
}
#[test]
fn test_schema_parsing_one_of_with_tag_content() {
let schema = include_str!("../../tests/types/input/VpcFirewallRuleTarget.json");
let schema = serde_json::from_str::<openapiv3::Schema>(schema).unwrap();
let mut type_space = super::TypeSpace {
types: indexmap::map::IndexMap::new(),
spec: crate::load_json_spec(include_str!("../../tests/oxide.json")).unwrap(),
rendered: quote!(),
};
type_space
.render_schema("VpcFirewallRuleTarget", &schema)
.unwrap();
expectorate::assert_contents(
"tests/types/oxide.vpc-filewall-rule-target.rs.gen",
&super::get_text_fmt(&type_space.rendered).unwrap(),
);
}
#[test]
fn test_schema_parsing_one_of_with_tag_no_content() {
let schema = include_str!("../../tests/types/input/AsyncApiCallOutput.json");
let schema = serde_json::from_str::<openapiv3::Schema>(schema).unwrap();
let mut type_space = super::TypeSpace {
types: indexmap::map::IndexMap::new(),
spec: crate::load_json_spec(include_str!("../../../spec.json")).unwrap(),
rendered: quote!(),
};
type_space
.render_schema("AsyncApiCallOutput", &schema)
.unwrap();
expectorate::assert_contents(
"tests/types/kittycad.async-api-call-output.rs.gen",
&super::get_text_fmt(&type_space.rendered).unwrap(),
);
}
#[test]
fn test_schema_parsing_one_of_enum_needs_gen() {
let schema = r##"{
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"sha256"
]
},
"value": {
"type": "string"
}
},
"required": [
"type",
"value"
]
}
]
}"##;
let schema = serde_json::from_str::<openapiv3::Schema>(schema).unwrap();
let mut type_space = super::TypeSpace {
types: indexmap::map::IndexMap::new(),
spec: crate::load_json_spec(include_str!("../../tests/oxide.json")).unwrap(),
rendered: quote!(),
};
type_space.render_schema("Digest", &schema).unwrap();
expectorate::assert_contents(
"tests/types/oxide.digest.rs.gen",
&super::get_text_fmt(&type_space.rendered).unwrap(),
);
}
}