1use scalar_cms::{
2 db::{ContentActions, ValidationContext},
3 editor_field::ToEditorField,
4 validations::{ErroredField, Validate, ValidationError},
5 DatabaseConnection, Document, EditorField,
6};
7use serde::{Deserialize, Serialize};
8
9use url::Url;
10
11#[cfg(feature = "s3")]
12pub mod bucket;
13#[cfg(feature = "s3")]
14pub use bucket::*;
15
16#[derive(Serialize, Deserialize, Default, Debug)]
20pub struct Null([(); 0]);
21
22impl ToEditorField for Null {
23 fn to_editor_field(
24 _default: Option<impl Into<Self>>,
25 name: &'static str,
26 title: &'static str,
27 placeholder: Option<&'static str>,
28 validator: Option<&'static str>,
29 component_key: Option<&'static str>,
30 ) -> EditorField
31 where
32 Self: std::marker::Sized,
33 {
34 EditorField {
35 name,
36 title,
37 placeholder,
38 required: true,
39 validator,
40 field_type: scalar_cms::EditorType::Null {
41 component_key: component_key.map(Into::into),
42 },
43 }
44 }
45}
46
47#[derive(EditorField, Serialize, Deserialize)]
48#[field(editor_component = "image")]
49#[derive(Debug)]
50pub struct ImageData<D: ToEditorField> {
51 pub url: Url,
52 pub additional_data: D,
53}
54
55pub type Image = ImageData<Null>;
56
57impl<D: ToEditorField + Validate + Sync> Validate for ImageData<D> {
58 async fn validate<DB: DatabaseConnection + ContentActions<DD> + Sync, DD: Document + Sync>(
59 &self,
60 ctx: ValidationContext<'_, DB, DD>,
61 ) -> Result<(), scalar_cms::validations::ValidationError> {
62 self.additional_data.validate(ctx).await
63 }
64}
65
66#[derive(EditorField, Debug, Serialize, Deserialize)]
67#[field(editor_component = "cropped-image")]
68pub struct CroppedImageData<D: ToEditorField, const VALIDATE: bool = true> {
71 pub url: Url,
72 pub gravity_x: f32,
73 pub gravity_y: f32,
74 pub additional_data: D,
75}
76
77pub type CroppedImage = CroppedImageData<Null, false>;
78
79impl<const VALIDATE: bool, D: ToEditorField> CroppedImageData<D, VALIDATE> {
80 #[inline]
81 fn validate_inner(
82 &self,
83 additional_result: Result<(), ValidationError>,
84 ) -> Result<(), ValidationError> {
85 let results = [
86 (0.0..=1.0).contains(&self.gravity_x).then_some(()).ok_or((
87 "gravity_x",
88 ValidationError::Single("gravity_x must be between 0 and 1".into()),
89 )),
90 (0.0..=1.0).contains(&self.gravity_y).then_some(()).ok_or((
91 "gravity_y",
92 ValidationError::Single("gravity_y must be between 0 and 1".into()),
93 )),
94 additional_result.map_err(|e| ("additional_data", e)),
95 ];
96 if results.iter().all(Result::is_ok) {
98 Ok(())
99 } else {
100 Err(ValidationError::Composite(
101 results
102 .into_iter()
103 .filter_map(|r| {
104 r.err().map(|(field, error)| ErroredField {
105 field: field.into(),
106 error,
107 })
108 })
109 .collect(),
110 ))
111 }
112 }
113}
114
115impl<D: ToEditorField + Validate + Sync> Validate for CroppedImageData<D, true> {
116 async fn validate<DB: DatabaseConnection + ContentActions<DD> + Sync, DD: Document + Sync>(
117 &self,
118 ctx: ValidationContext<'_, DB, DD>,
119 ) -> Result<(), scalar_cms::validations::ValidationError> {
120 self.validate_inner(self.additional_data.validate(ctx).await)
121 }
122}
123
124impl<D: ToEditorField + Send + Sync> Validate for CroppedImageData<D, false> {
125 async fn validate<DB: DatabaseConnection, DD: Document>(
126 &self,
127 _ctx: ValidationContext<'_, DB, DD>,
128 ) -> Result<(), scalar_cms::validations::ValidationError> {
129 self.validate_inner(Ok(()))
130 }
131}
132
133#[derive(EditorField, Debug, Serialize, Deserialize)]
134#[field(editor_component = "file")]
135pub struct FileData<D: ToEditorField> {
136 pub url: Url,
137 pub additional_data: D,
138}
139
140pub type File = FileData<Null>;
141
142impl<D: ToEditorField + Validate + Send + Sync> Validate for FileData<D> {
143 async fn validate<
144 DB: DatabaseConnection + ContentActions<DD> + Sync,
145 DD: Document + Send + Sync,
146 >(
147 &self,
148 ctx: ValidationContext<'_, DB, DD>,
149 ) -> Result<(), scalar_cms::validations::ValidationError> {
150 self.additional_data.validate(ctx).await
151 }
152}