diff --git a/bin/configs/manual/rust-axum-recursive-models.yaml b/bin/configs/manual/rust-axum-recursive-models.yaml new file mode 100644 index 000000000000..c063a4b47998 --- /dev/null +++ b/bin/configs/manual/rust-axum-recursive-models.yaml @@ -0,0 +1,13 @@ +generatorName: rust-axum +outputDir: samples/server/petstore/rust-axum/output/rust-axum-recursive-models +inputSpec: modules/openapi-generator/src/test/resources/3_1/rust-axum/rust-axum-recursive-models.yaml +templateDir: modules/openapi-generator/src/main/resources/rust-axum +generateAliasAsModel: true +additionalProperties: + hideGenerationTimestamp: "true" + packageName: rust-axum-recursive-models + boxRecursiveModels: "true" + homePageUrl: https://github.com/openapitools/openapi-generator +globalProperties: + skipFormModel: "false" +enablePostProcessFile: true diff --git a/docs/generators/rust-axum.md b/docs/generators/rust-axum.md index 19d87abc1240..59e43528386f 100644 --- a/docs/generators/rust-axum.md +++ b/docs/generators/rust-axum.md @@ -20,6 +20,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl | ------ | ----------- | ------ | ------- | |allowBlockingResponseSerialize|By default, json/form-urlencoded response serialization, which might perform a lot of compute in a future without yielding, is executed on a blocking thread via tokio::task::spawn_blocking. Set this option to true will override this behaviour and allow blocking call to happen. It helps to improve the performance when response serialization (e.g. returns tiny data) is low cost.| |false| |allowBlockingValidator|By default, validation process, which might perform a lot of compute in a future without yielding, is executed on a blocking thread via tokio::task::spawn_blocking. Set this option to true will override this behaviour and allow blocking call to happen. It helps to improve the performance when validating request-data (header, path, query, body) is low cost.| |false| +|boxRecursiveModels|Wrap model fields that lead back to their own model (directly or through other models) in Box, so recursive models compile. Changes the type of those fields.| |false| |disableValidator|Disable validating request-data (header, path, query, body) against OpenAPI Schema Specification.| |false| |packageName|Rust crate name (convention: snake_case).| |openapi| |packageVersion|Rust crate version.| |null| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustAxumServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustAxumServerCodegen.java index a60cf5f2c7c2..8575f85a8eec 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustAxumServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustAxumServerCodegen.java @@ -68,6 +68,7 @@ public class RustAxumServerCodegen extends AbstractRustCodegen implements Codege private Boolean basicAuthorization = false; private Boolean basicAnalytic = false; private Boolean ownedRequest = false; + private Boolean boxRecursiveModels = false; // Types private static final String uuidType = "uuid::Uuid"; @@ -251,6 +252,12 @@ public RustAxumServerCodegen() { optAllowBlockingResponseSerialize.setType("bool"); optAllowBlockingResponseSerialize.defaultValue(allowBlockingResponseSerialize.toString()); + CliOption optBoxRecursiveModels = new CliOption("boxRecursiveModels", + String.join("", "Wrap model fields that lead back to their own model (directly or through other ", + "models) in Box, so recursive models compile. Changes the type of those fields.")); + optBoxRecursiveModels.setType("bool"); + optBoxRecursiveModels.defaultValue(boxRecursiveModels.toString()); + cliOptions = new ArrayList<>( List.of( new CliOption(CodegenConstants.PACKAGE_NAME, @@ -260,7 +267,8 @@ public RustAxumServerCodegen() { "Rust crate version."), optDisableValidator, optAllowBlockingValidator, - optAllowBlockingResponseSerialize + optAllowBlockingResponseSerialize, + optBoxRecursiveModels ) ); @@ -359,6 +367,12 @@ public void processOpts() { } else { additionalProperties.put("ownedRequest", ownedRequest); } + + if (additionalProperties.containsKey("boxRecursiveModels")) { + boxRecursiveModels = convertPropertyToBooleanAndWriteBack("boxRecursiveModels"); + } else { + additionalProperties.put("boxRecursiveModels", boxRecursiveModels); + } } private void setPackageName(final String packageName) { @@ -1437,6 +1451,70 @@ public ModelsMap postProcessModels(ModelsMap modelsMap) { return super.postProcessModelsEnum(modelsMap); } + @Override + public Map postProcessAllModels(Map objs) { + objs = super.postProcessAllModels(objs); + if (!boxRecursiveModels) { + return objs; + } + + // Model fields that lead back to their own model (directly or through other models) + // would make the struct infinitely sized, so they are boxed. + final Map models = new HashMap<>(); + for (ModelsMap modelsMap : objs.values()) { + for (ModelMap mo : modelsMap.getModels()) { + models.put(mo.getModel().classname, mo.getModel()); + } + } + + for (CodegenModel cm : models.values()) { + for (CodegenProperty var : cm.vars) { + final String target = directModelReference(var); + if (target != null && reachesModel(target, cm.classname, models, new HashSet<>())) { + var.vendorExtensions.put("x-is-boxed", true); + } + } + } + + return objs; + } + + /** + * Returns the name of the model a property holds inline (i.e. not inside a container), or null. + */ + private static String directModelReference(CodegenProperty property) { + if (property.isContainer || !property.dataType.startsWith("models::")) { + return null; + } + return property.dataType.substring("models::".length()); + } + + private static boolean reachesModel(String from, String to, Map models, Set visited) { + if (from.equals(to)) { + return true; + } + final CodegenModel cm = models.get(from); + if (cm == null || !visited.add(from)) { + return false; + } + // oneOf/anyOf models are enums that hold their variants inline + final List fields = new ArrayList<>(cm.vars); + final CodegenComposedSchemas cs = cm.getComposedSchemas(); + if (cs != null && cs.getOneOf() != null) { + fields.addAll(cs.getOneOf()); + } + if (cs != null && cs.getAnyOf() != null) { + fields.addAll(cs.getAnyOf()); + } + for (CodegenProperty var : fields) { + final String target = directModelReference(var); + if (target != null && reachesModel(target, to, models, visited)) { + return true; + } + } + return false; + } + @Override public void postProcessFile(File file, String fileType) { super.postProcessFile(file, fileType); diff --git a/modules/openapi-generator/src/main/resources/rust-axum/models.mustache b/modules/openapi-generator/src/main/resources/rust-axum/models.mustache index 0c29edad984d..46558cfa5b37 100644 --- a/modules/openapi-generator/src/main/resources/rust-axum/models.mustache +++ b/modules/openapi-generator/src/main/resources/rust-axum/models.mustache @@ -1111,7 +1111,7 @@ pub struct {{{classname}}} { {{/isNullable}} {{/hasValidation}} {{#required}} - pub {{{name}}}: {{#isNullable}}Nullable<{{/isNullable}}{{{dataType}}}{{#isNullable}}>{{/isNullable}}, + pub {{{name}}}: {{#isNullable}}Nullable<{{/isNullable}}{{#vendorExtensions.x-is-boxed}}Box<{{/vendorExtensions.x-is-boxed}}{{{dataType}}}{{#vendorExtensions.x-is-boxed}}>{{/vendorExtensions.x-is-boxed}}{{#isNullable}}>{{/isNullable}}, {{/required}} {{^required}} {{#isNullable}} @@ -1119,7 +1119,7 @@ pub struct {{{classname}}} { #[serde(default = "default_optional_nullable")] {{/isNullable}} #[serde(skip_serializing_if="Option::is_none")] - pub {{{name}}}: Option<{{#isNullable}}Nullable<{{/isNullable}}{{{dataType}}}{{#isNullable}}>{{/isNullable}}>, + pub {{{name}}}: Option<{{#isNullable}}Nullable<{{/isNullable}}{{#vendorExtensions.x-is-boxed}}Box<{{/vendorExtensions.x-is-boxed}}{{{dataType}}}{{#vendorExtensions.x-is-boxed}}>{{/vendorExtensions.x-is-boxed}}{{#isNullable}}>{{/isNullable}}>, {{/required}} {{/vars}} @@ -1187,7 +1187,7 @@ fn validate_byte_{{#lambda.lowercase}}{{{classname}}}_{{{name}}}{{/lambda.lowerc impl {{{classname}}} { #[allow(clippy::new_without_default, clippy::too_many_arguments)] - pub fn new({{#vars}}{{^isDiscriminator}}{{^defaultValue}}{{{name}}}: {{#isNullable}}Nullable<{{/isNullable}}{{{dataType}}}{{#isNullable}}>{{/isNullable}}, {{/defaultValue}}{{/isDiscriminator}}{{#isDiscriminator}}{{#vendorExtensions.x-discriminator-multi}}{{{name}}}: {{{dataType}}}, {{/vendorExtensions.x-discriminator-multi}}{{^vendorExtensions.x-discriminator-multi}}{{^isString}}{{^vendorExtensions.x-discriminator-enum-variant}}{{^defaultValue}}{{{name}}}: {{#isNullable}}Nullable<{{/isNullable}}{{{dataType}}}{{#isNullable}}>{{/isNullable}}, {{/defaultValue}}{{/vendorExtensions.x-discriminator-enum-variant}}{{/isString}}{{/vendorExtensions.x-discriminator-multi}}{{/isDiscriminator}}{{/vars}}) -> {{{classname}}} { + pub fn new({{#vars}}{{^isDiscriminator}}{{^defaultValue}}{{{name}}}: {{#isNullable}}Nullable<{{/isNullable}}{{#vendorExtensions.x-is-boxed}}Box<{{/vendorExtensions.x-is-boxed}}{{{dataType}}}{{#vendorExtensions.x-is-boxed}}>{{/vendorExtensions.x-is-boxed}}{{#isNullable}}>{{/isNullable}}, {{/defaultValue}}{{/isDiscriminator}}{{#isDiscriminator}}{{#vendorExtensions.x-discriminator-multi}}{{{name}}}: {{{dataType}}}, {{/vendorExtensions.x-discriminator-multi}}{{^vendorExtensions.x-discriminator-multi}}{{^isString}}{{^vendorExtensions.x-discriminator-enum-variant}}{{^defaultValue}}{{{name}}}: {{#isNullable}}Nullable<{{/isNullable}}{{{dataType}}}{{#isNullable}}>{{/isNullable}}, {{/defaultValue}}{{/vendorExtensions.x-discriminator-enum-variant}}{{/isString}}{{/vendorExtensions.x-discriminator-multi}}{{/isDiscriminator}}{{/vars}}) -> {{{classname}}} { {{{classname}}} { {{#vars}} {{^isDiscriminator}}{{#defaultValue}}{{{name}}}: {{{defaultValue}}}{{/defaultValue}}{{^defaultValue}}{{{name}}}{{/defaultValue}}{{/isDiscriminator}}{{#isDiscriminator}}{{#vendorExtensions.x-discriminator-multi}}{{{name}}}{{/vendorExtensions.x-discriminator-multi}}{{^vendorExtensions.x-discriminator-multi}}{{#isString}}{{{name}}}: Self::_name_for_{{{name}}}(){{/isString}}{{^isString}}{{#vendorExtensions.x-discriminator-enum-variant}}{{{name}}}: Self::_name_for_{{{name}}}(){{/vendorExtensions.x-discriminator-enum-variant}}{{^vendorExtensions.x-discriminator-enum-variant}}{{#defaultValue}}{{{name}}}: {{{defaultValue}}}{{/defaultValue}}{{^defaultValue}}{{{name}}}{{/defaultValue}}{{/vendorExtensions.x-discriminator-enum-variant}}{{/isString}}{{/vendorExtensions.x-discriminator-multi}}{{/isDiscriminator}}, {{/vars}} @@ -1335,7 +1335,7 @@ impl std::str::FromStr for {{{classname}}} { {{{name}}}: std::result::Result::Err("Nullable types not supported in {{{classname}}}".to_string())?, {{/isNullable}} {{^isNullable}} - {{{name}}}: intermediate_rep.{{{name}}}.into_iter().next(){{#required}}.ok_or_else(|| "{{{baseName}}} missing in {{{classname}}}".to_string())?{{/required}}, + {{{name}}}: intermediate_rep.{{{name}}}.into_iter().next(){{#vendorExtensions.x-is-boxed}}.map(Box::new){{/vendorExtensions.x-is-boxed}}{{#required}}.ok_or_else(|| "{{{baseName}}} missing in {{{classname}}}".to_string())?{{/required}}, {{/isNullable}} {{/vars}} }) diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustAxumServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustAxumServerCodegenTest.java index b49b28e7c2fe..07268866fef7 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustAxumServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustAxumServerCodegenTest.java @@ -153,4 +153,54 @@ public void testDiscriminatorReferencingEnum() throws IOException { TestUtils.assertFileContains(modelsPath, "String::from(\"car\")"); TestUtils.assertFileContains(Path.of(target.toString(), "/src/types.rs"), "macro_rules! impl_deserialize_tagged"); } + + @Test + public void testRecursiveModelFieldsAreBoxed() throws IOException { + Path target = Files.createTempDirectory("test"); + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("rust-axum") + .setInputSpec("src/test/resources/3_1/rust-axum/rust-axum-recursive-models.yaml") + .addAdditionalProperty("boxRecursiveModels", true) + .setSkipOverwrite(false) + .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/")); + List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + Path modelsPath = Path.of(target.toString(), "/src/models.rs"); + TestUtils.assertFileExists(modelsPath); + // Direct self reference + TestUtils.assertFileContains(modelsPath, "pub parent: Option>,"); + TestUtils.assertFileContains(modelsPath, "parent: intermediate_rep.parent.into_iter().next().map(Box::new),"); + // Containers already add indirection + TestUtils.assertFileContains(modelsPath, "pub children: Option>,"); + // Nullable self reference (anyOf/oneOf with null) + TestUtils.assertFileContains(modelsPath, "pub manager: Nullable>,"); + TestUtils.assertFileContains(modelsPath, "pub mentor: Option>>,"); + TestUtils.assertFileContains(modelsPath, "pub fn new(name: String, manager: Nullable>, ) -> Employee {"); + // Indirect cycle Employee -> Department -> Employee + TestUtils.assertFileContains(modelsPath, "pub department: Option>,"); + TestUtils.assertFileContains(modelsPath, "pub head: Option>,"); + // Cycle through a oneOf variant Circle -> Shape -> Circle + TestUtils.assertFileContains(modelsPath, "pub inner: Option>,"); + // Non-recursive references are left as they are + TestUtils.assertFileContains(modelsPath, "pub owner: Option,"); + } + + @Test + public void testRecursiveModelFieldsAreNotBoxedByDefault() throws IOException { + Path target = Files.createTempDirectory("test"); + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("rust-axum") + .setInputSpec("src/test/resources/3_1/rust-axum/rust-axum-recursive-models.yaml") + .setSkipOverwrite(false) + .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/")); + List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + Path modelsPath = Path.of(target.toString(), "/src/models.rs"); + TestUtils.assertFileExists(modelsPath); + TestUtils.assertFileContains(modelsPath, "pub parent: Option,"); + TestUtils.assertFileContains(modelsPath, "pub manager: Nullable,"); + TestUtils.assertFileNotContains(modelsPath, "Box(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } +} +``` diff --git a/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/apis/default.rs b/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/apis/default.rs new file mode 100644 index 000000000000..8c1d0d45e395 --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/apis/default.rs @@ -0,0 +1,82 @@ +use async_trait::async_trait; +use axum::extract::*; +use axum_extra::extract::CookieJar; +use bytes::Bytes; +use headers::Host; +use http::Method; +use serde::{Deserialize, Serialize}; + +use crate::{models, types::*}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum GetEmployeeResponse { + /// OK + Status200_OK(models::Employee), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum GetNodeResponse { + /// OK + Status200_OK(models::Node), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum GetPetResponse { + /// OK + Status200_OK(models::Pet), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum GetShapeResponse { + /// OK + Status200_OK(models::Shape), +} + +/// Default +#[async_trait] +#[allow(clippy::ptr_arg)] +pub trait Default: super::ErrorHandler { + /// GetEmployee - GET /employees + async fn get_employee( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + ) -> Result; + + /// GetNode - GET /nodes + async fn get_node( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + ) -> Result; + + /// GetPet - GET /pets + async fn get_pet( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + ) -> Result; + + /// GetShape - GET /shapes + async fn get_shape( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + ) -> Result; +} diff --git a/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/apis/mod.rs b/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/apis/mod.rs new file mode 100644 index 000000000000..da9c9e1b91fa --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/apis/mod.rs @@ -0,0 +1,21 @@ +pub mod default; + +// Error handler for unhandled errors. +#[async_trait::async_trait] +pub trait ErrorHandler { + #[allow(unused_variables)] + #[tracing::instrument(skip_all)] + async fn handle_error( + &self, + method: &::http::Method, + host: &headers::Host, + cookies: &axum_extra::extract::CookieJar, + error: E, + ) -> Result { + tracing::error!("Unhandled error: {:?}", error); + axum::response::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .body(axum::body::Body::empty()) + .map_err(|_| http::StatusCode::INTERNAL_SERVER_ERROR) + } +} diff --git a/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/header.rs b/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/header.rs new file mode 100644 index 000000000000..970a99da88e4 --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/header.rs @@ -0,0 +1,188 @@ +use std::{convert::TryFrom, fmt, ops::Deref}; + +use chrono::{DateTime, Utc}; +use http::HeaderValue; + +/// A struct to allow homogeneous conversion into a HeaderValue. We can't +/// implement the From/Into trait on HeaderValue because we don't own +/// either of the types. +#[derive(Debug, Clone)] +pub(crate) struct IntoHeaderValue(pub T); + +// Generic implementations + +impl Deref for IntoHeaderValue { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +// Derive for each TryFrom in http::HeaderValue + +macro_rules! ihv_generate { + ($t:ident) => { + impl TryFrom for IntoHeaderValue<$t> { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => match hdr_value.parse::<$t>() { + Ok(hdr_value) => Ok(IntoHeaderValue(hdr_value)), + Err(e) => Err(format!( + r#"Unable to parse {} as a string: {e}"#, + stringify!($t) + )), + }, + Err(e) => Err(format!( + r#"Unable to parse header {hdr_value:?} as a string - {e}"# + )), + } + } + } + + impl TryFrom> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue<$t>) -> Result { + Ok(hdr_value.0.into()) + } + } + }; +} + +ihv_generate!(u64); +ihv_generate!(i64); +ihv_generate!(i16); +ihv_generate!(u16); +ihv_generate!(u32); +ihv_generate!(usize); +ihv_generate!(isize); +ihv_generate!(i32); + +// Custom derivations + +// Vec + +impl TryFrom for IntoHeaderValue> { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => Ok(IntoHeaderValue( + hdr_value + .split(',') + .filter_map(|x| match x.trim() { + "" => None, + y => Some(y.to_string()), + }) + .collect(), + )), + Err(e) => Err(format!( + r#"Unable to parse header: {hdr_value:?} as a string - {e}"# + )), + } + } +} + +impl TryFrom>> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue>) -> Result { + match HeaderValue::from_str(&hdr_value.0.join(", ")) { + Ok(hdr_value) => Ok(hdr_value), + Err(e) => Err(format!( + r#"Unable to convert {hdr_value:?} into a header - {e}"# + )), + } + } +} + +// String + +impl TryFrom for IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => Ok(IntoHeaderValue(hdr_value.to_string())), + Err(e) => Err(format!(r#"Unable to convert header {hdr_value:?} to {e}"#)), + } + } +} + +impl TryFrom> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue) -> Result { + match HeaderValue::from_str(&hdr_value.0) { + Ok(hdr_value) => Ok(hdr_value), + Err(e) => Err(format!( + r#"Unable to convert {hdr_value:?} from a header {e}"# + )), + } + } +} + +// Bool + +impl TryFrom for IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => match hdr_value.parse() { + Ok(hdr_value) => Ok(IntoHeaderValue(hdr_value)), + Err(e) => Err(format!(r#"Unable to parse bool from {hdr_value} - {e}"#)), + }, + Err(e) => Err(format!( + r#"Unable to convert {hdr_value:?} from a header {e}"# + )), + } + } +} + +impl TryFrom> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue) -> Result { + match HeaderValue::from_str(&hdr_value.0.to_string()) { + Ok(hdr_value) => Ok(hdr_value), + Err(e) => Err(format!( + r#"Unable to convert: {hdr_value:?} into a header: {e}"# + )), + } + } +} + +// DateTime + +impl TryFrom for IntoHeaderValue> { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => match DateTime::parse_from_rfc3339(hdr_value) { + Ok(date) => Ok(IntoHeaderValue(date.with_timezone(&Utc))), + Err(e) => Err(format!(r#"Unable to parse: {hdr_value} as date - {e}"#)), + }, + Err(e) => Err(format!( + r#"Unable to convert header {hdr_value:?} to string {e}"# + )), + } + } +} + +impl TryFrom>> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue>) -> Result { + match HeaderValue::from_str(hdr_value.0.to_rfc3339().as_str()) { + Ok(hdr_value) => Ok(hdr_value), + Err(e) => Err(format!( + r#"Unable to convert {hdr_value:?} to a header: {e}"# + )), + } + } +} diff --git a/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/lib.rs b/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/lib.rs new file mode 100644 index 000000000000..09763630f1ac --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/lib.rs @@ -0,0 +1,32 @@ +#![allow( + missing_docs, + trivial_casts, + unused_variables, + unused_mut, + unused_extern_crates, + non_camel_case_types, + unused_imports, + unused_attributes +)] +#![allow( + clippy::derive_partial_eq_without_eq, + clippy::disallowed_names, + clippy::too_many_arguments +)] + +#[cfg(feature = "server")] +extern crate futures_util; + +pub const BASE_PATH: &str = ""; +pub const API_VERSION: &str = "1.0.0"; + +#[cfg(feature = "server")] +pub mod server; + +#[cfg(feature = "server")] +pub mod apis; +pub mod models; +pub mod types; + +#[cfg(feature = "server")] +pub(crate) mod header; diff --git a/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/models.rs b/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/models.rs new file mode 100644 index 000000000000..5271f9c2718a --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/models.rs @@ -0,0 +1,1182 @@ +#![allow(unused_qualifications)] + +#[cfg(feature = "server")] +use http::HeaderValue; +use validator::Validate; + +#[cfg(feature = "server")] +use crate::header; +use crate::{models, types::*}; + +#[cfg(feature = "server")] +#[allow(dead_code)] +pub type SSE = std::pin::Pin< + std::boxed::Box< + dyn futures_util::Stream< + Item = std::result::Result, + > + std::marker::Send + + std::marker::Sync, + >, +>; + +#[allow(dead_code)] +fn from_validation_error(e: validator::ValidationError) -> validator::ValidationErrors { + let mut errs = validator::ValidationErrors::new(); + errs.add("na", e); + errs +} + +/// XSS detection relies on `ammonia`, which is only pulled in by the `server` feature. +/// Without it (e.g. models-only WASM builds), values are never flagged as HTML. +#[cfg(feature = "server")] +fn contains_html(v: &str) -> bool { + ammonia::is_html(v) +} + +#[cfg(not(feature = "server"))] +fn contains_html(_v: &str) -> bool { + false +} + +#[allow(dead_code)] +pub fn check_xss_string(v: &str) -> std::result::Result<(), validator::ValidationError> { + if contains_html(v) { + std::result::Result::Err(validator::ValidationError::new("xss detected")) + } else { + std::result::Result::Ok(()) + } +} + +#[allow(dead_code)] +pub fn check_xss_vec_string(v: &[String]) -> std::result::Result<(), validator::ValidationError> { + if v.iter().any(|i| contains_html(i)) { + std::result::Result::Err(validator::ValidationError::new("xss detected")) + } else { + std::result::Result::Ok(()) + } +} + +#[allow(dead_code)] +pub fn check_xss_map_string( + v: &std::collections::HashMap, +) -> std::result::Result<(), validator::ValidationError> { + if v.keys().any(|k| contains_html(k)) || v.values().any(|v| contains_html(v)) { + std::result::Result::Err(validator::ValidationError::new("xss detected")) + } else { + std::result::Result::Ok(()) + } +} + +#[allow(dead_code)] +pub fn check_xss_map_nested( + v: &std::collections::HashMap, +) -> std::result::Result<(), validator::ValidationError> +where + T: validator::Validate, +{ + if v.keys().any(|k| contains_html(k)) || v.values().any(|v| v.validate().is_err()) { + std::result::Result::Err(validator::ValidationError::new("xss detected")) + } else { + std::result::Result::Ok(()) + } +} + +#[allow(dead_code)] +pub fn check_xss_map( + v: &std::collections::HashMap, +) -> std::result::Result<(), validator::ValidationError> { + if v.keys().any(|k| contains_html(k)) { + std::result::Result::Err(validator::ValidationError::new("xss detected")) + } else { + std::result::Result::Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct Circle { + #[serde(rename = "radius")] + #[serde(skip_serializing_if = "Option::is_none")] + pub radius: Option, + + #[serde(rename = "inner")] + #[validate(nested)] + #[serde(skip_serializing_if = "Option::is_none")] + pub inner: Option>, +} + +impl Circle { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new() -> Circle { + Circle { + radius: None, + inner: None, + } + } +} + +/// Converts the Circle value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for Circle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + self.radius + .as_ref() + .map(|radius| ["radius".to_string(), radius.to_string()].join(",")), + // Skipping inner in query parameter serialization + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a Circle value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for Circle { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub radius: Vec, + pub inner: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing Circle".to_string(), + ) + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "radius" => intermediate_rep.radius.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "inner" => intermediate_rep.inner.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing Circle".to_string(), + ) + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(Circle { + radius: intermediate_rep.radius.into_iter().next(), + inner: intermediate_rep.inner.into_iter().next().map(Box::new), + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Invalid header value for Circle - value: {hdr_value} is invalid {e}"# + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + r#"Unable to convert header value '{value}' into Circle - {err}"# + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Unable to convert header: {hdr_value:?} to string: {e}"# + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct Department { + #[serde(rename = "name")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + #[serde(rename = "head")] + #[validate(nested)] + #[serde(skip_serializing_if = "Option::is_none")] + pub head: Option>, +} + +impl Department { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new() -> Department { + Department { + name: None, + head: None, + } + } +} + +/// Converts the Department value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for Department { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + self.name + .as_ref() + .map(|name| ["name".to_string(), name.to_string()].join(",")), + // Skipping head in query parameter serialization + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a Department value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for Department { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub name: Vec, + pub head: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing Department".to_string(), + ) + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "name" => intermediate_rep.name.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "head" => intermediate_rep.head.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing Department".to_string(), + ) + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(Department { + name: intermediate_rep.name.into_iter().next(), + head: intermediate_rep.head.into_iter().next().map(Box::new), + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Invalid header value for Department - value: {hdr_value} is invalid {e}"# + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + r#"Unable to convert header value '{value}' into Department - {err}"# + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Unable to convert header: {hdr_value:?} to string: {e}"# + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct Employee { + #[serde(rename = "name")] + #[validate(custom(function = "check_xss_string"))] + pub name: String, + + #[serde(rename = "manager")] + pub manager: Nullable>, + + #[serde(rename = "mentor")] + #[serde(deserialize_with = "deserialize_optional_nullable")] + #[serde(default = "default_optional_nullable")] + #[serde(skip_serializing_if = "Option::is_none")] + pub mentor: Option>>, + + #[serde(rename = "department")] + #[validate(nested)] + #[serde(skip_serializing_if = "Option::is_none")] + pub department: Option>, +} + +impl Employee { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new(name: String, manager: Nullable>) -> Employee { + Employee { + name, + manager, + mentor: None, + department: None, + } + } +} + +/// Converts the Employee value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for Employee { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + Some("name".to_string()), + Some(self.name.to_string()), + // Skipping manager in query parameter serialization + + // Skipping mentor in query parameter serialization + + // Skipping department in query parameter serialization + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a Employee value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for Employee { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub name: Vec, + pub manager: Vec, + pub mentor: Vec, + pub department: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing Employee".to_string(), + ) + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "name" => intermediate_rep.name.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + "manager" => { + return std::result::Result::Err( + "Parsing a nullable type in this style is not supported in Employee" + .to_string(), + ) + } + "mentor" => { + return std::result::Result::Err( + "Parsing a nullable type in this style is not supported in Employee" + .to_string(), + ) + } + #[allow(clippy::redundant_clone)] + "department" => intermediate_rep.department.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing Employee".to_string(), + ) + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(Employee { + name: intermediate_rep + .name + .into_iter() + .next() + .ok_or_else(|| "name missing in Employee".to_string())?, + manager: std::result::Result::Err( + "Nullable types not supported in Employee".to_string(), + )?, + mentor: std::result::Result::Err( + "Nullable types not supported in Employee".to_string(), + )?, + department: intermediate_rep.department.into_iter().next().map(Box::new), + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Invalid header value for Employee - value: {hdr_value} is invalid {e}"# + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + r#"Unable to convert header value '{value}' into Employee - {err}"# + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Unable to convert header: {hdr_value:?} to string: {e}"# + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct Node { + #[serde(rename = "name")] + #[validate(custom(function = "check_xss_string"))] + pub name: String, + + #[serde(rename = "parent")] + #[validate(nested)] + #[serde(skip_serializing_if = "Option::is_none")] + pub parent: Option>, + + #[serde(rename = "children")] + #[validate(nested)] + #[serde(skip_serializing_if = "Option::is_none")] + pub children: Option>, +} + +impl Node { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new(name: String) -> Node { + Node { + name, + parent: None, + children: None, + } + } +} + +/// Converts the Node value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for Node { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + Some("name".to_string()), + Some(self.name.to_string()), + // Skipping parent in query parameter serialization + + // Skipping children in query parameter serialization + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a Node value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for Node { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub name: Vec, + pub parent: Vec, + pub children: Vec>, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err("Missing value while parsing Node".to_string()) + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "name" => intermediate_rep.name.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "parent" => intermediate_rep.parent.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + "children" => { + return std::result::Result::Err( + "Parsing a container in this style is not supported in Node" + .to_string(), + ) + } + _ => { + return std::result::Result::Err( + "Unexpected key while parsing Node".to_string(), + ) + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(Node { + name: intermediate_rep + .name + .into_iter() + .next() + .ok_or_else(|| "name missing in Node".to_string())?, + parent: intermediate_rep.parent.into_iter().next().map(Box::new), + children: intermediate_rep.children.into_iter().next(), + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Invalid header value for Node - value: {hdr_value} is invalid {e}"# + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + r#"Unable to convert header value '{value}' into Node - {err}"# + )), + }, + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Unable to convert header: {hdr_value:?} to string: {e}"# + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct Owner { + #[serde(rename = "name")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +impl Owner { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new() -> Owner { + Owner { name: None } + } +} + +/// Converts the Owner value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for Owner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![self + .name + .as_ref() + .map(|name| ["name".to_string(), name.to_string()].join(","))]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a Owner value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for Owner { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub name: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing Owner".to_string(), + ) + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "name" => intermediate_rep.name.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing Owner".to_string(), + ) + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(Owner { + name: intermediate_rep.name.into_iter().next(), + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Invalid header value for Owner - value: {hdr_value} is invalid {e}"# + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + r#"Unable to convert header value '{value}' into Owner - {err}"# + )), + }, + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Unable to convert header: {hdr_value:?} to string: {e}"# + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct Pet { + #[serde(rename = "name")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + #[serde(rename = "owner")] + #[validate(nested)] + #[serde(skip_serializing_if = "Option::is_none")] + pub owner: Option, +} + +impl Pet { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new() -> Pet { + Pet { + name: None, + owner: None, + } + } +} + +/// Converts the Pet value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for Pet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + self.name + .as_ref() + .map(|name| ["name".to_string(), name.to_string()].join(",")), + // Skipping owner in query parameter serialization + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a Pet value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for Pet { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub name: Vec, + pub owner: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err("Missing value while parsing Pet".to_string()) + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "name" => intermediate_rep.name.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "owner" => intermediate_rep.owner.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing Pet".to_string(), + ) + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(Pet { + name: intermediate_rep.name.into_iter().next(), + owner: intermediate_rep.owner.into_iter().next(), + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: header::IntoHeaderValue) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Invalid header value for Pet - value: {hdr_value} is invalid {e}"# + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + r#"Unable to convert header value '{value}' into Pet - {err}"# + )), + }, + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Unable to convert header: {hdr_value:?} to string: {e}"# + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +#[allow(non_camel_case_types, clippy::large_enum_variant)] +pub enum Shape { + Circle(models::Circle), + Square(models::Square), +} + +impl validator::Validate for Shape { + fn validate(&self) -> std::result::Result<(), validator::ValidationErrors> { + match self { + Self::Circle(v) => v.validate(), + Self::Square(v) => v.validate(), + } + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a Shape value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for Shape { + type Err = serde_json::Error; + + fn from_str(s: &str) -> std::result::Result { + serde_json::from_str(s) + } +} + +impl From for Shape { + fn from(value: models::Circle) -> Self { + Self::Circle(value) + } +} +impl From for Shape { + fn from(value: models::Square) -> Self { + Self::Square(value) + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct Square { + #[serde(rename = "side")] + #[serde(skip_serializing_if = "Option::is_none")] + pub side: Option, +} + +impl Square { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new() -> Square { + Square { side: None } + } +} + +/// Converts the Square value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for Square { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![self + .side + .as_ref() + .map(|side| ["side".to_string(), side.to_string()].join(","))]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a Square value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for Square { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub side: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing Square".to_string(), + ) + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "side" => intermediate_rep.side.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing Square".to_string(), + ) + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(Square { + side: intermediate_rep.side.into_iter().next(), + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Invalid header value for Square - value: {hdr_value} is invalid {e}"# + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + r#"Unable to convert header value '{value}' into Square - {err}"# + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Unable to convert header: {hdr_value:?} to string: {e}"# + )), + } + } +} diff --git a/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/server/mod.rs b/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/server/mod.rs new file mode 100644 index 000000000000..3a8f40a3e9d0 --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/server/mod.rs @@ -0,0 +1,325 @@ +use std::collections::HashMap; + +use axum::{body::Body, extract::*, response::Response, routing::*}; +use axum_extra::{ + extract::{CookieJar, Query as QueryExtra}, + TypedHeader, +}; +use bytes::Bytes; +use headers::Host; +use http::{header::CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue, Method, StatusCode}; +use tracing::error; +use validator::{Validate, ValidationErrors}; + +#[allow(unused_imports)] +use crate::{apis, models}; +use crate::{header, types::*}; +#[allow(unused_imports)] +use crate::{ + models::check_xss_map, models::check_xss_map_nested, models::check_xss_map_string, + models::check_xss_string, models::check_xss_vec_string, +}; + +/// Setup API Server. +pub fn new(api_impl: I) -> Router +where + I: AsRef + Clone + Send + Sync + 'static, + A: apis::default::Default + Send + Sync + 'static, + E: std::fmt::Debug + Send + Sync + 'static, +{ + // build our application with a route + Router::new() + .route("/employees", get(get_employee::)) + .route("/nodes", get(get_node::)) + .route("/pets", get(get_pet::)) + .route("/shapes", get(get_shape::)) + .with_state(api_impl) +} + +#[tracing::instrument(skip_all)] +fn get_employee_validation() -> std::result::Result<(), ValidationErrors> { + Ok(()) +} +/// GetEmployee - GET /employees +#[tracing::instrument(skip_all)] +async fn get_employee( + method: Method, + TypedHeader(host): TypedHeader, + cookies: CookieJar, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::default::Default + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || get_employee_validation()) + .await + .unwrap(); + + let Ok(()) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .get_employee(&method, &host, &cookies) + .await; + + let resp = match result { + Ok(rsp) => match rsp { + apis::default::GetEmployeeResponse::Status200_OK(body) => { + let mut response = Response::builder(); + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn get_node_validation() -> std::result::Result<(), ValidationErrors> { + Ok(()) +} +/// GetNode - GET /nodes +#[tracing::instrument(skip_all)] +async fn get_node( + method: Method, + TypedHeader(host): TypedHeader, + cookies: CookieJar, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::default::Default + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || get_node_validation()) + .await + .unwrap(); + + let Ok(()) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl.as_ref().get_node(&method, &host, &cookies).await; + + let resp = match result { + Ok(rsp) => match rsp { + apis::default::GetNodeResponse::Status200_OK(body) => { + let mut response = Response::builder(); + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn get_pet_validation() -> std::result::Result<(), ValidationErrors> { + Ok(()) +} +/// GetPet - GET /pets +#[tracing::instrument(skip_all)] +async fn get_pet( + method: Method, + TypedHeader(host): TypedHeader, + cookies: CookieJar, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::default::Default + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || get_pet_validation()) + .await + .unwrap(); + + let Ok(()) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl.as_ref().get_pet(&method, &host, &cookies).await; + + let resp = match result { + Ok(rsp) => match rsp { + apis::default::GetPetResponse::Status200_OK(body) => { + let mut response = Response::builder(); + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn get_shape_validation() -> std::result::Result<(), ValidationErrors> { + Ok(()) +} +/// GetShape - GET /shapes +#[tracing::instrument(skip_all)] +async fn get_shape( + method: Method, + TypedHeader(host): TypedHeader, + cookies: CookieJar, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::default::Default + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || get_shape_validation()) + .await + .unwrap(); + + let Ok(()) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl.as_ref().get_shape(&method, &host, &cookies).await; + + let resp = match result { + Ok(rsp) => match rsp { + apis::default::GetShapeResponse::Status200_OK(body) => { + let mut response = Response::builder(); + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[allow(dead_code)] +#[inline] +fn response_with_status_code_only(code: StatusCode) -> Result { + Response::builder() + .status(code) + .body(Body::empty()) + .map_err(|_| code) +} diff --git a/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/types.rs b/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/types.rs new file mode 100644 index 000000000000..c9a0eaf67365 --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-recursive-models/src/types.rs @@ -0,0 +1,790 @@ +use std::{mem, str::FromStr}; + +use base64::{engine::general_purpose, Engine}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[allow(dead_code)] +pub struct Object(pub serde_json::Value); + +impl validator::Validate for Object { + fn validate(&self) -> Result<(), validator::ValidationErrors> { + Ok(()) + } +} + +impl FromStr for Object { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(Self(serde_json::Value::String(s.to_owned()))) + } +} + +/// Serde helper function to create a default `Option>` while +/// deserializing +pub fn default_optional_nullable() -> Option> { + None +} + +/// Serde helper function to deserialize into an `Option>` +pub fn deserialize_optional_nullable<'de, D, T>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, + T: Deserialize<'de>, +{ + Option::::deserialize(deserializer).map(|val| match val { + Some(inner) => Some(Nullable::Present(inner)), + None => Some(Nullable::Null), + }) +} + +/// The Nullable type. Represents a value which may be specified as null on an API. +/// Note that this is distinct from a value that is optional and not present! +/// +/// Nullable implements many of the same methods as the Option type (map, unwrap, etc). +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub enum Nullable { + /// Null value + Null, + /// Value is present + Present(T), +} + +impl Nullable { + ///////////////////////////////////////////////////////////////////////// + // Querying the contained values + ///////////////////////////////////////////////////////////////////////// + + /// Returns `true` if the Nullable is a `Present` value. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let x: Nullable = Nullable::Present(2); + /// assert_eq!(x.is_present(), true); + /// + /// let x: Nullable = Nullable::Null; + /// assert_eq!(x.is_present(), false); + /// ``` + #[inline] + pub fn is_present(&self) -> bool { + match *self { + Nullable::Present(_) => true, + Nullable::Null => false, + } + } + + /// Returns `true` if the Nullable is a `Null` value. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let x: Nullable = Nullable::Present(2); + /// assert_eq!(x.is_null(), false); + /// + /// let x: Nullable = Nullable::Null; + /// assert_eq!(x.is_null(), true); + /// ``` + #[inline] + pub fn is_null(&self) -> bool { + !self.is_present() + } + + ///////////////////////////////////////////////////////////////////////// + // Adapter for working with references + ///////////////////////////////////////////////////////////////////////// + + /// Converts from `Nullable` to `Nullable<&T>`. + /// + /// # Examples + /// + /// Convert an `Nullable<`[`String`]`>` into a `Nullable<`[`usize`]`>`, preserving the original. + /// The [`map`] method takes the `self` argument by value, consuming the original, + /// so this technique uses `as_ref` to first take a `Nullable` to a reference + /// to the value inside the original. + /// + /// [`map`]: enum.Nullable.html#method.map + /// [`String`]: ../../std/string/struct.String.html + /// [`usize`]: ../../std/primitive.usize.html + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let num_as_str: Nullable = Nullable::Present("10".to_string()); + /// // First, cast `Nullable` to `Nullable<&String>` with `as_ref`, + /// // then consume *that* with `map`, leaving `num_as_str` on the stack. + /// let num_as_int: Nullable = num_as_str.as_ref().map(|n| n.len()); + /// println!("still can print num_as_str: {:?}", num_as_str); + /// ``` + #[inline] + pub fn as_ref(&self) -> Nullable<&T> { + match *self { + Nullable::Present(ref x) => Nullable::Present(x), + Nullable::Null => Nullable::Null, + } + } + + /// Converts from `Nullable` to `Nullable<&mut T>`. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let mut x = Nullable::Present(2); + /// match x.as_mut() { + /// Nullable::Present(v) => *v = 42, + /// Nullable::Null => {}, + /// } + /// assert_eq!(x, Nullable::Present(42)); + /// ``` + #[inline] + pub fn as_mut(&mut self) -> Nullable<&mut T> { + match *self { + Nullable::Present(ref mut x) => Nullable::Present(x), + Nullable::Null => Nullable::Null, + } + } + + ///////////////////////////////////////////////////////////////////////// + // Getting to contained values + ///////////////////////////////////////////////////////////////////////// + + /// Unwraps a Nullable, yielding the content of a `Nullable::Present`. + /// + /// # Panics + /// + /// Panics if the value is a [`Nullable::Null`] with a custom panic message provided by + /// `msg`. + /// + /// [`Nullable::Null`]: #variant.Null + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let x = Nullable::Present("value"); + /// assert_eq!(x.expect("the world is ending"), "value"); + /// ``` + /// + /// ```should_panic + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let x: Nullable<&str> = Nullable::Null; + /// x.expect("the world is ending"); // panics with `the world is ending` + /// ``` + #[inline] + pub fn expect(self, msg: &str) -> T { + match self { + Nullable::Present(val) => val, + Nullable::Null => expect_failed(msg), + } + } + + /// Moves the value `v` out of the `Nullable` if it is `Nullable::Present(v)`. + /// + /// In general, because this function may panic, its use is discouraged. + /// Instead, prefer to use pattern matching and handle the `Nullable::Null` + /// case explicitly. + /// + /// # Panics + /// + /// Panics if the self value equals [`Nullable::Null`]. + /// + /// [`Nullable::Null`]: #variant.Null + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let x = Nullable::Present("air"); + /// assert_eq!(x.unwrap(), "air"); + /// ``` + /// + /// ```should_panic + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.unwrap(), "air"); // fails + /// ``` + #[inline] + pub fn unwrap(self) -> T { + match self { + Nullable::Present(val) => val, + Nullable::Null => panic!("called `Nullable::unwrap()` on a `Nullable::Null` value"), + } + } + + /// Returns the contained value or a default. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// assert_eq!(Nullable::Present("car").unwrap_or("bike"), "car"); + /// assert_eq!(Nullable::Null.unwrap_or("bike"), "bike"); + /// ``` + #[inline] + pub fn unwrap_or(self, def: T) -> T { + match self { + Nullable::Present(x) => x, + Nullable::Null => def, + } + } + + /// Returns the contained value or computes it from a closure. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let k = 10; + /// assert_eq!(Nullable::Present(4).unwrap_or_else(|| 2 * k), 4); + /// assert_eq!(Nullable::Null.unwrap_or_else(|| 2 * k), 20); + /// ``` + #[inline] + pub fn unwrap_or_else T>(self, f: F) -> T { + match self { + Nullable::Present(x) => x, + Nullable::Null => f(), + } + } + + ///////////////////////////////////////////////////////////////////////// + // Transforming contained values + ///////////////////////////////////////////////////////////////////////// + + /// Maps a `Nullable` to `Nullable` by applying a function to a contained value. + /// + /// # Examples + /// + /// Convert a `Nullable<`[`String`]`>` into a `Nullable<`[`usize`]`>`, consuming the original: + /// + /// [`String`]: ../../std/string/struct.String.html + /// [`usize`]: ../../std/primitive.usize.html + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let maybe_some_string = Nullable::Present(String::from("Hello, World!")); + /// // `Nullable::map` takes self *by value*, consuming `maybe_some_string` + /// let maybe_some_len = maybe_some_string.map(|s| s.len()); + /// + /// assert_eq!(maybe_some_len, Nullable::Present(13)); + /// ``` + #[inline] + pub fn map U>(self, f: F) -> Nullable { + match self { + Nullable::Present(x) => Nullable::Present(f(x)), + Nullable::Null => Nullable::Null, + } + } + + /// Applies a function to the contained value (if any), + /// or returns a `default` (if not). + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let x = Nullable::Present("foo"); + /// assert_eq!(x.map_or(42, |v| v.len()), 3); + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.map_or(42, |v| v.len()), 42); + /// ``` + #[inline] + pub fn map_or U>(self, default: U, f: F) -> U { + match self { + Nullable::Present(t) => f(t), + Nullable::Null => default, + } + } + + /// Applies a function to the contained value (if any), + /// or computes a `default` (if not). + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let k = 21; + /// + /// let x = Nullable::Present("foo"); + /// assert_eq!(x.map_or_else(|| 2 * k, |v| v.len()), 3); + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.map_or_else(|| 2 * k, |v| v.len()), 42); + /// ``` + #[inline] + pub fn map_or_else U, F: FnOnce(T) -> U>(self, default: D, f: F) -> U { + match self { + Nullable::Present(t) => f(t), + Nullable::Null => default(), + } + } + + /// Transforms the `Nullable` into a [`Result`], mapping `Nullable::Present(v)` to + /// [`Ok(v)`] and `Nullable::Null` to [`Err(err)`][Err]. + /// + /// [`Result`]: ../../std/result/enum.Result.html + /// [`Ok(v)`]: ../../std/result/enum.Result.html#variant.Ok + /// [Err]: ../../std/result/enum.Result.html#variant.Err + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let x = Nullable::Present("foo"); + /// assert_eq!(x.ok_or(0), Ok("foo")); + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.ok_or(0), Err(0)); + /// ``` + #[inline] + pub fn ok_or(self, err: E) -> Result { + match self { + Nullable::Present(v) => Ok(v), + Nullable::Null => Err(err), + } + } + + /// Transforms the `Nullable` into a [`Result`], mapping `Nullable::Present(v)` to + /// [`Ok(v)`] and `Nullable::Null` to [`Err(err())`][Err]. + /// + /// [`Result`]: ../../std/result/enum.Result.html + /// [`Ok(v)`]: ../../std/result/enum.Result.html#variant.Ok + /// [Err]: ../../std/result/enum.Result.html#variant.Err + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let x = Nullable::Present("foo"); + /// assert_eq!(x.ok_or_else(|| 0), Ok("foo")); + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.ok_or_else(|| 0), Err(0)); + /// ``` + #[inline] + pub fn ok_or_else E>(self, err: F) -> Result { + match self { + Nullable::Present(v) => Ok(v), + Nullable::Null => Err(err()), + } + } + + ///////////////////////////////////////////////////////////////////////// + // Boolean operations on the values, eager and lazy + ///////////////////////////////////////////////////////////////////////// + + /// Returns `Nullable::Null` if the Nullable is `Nullable::Null`, otherwise returns `optb`. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let x = Nullable::Present(2); + /// let y: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.and(y), Nullable::Null); + /// + /// let x: Nullable = Nullable::Null; + /// let y = Nullable::Present("foo"); + /// assert_eq!(x.and(y), Nullable::Null); + /// + /// let x = Nullable::Present(2); + /// let y = Nullable::Present("foo"); + /// assert_eq!(x.and(y), Nullable::Present("foo")); + /// + /// let x: Nullable = Nullable::Null; + /// let y: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.and(y), Nullable::Null); + /// ``` + #[inline] + pub fn and(self, optb: Nullable) -> Nullable { + match self { + Nullable::Present(_) => optb, + Nullable::Null => Nullable::Null, + } + } + + /// Returns `Nullable::Null` if the Nullable is `Nullable::Null`, otherwise calls `f` with the + /// wrapped value and returns the result. + /// + /// Some languages call this operation flatmap. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// fn sq(x: u32) -> Nullable { Nullable::Present(x * x) } + /// fn nope(_: u32) -> Nullable { Nullable::Null } + /// + /// assert_eq!(Nullable::Present(2).and_then(sq).and_then(sq), Nullable::Present(16)); + /// assert_eq!(Nullable::Present(2).and_then(sq).and_then(nope), Nullable::Null); + /// assert_eq!(Nullable::Present(2).and_then(nope).and_then(sq), Nullable::Null); + /// assert_eq!(Nullable::Null.and_then(sq).and_then(sq), Nullable::Null); + /// ``` + #[inline] + pub fn and_then Nullable>(self, f: F) -> Nullable { + match self { + Nullable::Present(x) => f(x), + Nullable::Null => Nullable::Null, + } + } + + /// Returns the Nullable if it contains a value, otherwise returns `optb`. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let x = Nullable::Present(2); + /// let y = Nullable::Null; + /// assert_eq!(x.or(y), Nullable::Present(2)); + /// + /// let x = Nullable::Null; + /// let y = Nullable::Present(100); + /// assert_eq!(x.or(y), Nullable::Present(100)); + /// + /// let x = Nullable::Present(2); + /// let y = Nullable::Present(100); + /// assert_eq!(x.or(y), Nullable::Present(2)); + /// + /// let x: Nullable = Nullable::Null; + /// let y = Nullable::Null; + /// assert_eq!(x.or(y), Nullable::Null); + /// ``` + #[inline] + pub fn or(self, optb: Nullable) -> Nullable { + match self { + Nullable::Present(_) => self, + Nullable::Null => optb, + } + } + + /// Returns the Nullable if it contains a value, otherwise calls `f` and + /// returns the result. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// fn nobody() -> Nullable<&'static str> { Nullable::Null } + /// fn vikings() -> Nullable<&'static str> { Nullable::Present("vikings") } + /// + /// assert_eq!(Nullable::Present("barbarians").or_else(vikings), + /// Nullable::Present("barbarians")); + /// assert_eq!(Nullable::Null.or_else(vikings), Nullable::Present("vikings")); + /// assert_eq!(Nullable::Null.or_else(nobody), Nullable::Null); + /// ``` + #[inline] + pub fn or_else Nullable>(self, f: F) -> Nullable { + match self { + Nullable::Present(_) => self, + Nullable::Null => f(), + } + } + + ///////////////////////////////////////////////////////////////////////// + // Misc + ///////////////////////////////////////////////////////////////////////// + + /// Takes the value out of the Nullable, leaving a `Nullable::Null` in its place. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let mut x = Nullable::Present(2); + /// x.take(); + /// assert_eq!(x, Nullable::Null); + /// + /// let mut x: Nullable = Nullable::Null; + /// x.take(); + /// assert_eq!(x, Nullable::Null); + /// ``` + #[inline] + pub fn take(&mut self) -> Nullable { + mem::replace(self, Nullable::Null) + } +} + +impl Nullable<&T> { + /// Maps an `Nullable<&T>` to an `Nullable` by cloning the contents of the + /// Nullable. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let x = 12; + /// let opt_x = Nullable::Present(&x); + /// assert_eq!(opt_x, Nullable::Present(&12)); + /// let cloned = opt_x.cloned(); + /// assert_eq!(cloned, Nullable::Present(12)); + /// ``` + pub fn cloned(self) -> Nullable { + self.map(Clone::clone) + } +} + +impl Nullable { + /// Returns the contained value or a default + /// + /// Consumes the `self` argument then, if `Nullable::Present`, returns the contained + /// value, otherwise if `Nullable::Null`, returns the default value for that + /// type. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_recursive_models::types::Nullable; + /// + /// let x = Nullable::Present(42); + /// assert_eq!(42, x.unwrap_or_default()); + /// + /// let y: Nullable = Nullable::Null; + /// assert_eq!(0, y.unwrap_or_default()); + /// ``` + #[inline] + pub fn unwrap_or_default(self) -> T { + match self { + Nullable::Present(x) => x, + Nullable::Null => Default::default(), + } + } +} + +impl Default for Nullable { + /// Returns None. + #[inline] + fn default() -> Nullable { + Nullable::Null + } +} + +impl From for Nullable { + fn from(val: T) -> Nullable { + Nullable::Present(val) + } +} + +impl Serialize for Nullable +where + T: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + Nullable::Present(ref inner) => serializer.serialize_some(&inner), + Nullable::Null => serializer.serialize_none(), + } + } +} + +impl<'de, T> Deserialize<'de> for Nullable +where + T: serde::de::DeserializeOwned, +{ + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + // In order to deserialize a required, but nullable, value, we first have to check whether + // the value is present at all. To do this, we deserialize to a serde_json::Value, which + // fails if the value is missing, or gives serde_json::Value::Null if the value is present. + // If that succeeds as null, we can easily return a Null. + // If that succeeds as some value, we deserialize that value and return a Present. + // If that errors, we return the error. + let presence: Result<::serde_json::Value, _> = + serde::Deserialize::deserialize(deserializer); + match presence { + Ok(serde_json::Value::Null) => Ok(Nullable::Null), + Ok(some_value) => serde_json::from_value(some_value) + .map(Nullable::Present) + .map_err(serde::de::Error::custom), + Err(x) => Err(x), + } + } +} + +impl validator::Validate for Nullable +where + T: validator::Validate, +{ + fn validate(&self) -> Result<(), validator::ValidationErrors> { + match self { + Self::Present(x) => x.validate(), + Self::Null => Ok(()), + } + } +} + +impl<'a, T> validator::ValidateArgs<'a> for Nullable +where + T: validator::ValidateArgs<'a>, +{ + type Args = T::Args; + fn validate_with_args(&self, args: Self::Args) -> Result<(), validator::ValidationErrors> { + match self { + Self::Present(x) => x.validate_with_args(args), + Self::Null => Ok(()), + } + } +} + +impl validator::ValidateEmail for Nullable +where + T: validator::ValidateEmail, +{ + fn as_email_string(&'_ self) -> Option> { + match self { + Self::Present(x) => x.as_email_string(), + Self::Null => None, + } + } +} + +impl validator::ValidateUrl for Nullable +where + T: validator::ValidateUrl, +{ + fn as_url_string(&'_ self) -> Option> { + match self { + Self::Present(x) => x.as_url_string(), + Self::Null => None, + } + } +} + +impl validator::ValidateContains for Nullable +where + T: validator::ValidateContains, +{ + fn validate_contains(&self, needle: &str) -> bool { + match self { + Self::Present(x) => x.validate_contains(needle), + Self::Null => true, + } + } +} + +impl validator::ValidateRequired for Nullable +where + T: validator::ValidateRequired, +{ + fn is_some(&self) -> bool { + self.is_present() + } +} + +impl validator::ValidateRegex for Nullable +where + T: validator::ValidateRegex, +{ + fn validate_regex(&self, regex: impl validator::AsRegex) -> bool { + match self { + Self::Present(x) => x.validate_regex(regex), + Self::Null => true, + } + } +} + +impl validator::ValidateRange for Nullable +where + T: validator::ValidateRange, +{ + fn greater_than(&self, max: I) -> Option { + use validator::ValidateRange; + match self { + Self::Present(x) => x.greater_than(max), + Self::Null => None, + } + } + fn less_than(&self, min: I) -> Option { + use validator::ValidateRange; + match self { + Self::Present(x) => x.less_than(min), + Self::Null => None, + } + } +} + +impl validator::ValidateLength for Nullable +where + T: validator::ValidateLength, + I: PartialEq + PartialOrd, +{ + fn length(&self) -> Option { + use validator::ValidateLength; + match self { + Self::Present(x) => x.length(), + Self::Null => None, + } + } +} + +impl From> for Option { + fn from(value: Nullable) -> Option { + match value { + Nullable::Present(x) => Some(x), + Nullable::Null => None, + } + } +} + +#[inline(never)] +#[cold] +fn expect_failed(msg: &str) -> ! { + panic!("{}", msg) +} + +#[derive(Debug, Clone, PartialEq, PartialOrd)] +/// Base64-encoded byte array +pub struct ByteArray(pub Vec); + +impl Serialize for ByteArray { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&general_purpose::STANDARD.encode(&self.0)) + } +} + +impl<'de> Deserialize<'de> for ByteArray { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match general_purpose::STANDARD.decode(s) { + Ok(bin) => Ok(ByteArray(bin)), + _ => Err(serde::de::Error::custom("invalid base64")), + } + } +}