Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cot/src/common_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use cot::db::impl_postgres::PostgresValueRef;
use cot::db::impl_sqlite::SqliteValueRef;
use cot::form::FormFieldValidationError;
use email_address::EmailAddress;
use serde::{Deserialize, Serialize};
use thiserror::Error;

#[cfg(feature = "db")]
Expand Down Expand Up @@ -181,7 +182,7 @@ impl From<String> for Password {
/// let url_string = url.into_string();
/// assert_eq!(url_string, "https://example.com/");
/// ```
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Url(url::Url);

impl Url {
Expand Down Expand Up @@ -417,7 +418,7 @@ impl DatabaseField for Url {
/// // Convert using TryFrom
/// let email = Email::try_from("user@example.com").unwrap();
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Email(EmailAddress);

impl Email {
Expand Down
55 changes: 54 additions & 1 deletion cot/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ use sea_query::{
ColumnRef, Iden, IntoColumnRef, OnConflict, ReturningClause, SchemaStatementBuilder, SimpleExpr,
};
use sea_query_binder::{SqlxBinder, SqlxValues};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use sqlx::{Type, TypeInfo};
use thiserror::Error;
use tracing::{Instrument, Level, span, trace};
Expand Down Expand Up @@ -2077,6 +2078,29 @@ impl<T: Display> Display for Auto<T> {
}
}

impl<T: Serialize> Serialize for Auto<T> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Fixed(value) => value.serialize(serializer),
Self::Auto => Err(serde::ser::Error::custom(
"Auto::Auto values cannot be serialized",
)),
}
}
}

impl<'de, T: Deserialize<'de>> Deserialize<'de> for Auto<T> {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
T::deserialize(deserializer).map(Self::Fixed)
}
}

/// A wrapper over a string that has a limited length.
///
/// This type is used to represent a string that has a limited length in the
Expand All @@ -2100,7 +2124,9 @@ impl<T: Display> Display for Auto<T> {
/// let limited_string = LimitedString::<5>::new("too long");
/// assert!(limited_string.is_err());
/// ```
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deref, Display)]
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deref, Display, Serialize, Deserialize,
)]
pub struct LimitedString<const LIMIT: u32>(String);

impl<const LIMIT: u32> PartialEq<&str> for LimitedString<LIMIT> {
Expand Down Expand Up @@ -2364,4 +2390,31 @@ mod tests {
let auto_value = DbFieldValue::Auto;
let _ = auto_value.expect_value("expected a value");
}

#[test]
fn auto_serialize_fixed() {
let auto = Auto::fixed(42i32);
let serialized = serde_json::to_string(&auto).unwrap();
assert_eq!(serialized, "42");
}

#[test]
fn auto_serialize_auto() {
let auto = Auto::<i32>::Auto;
let value = serde_json::to_string(&auto);
assert!(value.is_err());
}

#[test]
fn auto_deserialize_fixed() {
let deserialized: Auto<i32> = serde_json::from_str("42").unwrap();
assert_eq!(deserialized, Auto::fixed(42));
}

#[test]
fn auto_deserialize_auto() {
let deserialized: std::result::Result<Auto<i32>, serde_json::Error> =
serde_json::from_str("null");
assert!(deserialized.is_err());
}
}
204 changes: 203 additions & 1 deletion cot/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,17 @@
#[cfg(feature = "swagger-ui")]
pub mod swagger_ui;

use std::borrow::Cow;
use std::marker::PhantomData;
use std::pin::Pin;

use aide::openapi::{
MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem, PathStyle,
QueryStyle, ReferenceOr, RequestBody, StatusCode,
};
use cot::common_types::Email;
#[cfg(feature = "db")]
use cot::db::{ForeignKey, LimitedString, Model};
use cot_core::handler::{BoxRequestHandler, RequestHandler, handle_all_parameters};
/// Derive macro for the [`ApiOperationResponse`] trait.
///
Expand Down Expand Up @@ -207,6 +211,9 @@ use schemars::{JsonSchema, Schema, SchemaGenerator};
use serde_json::Value;

use crate::auth::Auth;
use crate::common_types::Url;
#[cfg(feature = "db")]
use crate::db::Auto;
use crate::form::Form;
use crate::json::Json;
use crate::request::extractors::{FromRequest, FromRequestHead, Path, RequestForm, UrlQuery};
Expand Down Expand Up @@ -1134,10 +1141,131 @@ where
}
}

#[cfg(feature = "db")]
impl<T: JsonSchema> JsonSchema for Auto<T> {
fn schema_name() -> Cow<'static, str> {
format!("Auto_{}", T::schema_name()).into()
}

fn schema_id() -> Cow<'static, str> {
format!("Auto<{}>", T::schema_id()).into()
}

fn json_schema(generator: &mut SchemaGenerator) -> Schema {
T::json_schema(generator)
}
}

#[cfg(feature = "db")]
impl<const LIMIT: u32> JsonSchema for LimitedString<LIMIT> {
fn schema_name() -> Cow<'static, str> {
format!("LimitedString_{LIMIT}").into()
}

fn schema_id() -> Cow<'static, str> {
format!("LimitedString<{LIMIT}>").into()
}

fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
Schema::try_from(Value::Object({
let mut object = ::serde_json::Map::new();
let _ = object.insert(
("type").into(),
::serde_json::to_value("string")
.expect("serializing a fixed value should never fail"),
);
let _ = object.insert(
("maxLength").into(),
::serde_json::to_value(LIMIT).expect("serializing a fixed value should not fail"),
);
object
}))
.expect("invalid schema for LimitedString")
}
}

#[cfg(feature = "db")]
impl<T: JsonSchema + Model> JsonSchema for ForeignKey<T>
where
T::PrimaryKey: JsonSchema,
{
fn schema_name() -> Cow<'static, str> {
format!("ForeignKey_{}", T::schema_name()).into()
}

fn schema_id() -> Cow<'static, str> {
format!("ForeignKey<{}>", T::schema_id()).into()
}

fn json_schema(generator: &mut SchemaGenerator) -> Schema {
let mut schema = T::PrimaryKey::json_schema(generator);
if let Some(obj) = schema.as_object_mut() {
let _ = obj.insert(
("description").into(),
Value::String(format!("Primary key for {}", T::schema_name())),
);
}
schema
}
}

impl JsonSchema for Email {
fn schema_name() -> Cow<'static, str> {
"Email".into()
}

fn schema_id() -> Cow<'static, str> {
"Email".into()
}

fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
Schema::try_from(Value::Object({
let mut object = serde_json::Map::new();
let _ = object.insert(
("type").into(),
serde_json::to_value("string").expect("serializing a fixed value should not fail"),
);
let _ = object.insert(
("format").into(),
serde_json::to_value("email").expect("serializing a fixed value should not fail"),
);
object
}))
.expect("invalid schema for Email")
}
}

impl JsonSchema for Url {
fn schema_name() -> Cow<'static, str> {
"Url".into()
}

fn schema_id() -> Cow<'static, str> {
"Url".into()
}

fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
Schema::try_from(Value::Object({
let mut object = serde_json::Map::new();
let _ = object.insert(
("type").into(),
serde_json::to_value("string").expect("serializing a fixed value should not fail"),
);
let _ = object.insert(
("format").into(),
serde_json::to_value("uri").expect("serializing a fixed value should not fail"),
);
object
}))
.expect("invalid schema for Url")
}
}

#[cfg(test)]
mod tests {
use aide::openapi::{Operation, Parameter};
use schemars::SchemaGenerator;
use cot_macros::model;
use schemars::{JsonSchema, SchemaGenerator};
use serde::{Deserialize, Serialize};

use super::*;
Expand Down Expand Up @@ -1586,4 +1714,78 @@ mod tests {
assert_eq!(status_code_2, &Some(StatusCode::Code(400)));
assert_eq!(response_2.description, "Bad Request");
}

#[test]
fn json_schema_for_auto() {
let mut generator = SchemaGenerator::default();

let schema = <Auto<i32>>::json_schema(&mut generator);
let value = serde_json::to_value(&schema).unwrap();
assert_eq!(value["type"], "integer");
assert_eq!(<Auto<i32>>::schema_name(), "Auto_int32");
assert_eq!(<Auto<i32>>::schema_id(), "Auto<int32>");
}

#[test]
fn json_schema_for_limited_string() {
let mut generator = SchemaGenerator::default();

let schema = <LimitedString<10>>::json_schema(&mut generator);
let value = serde_json::to_value(&schema).unwrap();
assert_eq!(value["type"], "string");
assert_eq!(value["maxLength"], 10);
assert_eq!(<LimitedString<10>>::schema_name(), "LimitedString_10");
assert_eq!(<LimitedString<10>>::schema_id(), "LimitedString<10>");
}

#[test]
fn json_schema_for_foreign_key() {
#[derive(Debug, Clone, PartialEq, schemars::JsonSchema)]
#[model]
struct TestModel {
#[model(primary_key)]
id: Auto<i32>,
}

let mut generator = SchemaGenerator::default();

let schema = <ForeignKey<TestModel>>::json_schema(&mut generator);

let value = serde_json::to_value(&schema).unwrap();
assert_eq!(value["type"], "integer");
assert_eq!(value["format"], "int32");
assert_eq!(value["description"], "Primary key for TestModel");
assert_eq!(
<ForeignKey<TestModel>>::schema_name(),
"ForeignKey_TestModel"
);
assert_eq!(
<ForeignKey<TestModel>>::schema_id(),
format!("ForeignKey<{}>", TestModel::schema_id())
);
}

#[test]
fn json_schema_for_email() {
let mut generator = SchemaGenerator::default();

let schema = Email::json_schema(&mut generator);
let value = serde_json::to_value(&schema).unwrap();
assert_eq!(value["type"], "string");
assert_eq!(value["format"], "email");
assert_eq!(Email::schema_name(), "Email");
assert_eq!(Email::schema_id(), "Email");
}

#[test]
fn json_schema_for_url() {
let mut generator = SchemaGenerator::default();

let schema = Url::json_schema(&mut generator);
let value = serde_json::to_value(&schema).unwrap();
assert_eq!(value["type"], "string");
assert_eq!(value["format"], "uri");
assert_eq!(Url::schema_name(), "Url");
assert_eq!(Url::schema_id(), "Url");
}
}
Loading