1use jsonschema::Validator;
2use lazy_static::lazy_static;
3use schemars::{schema_for, JsonSchema};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::sync::Arc;
7use thiserror::Error;
8use trailbase_extension::jsonschema::{SchemaEntry, ValidationError};
9use uuid::Uuid;
10
11#[derive(Debug, Clone, Error)]
12pub enum SchemaError {
13 #[error("JSONSchema validation error: {0}")]
14 JsonSchema(Arc<ValidationError>),
15 #[error("Cannot update builtin schemas")]
16 BuiltinSchema,
17 #[error("Missing name")]
18 MissingName,
19}
20
21#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
25#[serde(deny_unknown_fields)]
26pub struct FileUploadInput {
27 pub name: Option<String>,
29
30 pub filename: Option<String>,
32
33 pub content_type: Option<String>,
35
36 pub data: Vec<u8>,
38}
39
40impl FileUploadInput {
41 pub fn consume(self) -> Result<(Option<String>, FileUpload, Vec<u8>), SchemaError> {
42 let mime_type = infer::get(&self.data).map(|t| t.mime_type().to_string());
44
45 return Ok((
46 self.name,
47 FileUpload::new(
48 uuid::Uuid::new_v4(),
49 self.filename,
50 self.content_type,
51 mime_type,
52 ),
53 self.data,
54 ));
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
59#[serde(deny_unknown_fields)]
60pub struct FileUpload {
61 id: String,
63
64 filename: Option<String>,
66
67 content_type: Option<String>,
69
70 mime_type: Option<String>,
72}
73
74impl FileUpload {
75 pub fn new(
76 id: Uuid,
77 filename: Option<String>,
78 content_type: Option<String>,
79 mime_type: Option<String>,
80 ) -> Self {
81 Self {
82 id: id.to_string(),
83 filename,
84 content_type,
85 mime_type,
86 }
87 }
88
89 pub fn path(&self) -> &str {
90 &self.id
91 }
92
93 pub fn content_type(&self) -> Option<&str> {
94 self.content_type.as_deref()
95 }
96
97 pub fn original_filename(&self) -> Option<&str> {
98 self.filename.as_deref()
99 }
100}
101
102#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
103pub struct FileUploads(pub Vec<FileUpload>);
104
105fn builtin_schemas() -> &'static HashMap<String, SchemaEntry> {
106 fn validate_mime_type(value: &serde_json::Value, extra_args: Option<&str>) -> bool {
107 let Some(valid_mime_types) = extra_args else {
108 return true;
109 };
110
111 if let serde_json::Value::Object(ref map) = value {
112 if let Some(serde_json::Value::String(mime_type)) = map.get("mime_type") {
113 if valid_mime_types.contains(mime_type) {
114 return true;
115 }
116 }
117 }
118
119 return false;
120 }
121
122 lazy_static! {
123 static ref builtins: HashMap<String, SchemaEntry> = HashMap::<String, SchemaEntry>::from([
124 (
125 "std.FileUpload".to_string(),
126 SchemaEntry::from(
127 serde_json::to_value(schema_for!(FileUpload)).unwrap(),
128 Some(Arc::new(validate_mime_type))
129 )
130 .unwrap()
131 ),
132 (
133 "std.FileUploads".to_string(),
134 SchemaEntry::from(
135 serde_json::to_value(schema_for!(FileUploads)).unwrap(),
136 None
137 )
138 .unwrap(),
139 )
140 ]);
141 }
142
143 return &builtins;
144}
145
146#[derive(Debug, Clone)]
147pub struct Schema {
148 pub name: String,
149 pub schema: serde_json::Value,
150 pub builtin: bool,
151}
152
153pub fn get_schema(name: &str) -> Option<Schema> {
154 let builtins = builtin_schemas();
155
156 trailbase_extension::jsonschema::get_schema(name).map(|s| Schema {
157 name: name.to_string(),
158 schema: s,
159 builtin: builtins.contains_key(name),
160 })
161}
162
163pub fn get_compiled_schema(name: &str) -> Option<Arc<Validator>> {
164 trailbase_extension::jsonschema::get_compiled_schema(name)
165}
166
167pub fn get_schemas() -> Vec<Schema> {
168 let builtins = builtin_schemas();
169 return trailbase_extension::jsonschema::get_schemas()
170 .into_iter()
171 .map(|(name, value)| {
172 let builtin = builtins.contains_key(&name);
173 return Schema {
174 name,
175 schema: value,
176 builtin,
177 };
178 })
179 .collect();
180}
181
182pub fn set_user_schema(name: &str, pattern: Option<serde_json::Value>) -> Result<(), SchemaError> {
183 let builtins = builtin_schemas();
184 if builtins.contains_key(name) {
185 return Err(SchemaError::BuiltinSchema);
186 }
187
188 if let Some(p) = pattern {
189 let entry = SchemaEntry::from(p, None).map_err(|err| SchemaError::JsonSchema(Arc::new(err)))?;
190 trailbase_extension::jsonschema::set_schema(name, Some(entry));
191 } else {
192 trailbase_extension::jsonschema::set_schema(name, None);
193 }
194
195 return Ok(());
196}
197
198lazy_static! {
199 static ref INIT: std::sync::Mutex<bool> = std::sync::Mutex::new(false);
200}
201
202pub fn set_user_schemas(schemas: Vec<(String, serde_json::Value)>) -> Result<(), SchemaError> {
203 let mut entries: Vec<(String, SchemaEntry)> = vec![];
204 for (name, entry) in builtin_schemas() {
205 entries.push((name.clone(), entry.clone()));
206 }
207
208 for (name, schema) in schemas {
209 entries.push((
210 name,
211 SchemaEntry::from(schema, None).map_err(|err| SchemaError::JsonSchema(Arc::new(err)))?,
212 ));
213 }
214
215 trailbase_extension::jsonschema::set_schemas(Some(entries));
216
217 *INIT.lock().unwrap() = true;
218
219 return Ok(());
220}
221
222pub(crate) fn try_init_schemas() {
223 let mut init = INIT.lock().unwrap();
224 if !*init {
225 let entries = builtin_schemas()
226 .iter()
227 .map(|(name, entry)| (name.clone(), entry.clone()))
228 .collect::<Vec<_>>();
229
230 trailbase_extension::jsonschema::set_schemas(Some(entries));
231 *init = true;
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use serde_json::json;
238
239 use super::*;
240
241 #[test]
242 fn test_builtin_schemas() {
243 assert!(builtin_schemas().len() > 0);
244
245 for (name, schema) in builtin_schemas() {
246 trailbase_extension::jsonschema::set_schema(&name, Some(schema.clone()));
247 }
248
249 {
250 let schema = get_schema("std.FileUpload").unwrap();
251 let compiled_schema = Validator::new(&schema.schema).unwrap();
252 let input = json!({
253 "id": "foo",
254 "mime_type": "my_foo",
255 });
256 if let Err(err) = compiled_schema.validate(&input) {
257 panic!("{err:?}");
258 };
259 }
260
261 {
262 let schema = get_schema("std.FileUploads").unwrap();
263 let compiled_schema = Validator::new(&schema.schema).unwrap();
264 assert!(compiled_schema
265 .validate(&json!([
266 {
267 "id": "foo0",
268 "mime_type": "my_foo0",
269 },
270 {
271 "id": "foo1",
272 "mime_type": "my_foo1",
273 },
274 ]))
275 .is_ok());
276 }
277 }
278}