Skip to main content

openapi_trait_shared/codegen/
types.rs

1use heck::ToPascalCase;
2use openapiv3::{
3    AdditionalProperties, IntegerFormat, NumberFormat, ObjectType, ReferenceOr, Schema, SchemaKind,
4    StringFormat, Type,
5};
6use proc_macro2::TokenStream;
7use quote::{format_ident, quote};
8
9/// Map an `OpenAPI` `Schema` (or `$ref`) to a Rust type `TokenStream`.
10///
11/// `required` controls whether the result is wrapped in `Option<T>`.
12///
13/// This is the context-free entry point: any inline `oneOf` / `allOf` / `anyOf`
14/// encountered along the way falls back to `serde_json::Value`. Use
15/// [`schema_to_rust_type_ctx`] when a parent name is available so that inline
16/// compositions can be synthesized into named top-level types.
17#[must_use]
18pub fn schema_to_rust_type(ref_or: &ReferenceOr<Schema>, required: bool) -> TokenStream {
19    let mut sink: Vec<TokenStream> = Vec::new();
20    schema_to_rust_type_ctx(ref_or, required, None, &mut sink)
21    // sink is discarded — by definition no parent name means no synthesis.
22}
23
24/// Context-aware variant of [`schema_to_rust_type`].
25///
26/// When `parent_name` is `Some` and an inline composition is encountered, a
27/// top-level type definition is appended to `inline_types` (`parent_name` is
28/// used verbatim as the type ident) and the returned token stream references
29/// that ident.
30#[must_use]
31pub fn schema_to_rust_type_ctx(
32    ref_or: &ReferenceOr<Schema>,
33    required: bool,
34    parent_name: Option<&str>,
35    inline_types: &mut Vec<TokenStream>,
36) -> TokenStream {
37    let inner = ref_or_to_inner_type_ctx(ref_or, parent_name, inline_types);
38    if required {
39        inner
40    } else {
41        quote! { ::core::option::Option<#inner> }
42    }
43}
44
45/// Resolve a `$ref` or inline schema to its Rust type, threading inline-type
46/// synthesis context through.
47fn ref_or_to_inner_type_ctx(
48    ref_or: &ReferenceOr<Schema>,
49    parent_name: Option<&str>,
50    inline_types: &mut Vec<TokenStream>,
51) -> TokenStream {
52    match ref_or {
53        ReferenceOr::Reference { reference } => ref_to_ident(reference),
54        ReferenceOr::Item(schema) => schema_kind_to_type(schema, parent_name, inline_types),
55    }
56}
57
58#[must_use]
59pub fn ref_to_ident(reference: &str) -> TokenStream {
60    // "#/components/schemas/Foo" -> Foo
61    let name = reference.rsplit('/').next().unwrap_or(reference);
62    let ident = format_ident!("{}", name.to_pascal_case());
63    quote! { #ident }
64}
65
66/// Convert a schema to a Rust type, synthesizing a top-level composition type
67/// when `parent_name` is provided and the schema is a composition.
68fn schema_kind_to_type(
69    schema: &Schema,
70    parent_name: Option<&str>,
71    inline_types: &mut Vec<TokenStream>,
72) -> TokenStream {
73    match &schema.schema_kind {
74        SchemaKind::Type(Type::Object(obj)) => {
75            object_schema_to_type(schema, obj, parent_name, inline_types)
76        }
77        SchemaKind::Type(t) => primitive_type_to_rust(t, parent_name, inline_types),
78        SchemaKind::OneOf { one_of } => {
79            synthesize_inline_composition(parent_name, inline_types, |name, sink| {
80                super::compositions::generate_one_of(
81                    name,
82                    one_of,
83                    schema.schema_data.discriminator.as_ref(),
84                    schema.schema_data.description.as_ref(),
85                    sink,
86                )
87            })
88        }
89        SchemaKind::AnyOf { any_of } => {
90            synthesize_inline_composition(parent_name, inline_types, |name, sink| {
91                super::compositions::generate_any_of(
92                    name,
93                    any_of,
94                    schema.schema_data.description.as_ref(),
95                    sink,
96                )
97            })
98        }
99        SchemaKind::AllOf { all_of } => {
100            synthesize_inline_composition(parent_name, inline_types, |name, sink| {
101                super::compositions::generate_all_of(
102                    name,
103                    all_of,
104                    schema.schema_data.description.as_ref(),
105                    sink,
106                )
107            })
108        }
109        SchemaKind::Not { .. } | SchemaKind::Any(_) => {
110            // Intentionally unsupported: emit untyped JSON.
111            quote! { ::serde_json::Value }
112        }
113    }
114}
115
116/// Either synthesize a top-level composition type (when a parent name is
117/// available) and return a reference to it, or fall back to
118/// `serde_json::Value`.
119fn synthesize_inline_composition(
120    parent_name: Option<&str>,
121    inline_types: &mut Vec<TokenStream>,
122    generate: impl FnOnce(&str, &mut Vec<TokenStream>) -> TokenStream,
123) -> TokenStream {
124    parent_name.map_or_else(
125        || quote! { ::serde_json::Value },
126        |name| {
127            let tokens = generate(name, inline_types);
128            inline_types.push(tokens);
129            let ident = format_ident!("{}", name.to_pascal_case());
130            quote! { #ident }
131        },
132    )
133}
134
135/// Convert a primitive `OpenAPI` type to a Rust type token stream.
136fn primitive_type_to_rust(
137    t: &Type,
138    parent_name: Option<&str>,
139    inline_types: &mut Vec<TokenStream>,
140) -> TokenStream {
141    match t {
142        Type::Integer(i) => {
143            if i.format == openapiv3::VariantOrUnknownOrEmpty::Item(IntegerFormat::Int32) {
144                quote! { i32 }
145            } else {
146                quote! { i64 }
147            }
148        }
149        Type::Number(n) => {
150            if n.format == openapiv3::VariantOrUnknownOrEmpty::Item(NumberFormat::Float) {
151                quote! { f32 }
152            } else {
153                quote! { f64 }
154            }
155        }
156        Type::String(s) => {
157            // For string enums, the caller handles the dedicated enum type;
158            // here we just return String as a fallback.
159            if s.enumeration.is_empty() {
160                if matches!(
161                    &s.format,
162                    openapiv3::VariantOrUnknownOrEmpty::Item(StringFormat::Binary)
163                ) {
164                    quote! { ::std::vec::Vec<u8> }
165                } else {
166                    quote! { ::std::string::String }
167                }
168            } else {
169                quote! { ::std::string::String }
170            }
171        }
172        Type::Boolean(_) => quote! { bool },
173        Type::Array(a) => {
174            let item_ty = a.items.as_ref().map_or_else(
175                || quote! { ::serde_json::Value },
176                |items| ref_or_to_inner_type_ctx(&items.clone().unbox(), parent_name, inline_types),
177            );
178            quote! { ::std::vec::Vec<#item_ty> }
179        }
180        // Objects are handled in `schema_kind_to_type`, which has the full
181        // schema (description, synthesis context) available.
182        Type::Object(_) => quote! { ::serde_json::Value },
183    }
184}
185
186/// Convert an object schema to a Rust type.
187///
188/// - An object that declares `properties` is synthesized into a named top-level
189///   struct (via [`super::schemas::generate_object_struct`]) when a
190///   `parent_name` is available; the returned token stream references it.
191/// - An object with no declared `properties` but an `additionalProperties`
192///   entry is a map and becomes `HashMap<String, T>`.
193/// - Anything else (e.g. a free-form object with no schema info, or no parent
194///   name to synthesize against) falls back to untyped JSON.
195fn object_schema_to_type(
196    schema: &Schema,
197    obj: &ObjectType,
198    parent_name: Option<&str>,
199    inline_types: &mut Vec<TokenStream>,
200) -> TokenStream {
201    if !obj.properties.is_empty() {
202        return synthesize_inline_composition(parent_name, inline_types, |name, sink| {
203            super::schemas::generate_object_struct(name, schema, obj, sink)
204        });
205    }
206    if let Some(ap) = &obj.additional_properties {
207        if let Some(value_ty) = additional_properties_value_type(ap, parent_name, inline_types) {
208            return quote! {
209                ::std::collections::HashMap<::std::string::String, #value_ty>
210            };
211        }
212    }
213    quote! { ::serde_json::Value }
214}
215
216/// Resolve an `additionalProperties` declaration to the value type `T` of the
217/// resulting `HashMap<String, T>`.
218///
219/// - `additionalProperties: true` → `serde_json::Value` (any value allowed).
220/// - `additionalProperties: false` → `None` (no extra properties; the caller
221///   should not emit a map).
222/// - `additionalProperties: <schema>` → the mapped type of that schema.
223#[must_use]
224pub fn additional_properties_value_type(
225    ap: &AdditionalProperties,
226    parent_name: Option<&str>,
227    inline_types: &mut Vec<TokenStream>,
228) -> Option<TokenStream> {
229    match ap {
230        AdditionalProperties::Any(false) => None,
231        AdditionalProperties::Any(true) => Some(quote! { ::serde_json::Value }),
232        AdditionalProperties::Schema(schema) => {
233            Some(ref_or_to_inner_type_ctx(schema, parent_name, inline_types))
234        }
235    }
236}
237
238/// Returns true when the schema is a string with enumeration values.
239#[must_use]
240pub fn is_string_enum(schema: &Schema) -> bool {
241    if let SchemaKind::Type(Type::String(s)) = &schema.schema_kind {
242        !s.enumeration.is_empty()
243    } else {
244        false
245    }
246}
247
248/// Extract enum values from a string schema (skipping None entries).
249#[must_use]
250pub fn string_enum_values(schema: &Schema) -> Vec<String> {
251    if let SchemaKind::Type(Type::String(s)) = &schema.schema_kind {
252        s.enumeration.iter().filter_map(Clone::clone).collect()
253    } else {
254        vec![]
255    }
256}