Skip to content

Commit a415d6b

Browse files
authored
fix: add default support (#89)
1 parent 0d2da15 commit a415d6b

19 files changed

Lines changed: 793 additions & 23 deletions

crates/oapi-codegen/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ prettyplease = "0.3.0"
2424
proc-macro2 = "1.0.107"
2525
quote = "1.0.47"
2626
serde = { version = "1.0.229", features = ["derive"] }
27+
serde_json = "1.0.151"
2728
serde_yaml = "0.9.34"
2829
syn = { version = "3.0.3", features = ["full"] }
2930

@@ -36,7 +37,6 @@ openapiv3 = "2.2.0"
3637
percent-encoding = "2.3.2"
3738
reqwest = { version = "0.13.4", default-features = false, features = ["blocking", "json", "multipart", "query", "form"] }
3839
serde = { version = "1.0.229", features = ["derive"] }
39-
serde_json = "1.0.151"
4040
serde_urlencoded = "0.7.1"
4141
trycmd = "1.2.1"
4242
uuid = { version = "1.24.0", features = ["serde"] }

crates/oapi-codegen/src/console.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ fn hints_for(err: &Error) -> Vec<String> {
281281
| Error::OperationTypeCollision { hint, .. }
282282
| Error::SchemaNameCollision { hint, .. }
283283
| Error::RecursiveAlias { hint, .. }
284+
| Error::UnsupportedDefault { hint, .. }
284285
| Error::OperationNameCollision { hint, .. }
285286
| Error::InvalidTypeNameSuffix { hint, .. } => {
286287
return vec![hint.clone()];

crates/oapi-codegen/src/emit/models.rs

Lines changed: 171 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
//! Emitting model items (structs, enums, aliases) as token streams.
22
33
use proc_macro2::TokenStream;
4+
use quote::format_ident;
45
use quote::quote;
56

67
use crate::emit::doc_attr;
78
use crate::emit::emit_type;
89
use crate::error::Result;
910
use crate::ir::Alias;
11+
use crate::ir::DefaultValue;
1012
use crate::ir::Deprecation;
1113
use crate::ir::Enum;
1214
use crate::ir::EnumKind;
1315
use crate::ir::Field;
1416
use crate::ir::ForeignDerives;
1517
use crate::ir::Item;
18+
use crate::ir::RustType;
1619
use crate::ir::StringVariant;
1720
use crate::ir::Struct;
1821
use crate::ir::UnionVariant;
22+
use crate::naming::RustIdent;
1923

2024
/// The full derive set for one generated model: its serde traits, plus which of
2125
/// `Debug`, `Clone`, `PartialEq` the model can carry.
@@ -169,9 +173,23 @@ pub(crate) fn emit_struct(strukt: &Struct, derives: ModelDerives) -> Result<Toke
169173
let has_serde = serde.serialize || serde.deserialize;
170174

171175
let mut fields = Vec::with_capacity(strukt.fields.len());
176+
let mut defaults = Vec::new();
172177
for field in &strukt.fields {
173-
fields.push(emit_field(field, has_serde)?);
178+
fields.push(emit_field(field, serde, &strukt.name)?);
179+
// Only the `Deserialize` derive reads `default`, so only it calls these
180+
// functions. Emitted beside any other derive set, they are dead code.
181+
if let (Some(value), true) = (&field.default, serde.deserialize) {
182+
defaults.push(emit_default_fn(field, value)?);
183+
}
174184
}
185+
// serde needs a path to call. An associated function keeps these out of the
186+
// crate root, where every generated type lives. Field names are unique
187+
// within a struct, so the names built from them are too.
188+
let defaults = if defaults.is_empty() {
189+
quote! {}
190+
} else {
191+
quote! { impl #name { #(#defaults)* } }
192+
};
175193

176194
let additional = match &strukt.additional_properties {
177195
Some(element) => {
@@ -210,13 +228,75 @@ pub(crate) fn emit_struct(strukt: &Struct, derives: ModelDerives) -> Result<Toke
210228
#(#fields)*
211229
#additional
212230
}
231+
232+
#defaults
233+
});
234+
}
235+
236+
/// The name that serde calls to fill an absent property.
237+
fn default_fn_name(field: &Field) -> proc_macro2::Ident {
238+
return format_ident!("default_{}", field.name.logical());
239+
}
240+
241+
/// Render the associated function behind `#[serde(default = "..")]`.
242+
fn emit_default_fn(field: &Field, value: &DefaultValue) -> Result<TokenStream> {
243+
let name = default_fn_name(field);
244+
let ty = emit_type(&field.ty)?;
245+
let expr = emit_default_value(value, &field.ty)?;
246+
let doc = doc_attr(&Some(format!(
247+
"The `default` the document gives `{}`.",
248+
field.name.logical()
249+
)));
250+
return Ok(quote! {
251+
#doc
252+
fn #name() -> #ty {
253+
#expr
254+
}
213255
});
214256
}
215257

258+
/// Render a default as an expression of the field type.
259+
fn emit_default_value(value: &DefaultValue, ty: &RustType) -> Result<TokenStream> {
260+
match ty {
261+
RustType::Option(inner) => {
262+
let inner = emit_default_value(value, inner)?;
263+
return Ok(quote! { Some(#inner) });
264+
}
265+
RustType::Boxed(inner) => {
266+
let inner = emit_default_value(value, inner)?;
267+
return Ok(quote! { Box::new(#inner) });
268+
}
269+
_ => {}
270+
}
271+
let expr = match value {
272+
DefaultValue::Str(text) => quote! { #text.to_owned() },
273+
// No suffix, so one arm serves both `i32` and `i64`. A whole number
274+
// still reads as a float where the field is one.
275+
DefaultValue::Int(number) => {
276+
let literal = proc_macro2::Literal::i64_unsuffixed(*number);
277+
quote! { #literal }
278+
}
279+
DefaultValue::Float(number) => {
280+
let literal = proc_macro2::Literal::f64_unsuffixed(*number);
281+
quote! { #literal }
282+
}
283+
DefaultValue::Bool(flag) => quote! { #flag },
284+
DefaultValue::Variant(variant) => {
285+
let owner = emit_type(ty)?;
286+
let variant = variant.to_token();
287+
quote! { #owner::#variant }
288+
}
289+
// The return type pins this to the right empty collection.
290+
DefaultValue::Empty => quote! { Default::default() },
291+
};
292+
return Ok(expr);
293+
}
294+
216295
/// Render a single struct field. When the struct derives no serde trait,
217296
/// `#[serde(..)]` attributes are suppressed — without a serde derive macro in
218297
/// scope they are orphaned and fail to compile.
219-
fn emit_field(field: &Field, has_serde: bool) -> Result<TokenStream> {
298+
fn emit_field(field: &Field, serde: SerdeDerives, owner: &RustIdent) -> Result<TokenStream> {
299+
let has_serde = serde.serialize || serde.deserialize;
220300
let name = field.name.to_token();
221301
let ty = emit_type(&field.ty)?;
222302
let doc = doc_attr(&field.doc);
@@ -233,6 +313,12 @@ fn emit_field(field: &Field, has_serde: bool) -> Result<TokenStream> {
233313
if omit_empty && field.ty.is_option() {
234314
metas.push(quote! { skip_serializing_if = "Option::is_none" });
235315
}
316+
if field.default.is_some() && serde.deserialize {
317+
// `to_token` keeps any `r#` prefix. A path without it does not
318+
// compile.
319+
let path = format!("{}::{}", owner.to_token(), default_fn_name(field));
320+
metas.push(quote! { default = #path });
321+
}
236322
}
237323
let serde_attr = if !has_serde || metas.is_empty() {
238324
quote! {}
@@ -324,3 +410,86 @@ fn emit_alias(alias: &Alias) -> Result<TokenStream> {
324410
pub type #name = #ty;
325411
});
326412
}
413+
414+
#[cfg(test)]
415+
mod tests {
416+
use super::*;
417+
use crate::naming::Case;
418+
use crate::naming::to_ident;
419+
420+
/// One struct, `Widget`, with one field that carries a `default`.
421+
fn widget_with_a_default() -> Struct {
422+
return Struct {
423+
name: to_ident("Widget", Case::Pascal),
424+
doc: None,
425+
deprecated: None,
426+
fields: vec![Field {
427+
name: to_ident("count", Case::Snake),
428+
rename: None,
429+
doc: None,
430+
deprecated: None,
431+
ty: RustType::I64,
432+
required: false,
433+
omit_empty: None,
434+
serde_skip: false,
435+
default: Some(DefaultValue::Int(10)),
436+
}],
437+
additional_properties: None,
438+
deny_unknown_fields: false,
439+
};
440+
}
441+
442+
fn rendered(serde: SerdeDerives) -> String {
443+
let derives = ModelDerives {
444+
serde,
445+
foreign: ForeignDerives {
446+
debug: true,
447+
clone: true,
448+
partial_eq: true,
449+
},
450+
};
451+
return emit_struct(&widget_with_a_default(), derives)
452+
.expect("this struct renders")
453+
.to_string();
454+
}
455+
456+
/// Only the `Deserialize` derive reads `default`. Beside any other derive
457+
/// set the function has no caller, and the generated crate warns.
458+
#[test]
459+
fn a_default_function_needs_the_deserialize_derive() {
460+
let cases = [
461+
(
462+
SerdeDerives {
463+
serialize: true,
464+
deserialize: true,
465+
},
466+
true,
467+
),
468+
(
469+
SerdeDerives {
470+
serialize: false,
471+
deserialize: true,
472+
},
473+
true,
474+
),
475+
(
476+
SerdeDerives {
477+
serialize: true,
478+
deserialize: false,
479+
},
480+
false,
481+
),
482+
(
483+
SerdeDerives {
484+
serialize: false,
485+
deserialize: false,
486+
},
487+
false,
488+
),
489+
];
490+
for (serde, is_emitted) in cases {
491+
let code = rendered(serde);
492+
assert_eq!(code.contains("default_count"), is_emitted, "{code}");
493+
}
494+
}
495+
}

crates/oapi-codegen/src/emit/usage.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ mod tests {
455455
required: true,
456456
omit_empty: None,
457457
serde_skip: false,
458+
default: None,
458459
};
459460
}
460461

crates/oapi-codegen/src/error.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,19 @@ pub enum Error {
168168
hint: String,
169169
},
170170

171+
/// The schema `default` does not fit the Rust type of the field. Either the
172+
/// two disagree, or the value has no literal form here. A dropped default
173+
/// leaves the document and the code in disagreement.
174+
UnsupportedDefault {
175+
/// The type that owns the property.
176+
owner: String,
177+
/// The property's name as the document writes it.
178+
property: String,
179+
/// The offending `default`, as JSON.
180+
declared: String,
181+
hint: String,
182+
},
183+
171184
/// A parameter declared `in: path` has no matching `{placeholder}` in the
172185
/// operation's path template. An OpenAPI path parameter must appear in the
173186
/// path, and lowering it from the template will otherwise silently drop it
@@ -394,6 +407,17 @@ impl std::fmt::Display for Error {
394407
"the {location} of `{method} {path}` declares only content types the generator cannot represent: {declared}"
395408
);
396409
}
410+
Error::UnsupportedDefault {
411+
owner,
412+
property,
413+
declared,
414+
..
415+
} => {
416+
return write!(
417+
f,
418+
"the `default` of `{owner}.{property}` cannot be represented as a value of the property's Rust type: {declared}"
419+
);
420+
}
397421
Error::InvalidPathParameter { method, path, name } => {
398422
return write!(
399423
f,
@@ -480,6 +504,7 @@ impl std::error::Error for Error {
480504
| Error::UnsupportedSpecVersion { .. }
481505
| Error::UnsupportedSpecKey { .. }
482506
| Error::UnsupportedContentType { .. }
507+
| Error::UnsupportedDefault { .. }
483508
| Error::UnresolvedRef(_)
484509
| Error::UnsupportedRef { .. }
485510
| Error::UnsupportedSchema { .. }

crates/oapi-codegen/src/ir.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,38 @@ pub struct Field {
8585
pub omit_empty: Option<bool>,
8686
/// `x-rust-serde-skip`: drop the field from (de)serialization via `#[serde(skip)]`.
8787
pub serde_skip: bool,
88+
/// The value that serde uses when the property is absent, from `default`.
89+
///
90+
/// An optional property with a default is *not* wrapped in `Option`. Once
91+
/// parsed, it always holds a value. Only `nullable` keeps the `Option`,
92+
/// because there `null` is a value that the property can carry.
93+
///
94+
/// `default: null` never reaches here. The parser reads it as no default at
95+
/// all, and serde already leaves a missing `Option` as `None`.
96+
pub default: Option<DefaultValue>,
97+
}
98+
99+
/// The value that serde uses when a property is absent.
100+
///
101+
/// Lowered from the schema `default` and already checked against the field type.
102+
/// The JSON value alone is not enough to write Rust. `1` is `1` for an integer
103+
/// field and `1.0` for a number field. `"active"` is a string for a `String`
104+
/// field and a variant path for an enum.
105+
#[derive(Debug, Clone, PartialEq)]
106+
pub enum DefaultValue {
107+
/// A string literal.
108+
Str(String),
109+
/// An integer literal.
110+
Int(i64),
111+
/// A floating-point literal.
112+
Float(f64),
113+
/// A `bool` literal.
114+
Bool(bool),
115+
/// A unit variant of a generated string enum, named by its identifier.
116+
Variant(RustIdent),
117+
/// An empty collection, which `Default::default()` gives for both `Vec` and
118+
/// `HashMap`.
119+
Empty,
88120
}
89121

90122
/// A generated `enum`.

0 commit comments

Comments
 (0)