1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5pub mod clock;
6pub mod errors;
7pub mod studio;
8pub mod util;
9
10pub use clock::{Clock, MockClock, SystemClock};
11pub use studio::StudioConfig;
12
13pub const VERSION: &str = env!("CARGO_PKG_VERSION");
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum ExitCode {
21 Ok = 0,
22 Error = 1,
23 Usage = 64,
24 Unavailable = 69,
25}
26
27impl ExitCode {
28 pub const fn as_i32(self) -> i32 {
29 self as i32
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "lowercase")]
39pub enum Severity {
40 Error,
41 Warning,
42 Info,
43}
44
45impl fmt::Display for Severity {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 match self {
48 Severity::Error => f.write_str("error"),
49 Severity::Warning => f.write_str("warning"),
50 Severity::Info => f.write_str("info"),
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct Span {
58 pub file: String,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub line: Option<u32>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub column: Option<u32>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct Diagnostic {
71 pub severity: Severity,
72 pub code: String,
73 pub message: String,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub span: Option<Span>,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub hint: Option<String>,
78}
79
80impl fmt::Display for Diagnostic {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 write!(f, "[{}] {}: {}", self.severity, self.code, self.message)?;
83 if let Some(hint) = &self.hint {
84 write!(f, " (hint: {hint})")?;
85 }
86 Ok(())
87 }
88}
89
90pub const MANIFEST_VERSION: u32 = 1;
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
97pub struct AppManifest {
98 pub manifest_version: u32,
99 pub name: String,
100 pub version: String,
101 pub entities: Vec<ManifestEntity>,
102 pub routes: Vec<ManifestRoute>,
103 #[serde(default)]
104 pub queries: Vec<ManifestQuery>,
105 #[serde(default)]
106 pub actions: Vec<ManifestAction>,
107 #[serde(default)]
108 pub policies: Vec<ManifestPolicy>,
109 #[serde(default)]
120 pub auth: ManifestAuthConfig,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
127pub struct ManifestAuthConfig {
128 #[serde(default)]
129 pub user: ManifestAuthUserConfig,
130 #[serde(default)]
131 pub session: ManifestAuthSessionConfig,
132 #[serde(default, skip_serializing_if = "Vec::is_empty")]
135 pub trusted_origins: Vec<String>,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139pub struct ManifestAuthUserConfig {
140 #[serde(default = "default_user_entity")]
144 pub entity: String,
145 #[serde(default, skip_serializing_if = "Vec::is_empty")]
149 pub expose: Vec<String>,
150 #[serde(default, skip_serializing_if = "Vec::is_empty")]
155 pub hide: Vec<String>,
156}
157
158impl Default for ManifestAuthUserConfig {
159 fn default() -> Self {
160 Self {
161 entity: default_user_entity(),
162 expose: Vec::new(),
163 hide: Vec::new(),
164 }
165 }
166}
167
168fn default_user_entity() -> String {
169 "User".into()
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173pub struct ManifestAuthSessionConfig {
174 #[serde(default = "default_session_lifetime")]
176 pub expires_in: u64,
177 #[serde(default)]
182 pub cookie_cache: ManifestAuthCookieCacheConfig,
183}
184
185impl Default for ManifestAuthSessionConfig {
186 fn default() -> Self {
187 Self {
188 expires_in: default_session_lifetime(),
189 cookie_cache: ManifestAuthCookieCacheConfig::default(),
190 }
191 }
192}
193
194fn default_session_lifetime() -> u64 {
195 30 * 24 * 60 * 60
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204pub struct ManifestAuthCookieCacheConfig {
205 #[serde(default)]
206 pub enabled: bool,
207 #[serde(default = "default_cookie_cache_max_age")]
211 pub max_age: u64,
212 #[serde(default = "default_cookie_cache_claims")]
215 pub claims: Vec<String>,
216}
217
218impl Default for ManifestAuthCookieCacheConfig {
219 fn default() -> Self {
220 Self {
221 enabled: false,
222 max_age: default_cookie_cache_max_age(),
223 claims: default_cookie_cache_claims(),
224 }
225 }
226}
227
228fn default_cookie_cache_max_age() -> u64 {
229 5 * 60
230}
231
232fn default_cookie_cache_claims() -> Vec<String> {
233 vec!["is_admin".into(), "tenant_id".into()]
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
237pub struct ManifestEntity {
238 pub name: String,
239 pub fields: Vec<ManifestField>,
240 pub indexes: Vec<ManifestIndex>,
241 #[serde(default, skip_serializing_if = "Vec::is_empty")]
242 pub relations: Vec<ManifestRelation>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub search: Option<ManifestSearchConfig>,
248 #[serde(default = "default_crdt_enabled")]
256 pub crdt: bool,
257}
258
259fn default_crdt_enabled() -> bool {
260 true
261}
262
263impl Default for ManifestEntity {
264 fn default() -> Self {
265 Self {
266 name: String::new(),
267 fields: Vec::new(),
268 indexes: Vec::new(),
269 relations: Vec::new(),
270 search: None,
271 crdt: true,
272 }
273 }
274}
275
276#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
284pub struct ManifestSearchConfig {
285 #[serde(default)]
286 pub text: Vec<String>,
287 #[serde(default)]
288 pub facets: Vec<String>,
289 #[serde(default)]
290 pub sortable: Vec<String>,
291 #[serde(default)]
299 pub language: Option<String>,
300}
301
302impl ManifestSearchConfig {
303 pub fn is_empty(&self) -> bool {
304 self.text.is_empty() && self.facets.is_empty() && self.sortable.is_empty()
305 }
306
307 pub fn language_or_default(&self) -> &str {
311 self.language.as_deref().unwrap_or("english")
312 }
313}
314
315#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
316pub struct ManifestRelation {
317 pub name: String,
318 pub target: String,
319 pub field: String,
320 #[serde(default)]
321 pub many: bool,
322}
323
324#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
325pub struct ManifestField {
326 pub name: String,
327 #[serde(rename = "type")]
328 pub field_type: String,
329 pub optional: bool,
330 pub unique: bool,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
339 pub crdt: Option<CrdtAnnotation>,
340}
341
342#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
354#[serde(rename_all = "kebab-case")]
355pub enum CrdtAnnotation {
356 Lww,
358 Text,
360 Counter,
364 List,
366 #[serde(rename = "movable-list")]
369 MovableList,
370 Tree,
373}
374
375impl CrdtAnnotation {
376 pub fn as_str(self) -> &'static str {
379 match self {
380 Self::Lww => "lww",
381 Self::Text => "text",
382 Self::Counter => "counter",
383 Self::List => "list",
384 Self::MovableList => "movable-list",
385 Self::Tree => "tree",
386 }
387 }
388}
389
390impl std::fmt::Display for CrdtAnnotation {
391 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
392 f.write_str(self.as_str())
393 }
394}
395
396#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
397pub struct ManifestIndex {
398 pub name: String,
399 pub fields: Vec<String>,
400 pub unique: bool,
401}
402
403#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
404pub struct ManifestRoute {
405 pub path: String,
406 pub mode: String,
407 #[serde(skip_serializing_if = "Option::is_none")]
408 pub query: Option<String>,
409 #[serde(skip_serializing_if = "Option::is_none")]
410 pub auth: Option<String>,
411}
412
413#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
414pub struct ManifestQuery {
415 pub name: String,
416 #[serde(default, skip_serializing_if = "Vec::is_empty")]
417 pub input: Vec<ManifestField>,
418}
419
420#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
421pub struct ManifestAction {
422 pub name: String,
423 #[serde(default, skip_serializing_if = "Vec::is_empty")]
424 pub input: Vec<ManifestField>,
425}
426
427#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
436pub struct ManifestPolicy {
437 pub name: String,
438 #[serde(skip_serializing_if = "Option::is_none")]
439 pub entity: Option<String>,
440 #[serde(skip_serializing_if = "Option::is_none")]
441 pub action: Option<String>,
442 #[serde(default, skip_serializing_if = "String::is_empty")]
443 pub allow: String,
444 #[serde(default, rename = "allowRead", skip_serializing_if = "Option::is_none")]
446 pub allow_read: Option<String>,
447 #[serde(
450 default,
451 rename = "allowInsert",
452 skip_serializing_if = "Option::is_none"
453 )]
454 pub allow_insert: Option<String>,
455 #[serde(
457 default,
458 rename = "allowUpdate",
459 skip_serializing_if = "Option::is_none"
460 )]
461 pub allow_update: Option<String>,
462 #[serde(
464 default,
465 rename = "allowDelete",
466 skip_serializing_if = "Option::is_none"
467 )]
468 pub allow_delete: Option<String>,
469 #[serde(
472 default,
473 rename = "allowWrite",
474 skip_serializing_if = "Option::is_none"
475 )]
476 pub allow_write: Option<String>,
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 #[test]
484 fn exit_code_values() {
485 assert_eq!(ExitCode::Ok.as_i32(), 0);
486 assert_eq!(ExitCode::Error.as_i32(), 1);
487 assert_eq!(ExitCode::Usage.as_i32(), 64);
488 assert_eq!(ExitCode::Unavailable.as_i32(), 69);
489 }
490
491 #[test]
492 fn severity_display() {
493 assert_eq!(format!("{}", Severity::Error), "error");
494 assert_eq!(format!("{}", Severity::Warning), "warning");
495 assert_eq!(format!("{}", Severity::Info), "info");
496 }
497
498 #[test]
499 fn diagnostic_display_without_hint() {
500 let d = Diagnostic {
501 severity: Severity::Error,
502 code: "TEST".into(),
503 message: "something failed".into(),
504 span: None,
505 hint: None,
506 };
507 assert_eq!(format!("{d}"), "[error] TEST: something failed");
508 }
509
510 #[test]
511 fn diagnostic_display_with_hint() {
512 let d = Diagnostic {
513 severity: Severity::Warning,
514 code: "WARN".into(),
515 message: "check this".into(),
516 span: None,
517 hint: Some("try again".into()),
518 };
519 assert_eq!(
520 format!("{d}"),
521 "[warning] WARN: check this (hint: try again)"
522 );
523 }
524
525 #[test]
526 fn manifest_version_constant() {
527 assert_eq!(MANIFEST_VERSION, 1);
528 }
529}