poem_ext/
patch_value.rs

1//! Contains the [`PatchValue`] enum that can be used in `PATCH` endpoints to
2//! distinguish between values that should be updated and those that should
3//! remain unchanged.
4//!
5//! #### Example
6//! ```
7//! use poem_ext::{patch_value::PatchValue, responses::internal_server_error};
8//! use poem_openapi::{param::Path, payload::Json, Object, OpenApi};
9//! use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, Unchanged};
10//!
11//! struct Api {
12//!     db: DatabaseConnection,
13//! }
14//!
15//! #[OpenApi]
16//! impl Api {
17//!     #[oai(path = "/user/:user_id", method = "patch")]
18//!     async fn update_user(
19//!         &self,
20//!         user_id: Path<i32>,
21//!         data: Json<UpdateUserRequest>,
22//!     ) -> UpdateUser::Response {
23//!         let Some(user) = users::Entity::find_by_id(user_id.0).one(&self.db).await? else {
24//!             return UpdateUser::not_found();
25//!         };
26//!
27//!         users::ActiveModel {
28//!             id: Unchanged(user.id),
29//!             name: data.0.name.update(user.name),
30//!             password: data.0.password.update(user.password),
31//!         }
32//!         .update(&self.db)
33//!         .await?;
34//!
35//!         UpdateUser::ok()
36//!     }
37//! }
38//!
39//! #[derive(Debug, Object)]
40//! pub struct UpdateUserRequest {
41//!     #[oai(validator(max_length = 255))]
42//!     pub name: PatchValue<String>,
43//!     #[oai(validator(max_length = 255))]
44//!     pub password: PatchValue<String>,
45//! }
46//! #
47//! # poem_ext::response!(UpdateUser = {
48//! #     Ok(200),
49//! #     NotFound(404),
50//! # });
51//! # mod users {
52//! #     use sea_orm::entity::prelude::*;
53//! #
54//! #     #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
55//! #     #[sea_orm(table_name = "users")]
56//! #     pub struct Model {
57//! #         #[sea_orm(primary_key, auto_increment = false)]
58//! #         pub id: i32,
59//! #         #[sea_orm(column_type = "Text")]
60//! #         pub name: String,
61//! #         #[sea_orm(column_type = "Text")]
62//! #         pub password: String,
63//! #     }
64//! #
65//! #     #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
66//! #     pub enum Relation {}
67//! #
68//! #     impl ActiveModelBehavior for ActiveModel {}
69//! # }
70//! ```
71
72use std::borrow::Cow;
73
74use poem_openapi::{
75    registry::MetaSchemaRef,
76    types::{ParseFromJSON, ParseResult, ToJSON, Type},
77};
78#[cfg(feature = "sea-orm")]
79use sea_orm::ActiveValue;
80
81/// Can be used as a parameter in `PATCH` endpoints to distinguish between
82/// values that should be updated and those that should remain unchanged.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
84pub enum PatchValue<T> {
85    /// Update the value to the contained `T`.
86    Set(T),
87    /// Don't change the value.
88    #[default]
89    Unchanged,
90}
91
92impl<T> PatchValue<T> {
93    /// Convert this type to a [`sea_orm::ActiveValue`] that can be used to
94    /// construct an `ActiveModel`.
95    #[cfg(feature = "sea-orm")]
96    pub fn update(self, old: T) -> ActiveValue<T>
97    where
98        T: Into<sea_orm::Value>,
99    {
100        match self {
101            Self::Set(x) => ActiveValue::Set(x),
102            Self::Unchanged => ActiveValue::Unchanged(old),
103        }
104    }
105
106    /// Return the new value if this is [`Set(T)`](Self::Unchanged) or the old
107    /// value if [`Unchanged`](Self::Unchanged).
108    pub fn get_new<'a>(&'a self, old: &'a T) -> &'a T {
109        match self {
110            Self::Set(x) => x,
111            Self::Unchanged => old,
112        }
113    }
114
115    /// Convert a [`PatchValue<T>`] to a [`PatchValue<U>`].
116    pub fn map<U>(self, f: impl FnOnce(T) -> U) -> PatchValue<U> {
117        match self {
118            PatchValue::Set(x) => PatchValue::Set(f(x)),
119            PatchValue::Unchanged => PatchValue::Unchanged,
120        }
121    }
122}
123
124impl<T> ParseFromJSON for PatchValue<T>
125where
126    T: ParseFromJSON,
127{
128    fn parse_from_json(
129        value: Option<poem_openapi::__private::serde_json::Value>,
130    ) -> ParseResult<Self> {
131        match Option::<T>::parse_from_json(value) {
132            Ok(Some(x)) => Ok(Self::Set(x)),
133            Ok(None) => Ok(Self::Unchanged),
134            Err(x) => Err(x.propagate()),
135        }
136    }
137}
138
139impl<T> ToJSON for PatchValue<T>
140where
141    T: ToJSON,
142{
143    fn to_json(&self) -> Option<poem_openapi::__private::serde_json::Value> {
144        match self {
145            Self::Set(x) => Some(x),
146            Self::Unchanged => None,
147        }
148        .to_json()
149    }
150}
151
152impl<T> Type for PatchValue<T>
153where
154    T: Type,
155{
156    const IS_REQUIRED: bool = false; // default to unchanged
157
158    type RawValueType = T::RawValueType;
159
160    type RawElementValueType = T::RawElementValueType;
161
162    fn name() -> Cow<'static, str> {
163        format!("optional<{}>", T::name()).into()
164    }
165
166    fn schema_ref() -> MetaSchemaRef {
167        T::schema_ref()
168    }
169
170    fn as_raw_value(&self) -> Option<&Self::RawValueType> {
171        match self {
172            Self::Set(value) => value.as_raw_value(),
173            Self::Unchanged => None,
174        }
175    }
176
177    fn raw_element_iter<'a>(
178        &'a self,
179    ) -> Box<dyn Iterator<Item = &'a Self::RawElementValueType> + 'a> {
180        match self {
181            Self::Set(value) => value.raw_element_iter(),
182            Self::Unchanged => Box::new(std::iter::empty()),
183        }
184    }
185}
186
187#[cfg(feature = "serde")]
188impl<T> serde::Serialize for PatchValue<T>
189where
190    T: serde::Serialize,
191{
192    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
193    where
194        S: serde::Serializer,
195    {
196        match self {
197            PatchValue::Set(x) => Some(x),
198            PatchValue::Unchanged => None,
199        }
200        .serialize(serializer)
201    }
202}
203
204#[cfg(feature = "serde")]
205impl<'de, T> serde::Deserialize<'de> for PatchValue<T>
206where
207    T: serde::Deserialize<'de>,
208{
209    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
210    where
211        D: serde::Deserializer<'de>,
212    {
213        match Option::<T>::deserialize(deserializer) {
214            Ok(Some(x)) => Ok(Self::Set(x)),
215            Ok(None) => Ok(Self::Unchanged),
216            Err(err) => Err(err),
217        }
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use serde::{Deserialize, Serialize};
224
225    use super::{PatchValue::*, *};
226
227    #[test]
228    fn serialize() {
229        assert_eq!(
230            serde_json::to_string(&Test { value: Unchanged }).unwrap(),
231            r#"{"value":null}"#
232        );
233        assert_eq!(
234            serde_json::to_string(&Test { value: Set(42) }).unwrap(),
235            r#"{"value":42}"#
236        );
237    }
238
239    #[test]
240    fn deserialize() {
241        assert_eq!(
242            serde_json::from_str::<Test>(r#"{}"#).unwrap(),
243            Test { value: Unchanged }
244        );
245        assert_eq!(
246            serde_json::from_str::<Test>(r#"{"value":null}"#).unwrap(),
247            Test { value: Unchanged }
248        );
249        assert_eq!(
250            serde_json::from_str::<Test>(r#"{"value":42}"#).unwrap(),
251            Test { value: Set(42) }
252        );
253    }
254
255    #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
256    struct Test {
257        value: PatchValue<i32>,
258    }
259}