Skip to content

Commit a508ca3

Browse files
committed
chore(model): Factor SeaORM and wrapper type boilerplate into macros
1 parent e1e1a88 commit a508ca3

13 files changed

+229
-607
lines changed

Cargo.lock

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ serde_json = "1"
8181
serde_with = "3"
8282
service = { path = "./service" }
8383
sha2 = "0.10"
84+
strum = { version = "0.26", features = ["derive"] }
8485
tempfile = "3"
8586
thiserror = "1"
8687
# https://github.com/time-rs/time/issues/681

rustfmt.toml

+2
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
# https://rust-lang.github.io/rustfmt
12
array_width = 16
3+
merge_derives = false

service/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ serde = { workspace = true }
3232
serde_json = { workspace = true }
3333
serde_with = { workspace = true }
3434
sha2 = { workspace = true }
35+
strum = { workspace = true }
3536
tempfile = { workspace = true }
3637
thiserror = { workspace = true }
3738
tokio = { workspace = true }

service/src/config/defaults.rs

+3-5
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@
77

88
use std::{path::PathBuf, str::FromStr as _};
99

10-
use crate::model::{DateLike, Duration, JidNode, PossiblyInfinite};
11-
12-
use super::WorkspaceInvitationChannel;
10+
use crate::model::{DateLike, Duration, InvitationChannel, JidNode, PossiblyInfinite};
1311

1412
pub fn api_admin_node() -> JidNode {
1513
JidNode::from_str("prose-pod-api").expect("Invalid default `api_admin_node`")
@@ -93,8 +91,8 @@ pub fn branding_page_title() -> String {
9391
"Prose Pod API".to_string()
9492
}
9593

96-
pub fn notify_workspace_invitation_channel() -> WorkspaceInvitationChannel {
97-
WorkspaceInvitationChannel::Email
94+
pub fn notify_workspace_invitation_channel() -> InvitationChannel {
95+
InvitationChannel::Email
9896
}
9997

10098
pub fn notify_email_smtp_host() -> String {

service/src/config/mod.rs

+3-9
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use secrecy::SecretString;
1919
use serde::Deserialize;
2020
use url_serde::SerdeUrl;
2121

22-
use crate::model::{DateLike, Duration, JidNode, PossiblyInfinite};
22+
use crate::model::{DateLike, Duration, InvitationChannel, JidNode, PossiblyInfinite};
2323

2424
pub type AppConfig = Config;
2525

@@ -166,13 +166,7 @@ pub struct ConfigBranding {
166166
pub company_name: String,
167167
}
168168

169-
#[derive(Debug, Clone, Deserialize)]
170-
#[serde(rename_all = "snake_case")]
171-
pub enum WorkspaceInvitationChannel {
172-
Email,
173-
}
174-
175-
impl Default for WorkspaceInvitationChannel {
169+
impl Default for InvitationChannel {
176170
fn default() -> Self {
177171
defaults::notify_workspace_invitation_channel()
178172
}
@@ -181,7 +175,7 @@ impl Default for WorkspaceInvitationChannel {
181175
#[derive(Debug, Clone, Deserialize, Default)]
182176
pub struct ConfigNotify {
183177
#[serde(default = "defaults::notify_workspace_invitation_channel")]
184-
pub workspace_invitation_channel: WorkspaceInvitationChannel,
178+
pub workspace_invitation_channel: InvitationChannel,
185179
#[serde(default)]
186180
pub email: Option<ConfigNotifyEmail>,
187181
}

service/src/model/durations.rs

+44-55
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@
33
// Copyright: 2024, Rémi Bardon <[email protected]>
44
// License: Mozilla Public License v2.0 (MPL v2.0)
55

6+
use std::{
7+
fmt::{Debug, Display},
8+
ops::Deref,
9+
str::FromStr,
10+
};
11+
612
use iso8601_duration::Duration as ISODuration;
7-
use sea_orm::entity::prelude::*;
8-
use sea_orm::sea_query::{self, ArrayType, ValueTypeErr};
9-
use sea_orm::TryGetError;
13+
use sea_orm::{entity::prelude::*, sea_query};
1014
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
1115

12-
use std::fmt::{Debug, Display};
13-
use std::ops::Deref;
14-
use std::str::FromStr;
16+
use crate::sea_orm_try_get_by_string;
17+
18+
const DURATION_INFINITE: &'static str = "infinite";
1519

1620
#[derive(Copy, Clone, Debug, PartialEq)]
1721
pub struct Duration<Content: DurationContent>(pub Content);
@@ -103,35 +107,30 @@ impl<Content: DurationContent> TryFrom<ISODuration> for Duration<Content> {
103107
impl<Content: DurationContent> sea_orm::TryGetable for Duration<Content>
104108
where
105109
<Content as TryFrom<ISODuration>>::Error: Debug,
110+
<Content as FromStr>::Err: Debug,
106111
{
107-
fn try_get_by<I: sea_orm::ColIdx>(
108-
res: &sea_orm::QueryResult,
109-
index: I,
110-
) -> Result<Self, TryGetError> {
111-
let value: String = res.try_get_by(index).map_err(TryGetError::DbErr)?;
112-
Self::try_from(value)
113-
// Technically, the value is not `null`, but we wouldn't want to unsafely unwrap here.
114-
.map_err(|e| TryGetError::Null(format!("{:?}", e)))
115-
}
112+
sea_orm_try_get_by_string!();
116113
}
117114

118115
impl<Content: DurationContent> sea_query::ValueType for Duration<Content>
119116
where
120117
Self: TryFrom<String>,
121118
{
122-
fn try_from(v: Value) -> Result<Self, ValueTypeErr> {
119+
fn try_from(v: Value) -> Result<Self, sea_query::ValueTypeErr> {
123120
match v {
124-
Value::String(Some(value)) => (*value).try_into().map_err(|_| ValueTypeErr),
125-
_ => Err(ValueTypeErr),
121+
Value::String(Some(value)) => {
122+
Self::from_str(value.as_str()).map_err(|_| sea_query::ValueTypeErr)
123+
}
124+
_ => Err(sea_query::ValueTypeErr),
126125
}
127126
}
128127

129128
fn type_name() -> String {
130129
Content::type_name()
131130
}
132131

133-
fn array_type() -> ArrayType {
134-
ArrayType::String
132+
fn array_type() -> sea_query::ArrayType {
133+
sea_query::ArrayType::String
135134
}
136135

137136
fn column_type() -> ColumnType {
@@ -207,12 +206,12 @@ impl TryFrom<ISODuration> for TimeLike {
207206
}
208207
impl DurationContent for TimeLike {
209208
fn type_name() -> String {
210-
stringify!(Time).to_owned()
209+
"Time".to_owned()
211210
}
212211
}
213212
impl Display for TimeLike {
214213
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215-
write!(f, "{}", self.into_iso_duration())
214+
Display::fmt(&self.into_iso_duration(), f)
216215
}
217216
}
218217
impl FromStr for TimeLike {
@@ -278,12 +277,12 @@ impl TryFrom<ISODuration> for DateLike {
278277
}
279278
impl DurationContent for DateLike {
280279
fn type_name() -> String {
281-
stringify!(Date).to_owned()
280+
"Date".to_owned()
282281
}
283282
}
284283
impl Display for DateLike {
285284
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286-
write!(f, "{}", self.into_iso_duration())
285+
Display::fmt(&self.into_iso_duration(), f)
287286
}
288287
}
289288
impl FromStr for DateLike {
@@ -310,22 +309,13 @@ impl<D> PossiblyInfinite<D> {
310309
}
311310
}
312311

313-
impl<D> Into<Option<D>> for PossiblyInfinite<D> {
314-
fn into(self) -> Option<D> {
315-
match self {
316-
Self::Infinite => None,
317-
Self::Finite(d) => Some(d),
318-
}
319-
}
320-
}
321-
322312
impl<D: Eq> Eq for PossiblyInfinite<D> {}
323313

324314
impl<D: Display> Display for PossiblyInfinite<D> {
325315
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326316
match self {
327-
Self::Infinite => write!(f, "infinite"),
328-
Self::Finite(d) => write!(f, "{d}"),
317+
Self::Infinite => write!(f, "{DURATION_INFINITE}"),
318+
Self::Finite(d) => Display::fmt(d, f),
329319
}
330320
}
331321
}
@@ -335,17 +325,14 @@ impl<D: FromStr> FromStr for PossiblyInfinite<D> {
335325

336326
fn from_str(s: &str) -> Result<Self, Self::Err> {
337327
match s {
338-
"infinite" => Ok(Self::Infinite),
328+
DURATION_INFINITE => Ok(Self::Infinite),
339329
d => D::from_str(d).map(Self::Finite),
340330
}
341331
}
342332
}
343333

344334
impl<D: Display> Serialize for PossiblyInfinite<D> {
345-
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
346-
where
347-
S: Serializer,
348-
{
335+
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
349336
self.to_string().serialize(serializer)
350337
}
351338
}
@@ -363,14 +350,13 @@ where
363350
}
364351
}
365352

366-
impl<D> From<PossiblyInfinite<D>> for sea_query::Value
367-
where
368-
sea_query::Value: From<D>,
369-
{
353+
impl<D: Into<sea_query::Value>> From<PossiblyInfinite<D>> for sea_query::Value {
370354
fn from(value: PossiblyInfinite<D>) -> Self {
371355
match value {
372-
PossiblyInfinite::Infinite => Self::String(Some(Box::new("infinite".to_string()))),
373-
PossiblyInfinite::Finite(duration) => Self::from(duration),
356+
PossiblyInfinite::Infinite => {
357+
Self::String(Some(Box::new(DURATION_INFINITE.to_string())))
358+
}
359+
PossiblyInfinite::Finite(duration) => duration.into(),
374360
}
375361
}
376362
}
@@ -379,35 +365,38 @@ impl<D: sea_orm::TryGetable> sea_orm::TryGetable for PossiblyInfinite<D> {
379365
fn try_get_by<I: sea_orm::ColIdx>(
380366
res: &sea_orm::QueryResult,
381367
index: I,
382-
) -> Result<Self, TryGetError> {
368+
) -> Result<Self, sea_orm::TryGetError> {
369+
// https://github.com/SeaQL/sea-orm/discussions/1176#discussioncomment-4024088
383370
let value = res
384371
.try_get_by(index)
385-
.map_err(TryGetError::DbErr)
386-
.and_then(|opt: Option<String>| opt.ok_or(TryGetError::Null(format!("{index:?}"))))?;
372+
.map_err(sea_orm::TryGetError::DbErr)
373+
.and_then(|opt: Option<String>| {
374+
opt.ok_or(sea_orm::TryGetError::Null(format!("{index:?}")))
375+
})?;
387376
match value.as_str() {
388-
"infinite" => Ok(Self::Infinite),
377+
DURATION_INFINITE => Ok(Self::Infinite),
389378
_ => D::try_get_by(res, index).map(Self::Finite),
390379
}
391380
}
392381
}
393382

394383
impl<D: sea_query::ValueType> sea_query::ValueType for PossiblyInfinite<D> {
395-
fn try_from(v: Value) -> Result<Self, ValueTypeErr> {
384+
fn try_from(v: Value) -> Result<Self, sea_query::ValueTypeErr> {
396385
let value: Option<String> = v.unwrap();
397386
let Some(value) = value else {
398-
return Err(ValueTypeErr);
387+
return Err(sea_query::ValueTypeErr);
399388
};
400389
match value.as_str() {
401-
"infinite" => Ok(Self::Infinite),
390+
DURATION_INFINITE => Ok(Self::Infinite),
402391
_ => D::try_from(Value::String(Some(Box::new(value)))).map(Self::Finite),
403392
}
404393
}
405394

406395
fn type_name() -> String {
407-
format!("{}<{}>", stringify!(PossiblyInfinite), D::type_name())
396+
format!("PossiblyInfinite<{}>", D::type_name())
408397
}
409398

410-
fn array_type() -> ArrayType {
399+
fn array_type() -> sea_query::ArrayType {
411400
D::array_type()
412401
}
413402

service/src/model/email_address.rs

+3-72
Original file line numberDiff line numberDiff line change
@@ -3,79 +3,10 @@
33
// Copyright: 2024, Rémi Bardon <[email protected]>
44
// License: Mozilla Public License v2.0 (MPL v2.0)
55

6-
use std::{fmt::Display, ops::Deref, str::FromStr};
7-
86
use sea_orm::sea_query;
9-
use serde_with::{DeserializeFromStr, SerializeDisplay};
10-
11-
#[derive(Debug, Clone, Eq, Hash, PartialEq, SerializeDisplay, DeserializeFromStr)]
12-
pub struct EmailAddress(email_address::EmailAddress);
13-
14-
impl Deref for EmailAddress {
15-
type Target = email_address::EmailAddress;
16-
17-
fn deref(&self) -> &Self::Target {
18-
&self.0
19-
}
20-
}
21-
22-
impl Display for EmailAddress {
23-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24-
Display::fmt(&self.0, f)
25-
}
26-
}
27-
28-
impl FromStr for EmailAddress {
29-
type Err = email_address::Error;
30-
31-
fn from_str(s: &str) -> Result<Self, Self::Err> {
32-
email_address::EmailAddress::from_str(s).map(|e| EmailAddress(e))
33-
}
34-
}
35-
36-
impl From<EmailAddress> for sea_query::Value {
37-
fn from(value: EmailAddress) -> Self {
38-
Self::String(Some(Box::new(value.to_string())))
39-
}
40-
}
41-
42-
impl sea_orm::TryGetable for EmailAddress {
43-
fn try_get_by<I: sea_orm::ColIdx>(
44-
res: &sea_orm::prelude::QueryResult,
45-
index: I,
46-
) -> Result<Self, sea_orm::TryGetError> {
47-
let value: String = res.try_get_by(index).map_err(sea_orm::TryGetError::DbErr)?;
48-
Self::from_str(value.as_str())
49-
// Technically, the value is not `null`, but we wouldn't want to unsafely unwrap here.
50-
.map_err(|e| sea_orm::TryGetError::Null(format!("{:?}", e)))
51-
}
52-
}
53-
54-
impl sea_query::ValueType for EmailAddress {
55-
fn try_from(v: sea_orm::Value) -> Result<Self, sea_query::ValueTypeErr> {
56-
match v {
57-
sea_orm::Value::String(Some(value)) => {
58-
Self::from_str(value.as_str()).map_err(|_| sea_query::ValueTypeErr)
59-
}
60-
_ => Err(sea_query::ValueTypeErr),
61-
}
62-
}
63-
64-
fn type_name() -> String {
65-
stringify!(EmailAddress).to_string()
66-
}
677

68-
fn array_type() -> sea_query::ArrayType {
69-
sea_query::ArrayType::String
70-
}
8+
use crate::{sea_orm_string, wrapper_type};
719

72-
fn column_type() -> sea_orm::ColumnType {
73-
sea_orm::ColumnType::string(None)
74-
}
75-
}
10+
wrapper_type!(EmailAddress, email_address::EmailAddress);
7611

77-
impl sea_query::Nullable for EmailAddress {
78-
fn null() -> sea_orm::Value {
79-
sea_orm::Value::String(None)
80-
}
81-
}
12+
sea_orm_string!(EmailAddress);

0 commit comments

Comments
 (0)