[RUST-AXUM] Fix #18580: disableValidation does not create superfluous code (#18592)

* fix: Respect `disableValidator` CLI option

Setting the `disableValidator` option to `true` used to generate
validation code but avoid calling it from the route handlers.
This generated invalid code in some cases.
This commit renders the `server-operation-validate` only, when the
`disableValidator` option is set to false (the default value).

* test: Update examples and run integration test

The generated samples are updated with:
`./bin/generate-samples.sh ./bin/configs/manual/*.yaml`
The relevant integration test
`mvn integration-test -f samples/server/petstore/rust-axum/pom.xml`
passes.

* test: Add integration test

This integration tests provokes the generation of code that would
not compile (see #18580). The new change that makes sure validation
logic is not rendered when disabled, makes sure the faulty code is not
generated and therefore the project compiles.

* test: Update examples and run integration test

The generated samples are updated with:
`./bin/generate-samples.sh ./bin/configs/manual/*.yaml`

The relevant integration test
`mvn integration-test -f samples/server/petstore/rust-axum/pom.xml`
passes.
This commit is contained in:
myz-dev
2024-05-07 15:02:45 +02:00
committed by GitHub
parent 2d967ccae8
commit 06499605e1
14 changed files with 1245 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
generatorName: rust-axum
outputDir: samples/server/petstore/rust-axum/output/rust-axum-validation-test
inputSpec: modules/openapi-generator/src/test/resources/3_0/rust/rust-axum-validation-test.yaml
templateDir: modules/openapi-generator/src/main/resources/rust-axum
generateAliasAsModel: true
additionalProperties:
hideGenerationTimestamp: "true"
packageName: rust-axum-validation-test
disableValidator: "true"
enablePostProcessFile: true

View File

@@ -8,7 +8,9 @@ use crate::{Api{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}},
{{#apis}}
{{#operations}}
{{#operation}}
{{^disableValidator}}
{{>server-operation-validate}}
{{/disableValidator}}
{{>server-operation}}
{{/operation}}
{{/operations}}

View File

@@ -0,0 +1,23 @@
openapi: 3.0.1
info:
title: Test to check that validation logic is not rendered when disabled.
version: 0.0.1
paths:
/mail:
put:
description: Updates the email.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Email"
responses:
"204":
description: OK.
components:
schemas:
Email:
type: string
pattern: '^[\w\-\.]+@([\w-]+\.)+[\w-]{2,}$'
example: example@example.com

View File

@@ -0,0 +1,2 @@
target
Cargo.lock

View File

@@ -0,0 +1,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md

View File

@@ -0,0 +1,8 @@
.gitignore
Cargo.toml
README.md
src/header.rs
src/lib.rs
src/models.rs
src/server/mod.rs
src/types.rs

View File

@@ -0,0 +1,46 @@
[package]
name = "rust-axum-validation-test"
version = "0.0.1"
authors = ["OpenAPI Generator team and contributors"]
description = "No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)"
edition = "2021"
[features]
default = ["server"]
server = []
conversion = [
"frunk",
"frunk_derives",
"frunk_core",
"frunk-enum-core",
"frunk-enum-derive",
]
[dependencies]
async-trait = "0.1"
axum = { version = "0.7" }
axum-extra = { version = "0.9", features = ["cookie", "multipart"] }
base64 = "0.22"
bytes = "1"
chrono = { version = "0.4", features = ["serde"] }
frunk = { version = "0.4", optional = true }
frunk-enum-core = { version = "0.3", optional = true }
frunk-enum-derive = { version = "0.3", optional = true }
frunk_core = { version = "0.4", optional = true }
frunk_derives = { version = "0.4", optional = true }
http = "1"
lazy_static = "1"
regex = "1"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["raw_value"] }
serde_urlencoded = "0.7"
tokio = { version = "1", default-features = false, features = [
"signal",
"rt-multi-thread",
] }
tracing = { version = "0.1", features = ["attributes"] }
uuid = { version = "1", features = ["serde"] }
validator = { version = "0.18", features = ["derive"] }
[dev-dependencies]
tracing-subscriber = "0.3"

View File

@@ -0,0 +1,91 @@
# Rust API for rust-axum-validation-test
No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
## Overview
This server was generated by the [openapi-generator]
(https://openapi-generator.tech) project. By using the
[OpenAPI-Spec](https://github.com/OAI/OpenAPI-Specification) from a remote
server, you can easily generate a server stub.
To see how to make this your own, look here: [README]((https://openapi-generator.tech))
- API version: 0.0.1
- Generator version: 7.6.0-SNAPSHOT
This autogenerated project defines an API crate `rust-axum-validation-test` which contains:
* An `Api` trait defining the API in Rust.
* Data types representing the underlying data model.
* Axum router which accepts HTTP requests and invokes the appropriate `Api` method for each operation.
* Request validations (path, query, body params) are included.
## Using the generated library
The generated library has a few optional features that can be activated through Cargo.
* `server`
* This defaults to enabled and creates the basic skeleton of a server implementation based on Axum.
* To create the server stack you'll need to provide an implementation of the API trait to provide the server function.
* `conversions`
* This defaults to disabled and creates extra derives on models to allow "transmogrification" between objects of structurally similar types.
See https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section for how to use features in your `Cargo.toml`.
### Example
```rust
struct ServerImpl {
// database: sea_orm::DbConn,
}
#[allow(unused_variables)]
#[async_trait]
impl rust-axum-validation-test::Api for ServerImpl {
// API implementation goes here
}
pub async fn start_server(addr: &str) {
// initialize tracing
tracing_subscriber::fmt::init();
// Init Axum router
let app = rust-axum-validation-test::server::new(Arc::new(ServerImpl));
// Add layers to the router
let app = app.layer(...);
// Run the server with graceful shutdown
let listener = TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.unwrap();
}
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
}
```

View File

@@ -0,0 +1,197 @@
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<T>(pub T);
// Generic implementations
impl<T> Deref for IntoHeaderValue<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
// Derive for each TryFrom<T> in http::HeaderValue
macro_rules! ihv_generate {
($t:ident) => {
impl TryFrom<HeaderValue> for IntoHeaderValue<$t> {
type Error = String;
fn try_from(hdr_value: HeaderValue) -> Result<Self, Self::Error> {
match hdr_value.to_str() {
Ok(hdr_value) => match hdr_value.parse::<$t>() {
Ok(hdr_value) => Ok(IntoHeaderValue(hdr_value)),
Err(e) => Err(format!(
"Unable to parse {} as a string: {}",
stringify!($t),
e
)),
},
Err(e) => Err(format!(
"Unable to parse header {:?} as a string - {}",
hdr_value, e
)),
}
}
}
impl TryFrom<IntoHeaderValue<$t>> for HeaderValue {
type Error = String;
fn try_from(hdr_value: IntoHeaderValue<$t>) -> Result<Self, Self::Error> {
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<String>
impl TryFrom<HeaderValue> for IntoHeaderValue<Vec<String>> {
type Error = String;
fn try_from(hdr_value: HeaderValue) -> Result<Self, Self::Error> {
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!(
"Unable to parse header: {:?} as a string - {}",
hdr_value, e
)),
}
}
}
impl TryFrom<IntoHeaderValue<Vec<String>>> for HeaderValue {
type Error = String;
fn try_from(hdr_value: IntoHeaderValue<Vec<String>>) -> Result<Self, Self::Error> {
match HeaderValue::from_str(&hdr_value.0.join(", ")) {
Ok(hdr_value) => Ok(hdr_value),
Err(e) => Err(format!(
"Unable to convert {:?} into a header - {}",
hdr_value, e
)),
}
}
}
// String
impl TryFrom<HeaderValue> for IntoHeaderValue<String> {
type Error = String;
fn try_from(hdr_value: HeaderValue) -> Result<Self, Self::Error> {
match hdr_value.to_str() {
Ok(hdr_value) => Ok(IntoHeaderValue(hdr_value.to_string())),
Err(e) => Err(format!("Unable to convert header {:?} to {}", hdr_value, e)),
}
}
}
impl TryFrom<IntoHeaderValue<String>> for HeaderValue {
type Error = String;
fn try_from(hdr_value: IntoHeaderValue<String>) -> Result<Self, Self::Error> {
match HeaderValue::from_str(&hdr_value.0) {
Ok(hdr_value) => Ok(hdr_value),
Err(e) => Err(format!(
"Unable to convert {:?} from a header {}",
hdr_value, e
)),
}
}
}
// Bool
impl TryFrom<HeaderValue> for IntoHeaderValue<bool> {
type Error = String;
fn try_from(hdr_value: HeaderValue) -> Result<Self, Self::Error> {
match hdr_value.to_str() {
Ok(hdr_value) => match hdr_value.parse() {
Ok(hdr_value) => Ok(IntoHeaderValue(hdr_value)),
Err(e) => Err(format!("Unable to parse bool from {} - {}", hdr_value, e)),
},
Err(e) => Err(format!(
"Unable to convert {:?} from a header {}",
hdr_value, e
)),
}
}
}
impl TryFrom<IntoHeaderValue<bool>> for HeaderValue {
type Error = String;
fn try_from(hdr_value: IntoHeaderValue<bool>) -> Result<Self, Self::Error> {
match HeaderValue::from_str(&hdr_value.0.to_string()) {
Ok(hdr_value) => Ok(hdr_value),
Err(e) => Err(format!(
"Unable to convert: {:?} into a header: {}",
hdr_value, e
)),
}
}
}
// DateTime
impl TryFrom<HeaderValue> for IntoHeaderValue<DateTime<Utc>> {
type Error = String;
fn try_from(hdr_value: HeaderValue) -> Result<Self, Self::Error> {
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!("Unable to parse: {} as date - {}", hdr_value, e)),
},
Err(e) => Err(format!(
"Unable to convert header {:?} to string {}",
hdr_value, e
)),
}
}
}
impl TryFrom<IntoHeaderValue<DateTime<Utc>>> for HeaderValue {
type Error = String;
fn try_from(hdr_value: IntoHeaderValue<DateTime<Utc>>) -> Result<Self, Self::Error> {
match HeaderValue::from_str(hdr_value.0.to_rfc3339().as_str()) {
Ok(hdr_value) => Ok(hdr_value),
Err(e) => Err(format!(
"Unable to convert {:?} to a header: {}",
hdr_value, e
)),
}
}
}

View File

@@ -0,0 +1,54 @@
#![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)]
use async_trait::async_trait;
use axum::extract::*;
use axum_extra::extract::{CookieJar, Multipart};
use bytes::Bytes;
use http::Method;
use serde::{Deserialize, Serialize};
use types::*;
pub const BASE_PATH: &str = "";
pub const API_VERSION: &str = "0.0.1";
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[must_use]
#[allow(clippy::large_enum_variant)]
pub enum MailPutResponse {
/// OK.
Status204_OK,
}
/// API
#[async_trait]
#[allow(clippy::ptr_arg)]
pub trait Api {
/// MailPut - PUT /mail
async fn mail_put(
&self,
method: Method,
host: Host,
cookies: CookieJar,
body: models::Email,
) -> Result<MailPutResponse, String>;
}
#[cfg(feature = "server")]
pub mod server;
pub mod models;
pub mod types;
#[cfg(feature = "server")]
pub(crate) mod header;

View File

@@ -0,0 +1,56 @@
#![allow(unused_qualifications)]
use http::HeaderValue;
use validator::Validate;
#[cfg(feature = "server")]
use crate::header;
use crate::{models, types::*};
#[derive(Debug, Clone, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))]
pub struct Email(String);
impl validator::Validate for Email {
fn validate(&self) -> std::result::Result<(), validator::ValidationErrors> {
std::result::Result::Ok(())
}
}
impl std::convert::From<String> for Email {
fn from(x: String) -> Self {
Email(x)
}
}
impl std::fmt::Display for Email {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.0)
}
}
impl std::str::FromStr for Email {
type Err = std::string::ParseError;
fn from_str(x: &str) -> std::result::Result<Self, Self::Err> {
std::result::Result::Ok(Email(x.to_string()))
}
}
impl std::convert::From<Email> for String {
fn from(x: Email) -> Self {
x.0
}
}
impl std::ops::Deref for Email {
type Target = String;
fn deref(&self) -> &String {
&self.0
}
}
impl std::ops::DerefMut for Email {
fn deref_mut(&mut self) -> &mut String {
&mut self.0
}
}

View File

@@ -0,0 +1,67 @@
use std::collections::HashMap;
use axum::{body::Body, extract::*, response::Response, routing::*};
use axum_extra::extract::{CookieJar, Multipart};
use bytes::Bytes;
use http::{header::CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue, Method, StatusCode};
use tracing::error;
use validator::{Validate, ValidationErrors};
use crate::{header, types::*};
#[allow(unused_imports)]
use crate::models;
use crate::{Api, MailPutResponse};
/// Setup API Server.
pub fn new<I, A>(api_impl: I) -> Router
where
I: AsRef<A> + Clone + Send + Sync + 'static,
A: Api + 'static,
{
// build our application with a route
Router::new()
.route("/mail", put(mail_put::<I, A>))
.with_state(api_impl)
}
/// MailPut - PUT /mail
#[tracing::instrument(skip_all)]
async fn mail_put<I, A>(
method: Method,
host: Host,
cookies: CookieJar,
State(api_impl): State<I>,
Json(body): Json<models::Email>,
) -> Result<Response, StatusCode>
where
I: AsRef<A> + Send + Sync,
A: Api,
{
let result = api_impl
.as_ref()
.mail_put(method, host, cookies, body)
.await;
let mut response = Response::builder();
let resp = match result {
Ok(rsp) => match rsp {
MailPutResponse::Status204_OK => {
let mut response = response.status(204);
response.body(Body::empty())
}
},
Err(_) => {
// Application code returned an error. This should not happen, as the implementation should
// return a valid response.
response.status(500).body(Body::empty())
}
};
resp.map_err(|e| {
error!(error = ?e);
StatusCode::INTERNAL_SERVER_ERROR
})
}

View File

@@ -0,0 +1,665 @@
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(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<Self, Self::Err> {
Ok(Self(serde_json::Value::String(s.to_owned())))
}
}
/// Serde helper function to create a default `Option<Nullable<T>>` while
/// deserializing
pub fn default_optional_nullable<T>() -> Option<Nullable<T>> {
None
}
/// Serde helper function to deserialize into an `Option<Nullable<T>>`
pub fn deserialize_optional_nullable<'de, D, T>(
deserializer: D,
) -> Result<Option<Nullable<T>>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
Option::<T>::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<T> {
/// Null value
Null,
/// Value is present
Present(T),
}
impl<T> Nullable<T> {
/////////////////////////////////////////////////////////////////////////
// Querying the contained values
/////////////////////////////////////////////////////////////////////////
/// Returns `true` if the Nullable is a `Present` value.
///
/// # Examples
///
/// ```
/// # use rust_axum_validation_test::types::Nullable;
///
/// let x: Nullable<u32> = Nullable::Present(2);
/// assert_eq!(x.is_present(), true);
///
/// let x: Nullable<u32> = 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_validation_test::types::Nullable;
///
/// let x: Nullable<u32> = Nullable::Present(2);
/// assert_eq!(x.is_null(), false);
///
/// let x: Nullable<u32> = 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<T>` 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_validation_test::types::Nullable;
///
/// let num_as_str: Nullable<String> = Nullable::Present("10".to_string());
/// // First, cast `Nullable<String>` to `Nullable<&String>` with `as_ref`,
/// // then consume *that* with `map`, leaving `num_as_str` on the stack.
/// let num_as_int: Nullable<usize> = 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<T>` to `Nullable<&mut T>`.
///
/// # Examples
///
/// ```
/// # use rust_axum_validation_test::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_validation_test::types::Nullable;
///
/// let x = Nullable::Present("value");
/// assert_eq!(x.expect("the world is ending"), "value");
/// ```
///
/// ```{.should_panic}
/// # use rust_axum_validation_test::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<T>` 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_validation_test::types::Nullable;
///
/// let x = Nullable::Present("air");
/// assert_eq!(x.unwrap(), "air");
/// ```
///
/// ```{.should_panic}
/// # use rust_axum_validation_test::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_validation_test::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_validation_test::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<F: FnOnce() -> T>(self, f: F) -> T {
match self {
Nullable::Present(x) => x,
Nullable::Null => f(),
}
}
/////////////////////////////////////////////////////////////////////////
// Transforming contained values
/////////////////////////////////////////////////////////////////////////
/// Maps a `Nullable<T>` to `Nullable<U>` 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_validation_test::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, F: FnOnce(T) -> U>(self, f: F) -> Nullable<U> {
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_validation_test::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, F: FnOnce(T) -> 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_validation_test::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, D: FnOnce() -> U, F: FnOnce(T) -> U>(self, default: D, f: F) -> U {
match self {
Nullable::Present(t) => f(t),
Nullable::Null => default(),
}
}
/// Transforms the `Nullable<T>` into a [`Result<T, E>`], mapping `Nullable::Present(v)` to
/// [`Ok(v)`] and `Nullable::Null` to [`Err(err)`][Err].
///
/// [`Result<T, E>`]: ../../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_validation_test::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<E>(self, err: E) -> Result<T, E> {
match self {
Nullable::Present(v) => Ok(v),
Nullable::Null => Err(err),
}
}
/// Transforms the `Nullable<T>` into a [`Result<T, E>`], mapping `Nullable::Present(v)` to
/// [`Ok(v)`] and `Nullable::Null` to [`Err(err())`][Err].
///
/// [`Result<T, E>`]: ../../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_validation_test::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, F: FnOnce() -> E>(self, err: F) -> Result<T, E> {
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_validation_test::types::Nullable;
///
/// let x = Nullable::Present(2);
/// let y: Nullable<&str> = Nullable::Null;
/// assert_eq!(x.and(y), Nullable::Null);
///
/// let x: Nullable<u32> = 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<u32> = Nullable::Null;
/// let y: Nullable<&str> = Nullable::Null;
/// assert_eq!(x.and(y), Nullable::Null);
/// ```
#[inline]
pub fn and<U>(self, optb: Nullable<U>) -> Nullable<U> {
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_validation_test::types::Nullable;
///
/// fn sq(x: u32) -> Nullable<u32> { Nullable::Present(x * x) }
/// fn nope(_: u32) -> Nullable<u32> { 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<U, F: FnOnce(T) -> Nullable<U>>(self, f: F) -> Nullable<U> {
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_validation_test::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<u32> = Nullable::Null;
/// let y = Nullable::Null;
/// assert_eq!(x.or(y), Nullable::Null);
/// ```
#[inline]
pub fn or(self, optb: Nullable<T>) -> Nullable<T> {
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_validation_test::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<F: FnOnce() -> Nullable<T>>(self, f: F) -> Nullable<T> {
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_validation_test::types::Nullable;
///
/// let mut x = Nullable::Present(2);
/// x.take();
/// assert_eq!(x, Nullable::Null);
///
/// let mut x: Nullable<u32> = Nullable::Null;
/// x.take();
/// assert_eq!(x, Nullable::Null);
/// ```
#[inline]
pub fn take(&mut self) -> Nullable<T> {
mem::replace(self, Nullable::Null)
}
}
impl<'a, T: Clone> Nullable<&'a T> {
/// Maps an `Nullable<&T>` to an `Nullable<T>` by cloning the contents of the
/// Nullable.
///
/// # Examples
///
/// ```
/// # use rust_axum_validation_test::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<T> {
self.map(Clone::clone)
}
}
impl<T: Default> Nullable<T> {
/// 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_validation_test::types::Nullable;
///
/// let x = Nullable::Present(42);
/// assert_eq!(42, x.unwrap_or_default());
///
/// let y: Nullable<i32> = 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<T> Default for Nullable<T> {
/// Returns None.
#[inline]
fn default() -> Nullable<T> {
Nullable::Null
}
}
impl<T> From<T> for Nullable<T> {
fn from(val: T) -> Nullable<T> {
Nullable::Present(val)
}
}
impl<T> Serialize for Nullable<T>
where
T: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<T>
where
T: serde::de::DeserializeOwned,
{
fn deserialize<D>(deserializer: D) -> Result<Nullable<T>, 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),
}
}
}
#[inline(never)]
#[cold]
fn expect_failed(msg: &str) -> ! {
panic!("{}", msg)
}
#[derive(Debug, Clone, PartialEq, PartialOrd)]
/// Base64-encoded byte array
pub struct ByteArray(pub Vec<u8>);
impl Serialize for ByteArray {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&general_purpose::STANDARD.encode(&self.0))
}
}
impl<'de> Deserialize<'de> for ByteArray {
fn deserialize<D>(deserializer: D) -> Result<ByteArray, D::Error>
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")),
}
}
}