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 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub admin_field: Option<String>,
174}
175
176impl Default for ManifestAuthUserConfig {
177 fn default() -> Self {
178 Self {
179 entity: default_user_entity(),
180 expose: Vec::new(),
181 hide: Vec::new(),
182 admin_field: None,
183 }
184 }
185}
186
187fn default_user_entity() -> String {
188 "User".into()
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192pub struct ManifestAuthSessionConfig {
193 #[serde(default = "default_session_lifetime")]
195 pub expires_in: u64,
196 #[serde(default)]
201 pub cookie_cache: ManifestAuthCookieCacheConfig,
202}
203
204impl Default for ManifestAuthSessionConfig {
205 fn default() -> Self {
206 Self {
207 expires_in: default_session_lifetime(),
208 cookie_cache: ManifestAuthCookieCacheConfig::default(),
209 }
210 }
211}
212
213fn default_session_lifetime() -> u64 {
214 30 * 24 * 60 * 60
215}
216
217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223pub struct ManifestAuthCookieCacheConfig {
224 #[serde(default)]
225 pub enabled: bool,
226 #[serde(default = "default_cookie_cache_max_age")]
230 pub max_age: u64,
231 #[serde(default = "default_cookie_cache_claims")]
234 pub claims: Vec<String>,
235}
236
237impl Default for ManifestAuthCookieCacheConfig {
238 fn default() -> Self {
239 Self {
240 enabled: false,
241 max_age: default_cookie_cache_max_age(),
242 claims: default_cookie_cache_claims(),
243 }
244 }
245}
246
247fn default_cookie_cache_max_age() -> u64 {
248 5 * 60
249}
250
251fn default_cookie_cache_claims() -> Vec<String> {
252 vec!["is_admin".into(), "tenant_id".into()]
253}
254
255#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
256pub struct ManifestEntity {
257 pub name: String,
258 pub fields: Vec<ManifestField>,
259 pub indexes: Vec<ManifestIndex>,
260 #[serde(default, skip_serializing_if = "Vec::is_empty")]
261 pub relations: Vec<ManifestRelation>,
262 #[serde(default, skip_serializing_if = "Option::is_none")]
266 pub search: Option<ManifestSearchConfig>,
267 #[serde(default = "default_crdt_enabled")]
275 pub crdt: bool,
276}
277
278fn default_crdt_enabled() -> bool {
279 true
280}
281
282impl Default for ManifestEntity {
283 fn default() -> Self {
284 Self {
285 name: String::new(),
286 fields: Vec::new(),
287 indexes: Vec::new(),
288 relations: Vec::new(),
289 search: None,
290 crdt: true,
291 }
292 }
293}
294
295#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
303pub struct ManifestSearchConfig {
304 #[serde(default)]
305 pub text: Vec<String>,
306 #[serde(default)]
307 pub facets: Vec<String>,
308 #[serde(default)]
309 pub sortable: Vec<String>,
310 #[serde(default)]
318 pub language: Option<String>,
319}
320
321impl ManifestSearchConfig {
322 pub fn is_empty(&self) -> bool {
323 self.text.is_empty() && self.facets.is_empty() && self.sortable.is_empty()
324 }
325
326 pub fn language_or_default(&self) -> &str {
330 self.language.as_deref().unwrap_or("english")
331 }
332}
333
334#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
335pub struct ManifestRelation {
336 pub name: String,
337 pub target: String,
338 pub field: String,
339 #[serde(default)]
340 pub many: bool,
341}
342
343#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
344pub struct ManifestField {
345 pub name: String,
346 #[serde(rename = "type")]
347 pub field_type: String,
348 pub optional: bool,
349 pub unique: bool,
350 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub crdt: Option<CrdtAnnotation>,
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
373#[serde(rename_all = "kebab-case")]
374pub enum CrdtAnnotation {
375 Lww,
377 Text,
379 Counter,
383 List,
385 #[serde(rename = "movable-list")]
388 MovableList,
389 Tree,
392}
393
394impl CrdtAnnotation {
395 pub fn as_str(self) -> &'static str {
398 match self {
399 Self::Lww => "lww",
400 Self::Text => "text",
401 Self::Counter => "counter",
402 Self::List => "list",
403 Self::MovableList => "movable-list",
404 Self::Tree => "tree",
405 }
406 }
407}
408
409impl std::fmt::Display for CrdtAnnotation {
410 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411 f.write_str(self.as_str())
412 }
413}
414
415#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
416pub struct ManifestIndex {
417 pub name: String,
418 pub fields: Vec<String>,
419 pub unique: bool,
420}
421
422#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
423pub struct ManifestRoute {
424 pub path: String,
425 pub mode: String,
426 #[serde(skip_serializing_if = "Option::is_none")]
427 pub query: Option<String>,
428 #[serde(skip_serializing_if = "Option::is_none")]
429 pub auth: Option<String>,
430}
431
432#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
433pub struct ManifestQuery {
434 pub name: String,
435 #[serde(default, skip_serializing_if = "Vec::is_empty")]
436 pub input: Vec<ManifestField>,
437}
438
439#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
440pub struct ManifestAction {
441 pub name: String,
442 #[serde(default, skip_serializing_if = "Vec::is_empty")]
443 pub input: Vec<ManifestField>,
444}
445
446#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
455pub struct ManifestPolicy {
456 pub name: String,
457 #[serde(skip_serializing_if = "Option::is_none")]
458 pub entity: Option<String>,
459 #[serde(skip_serializing_if = "Option::is_none")]
460 pub action: Option<String>,
461 #[serde(default, skip_serializing_if = "String::is_empty")]
462 pub allow: String,
463 #[serde(default, rename = "allowRead", skip_serializing_if = "Option::is_none")]
465 pub allow_read: Option<String>,
466 #[serde(
469 default,
470 rename = "allowInsert",
471 skip_serializing_if = "Option::is_none"
472 )]
473 pub allow_insert: Option<String>,
474 #[serde(
476 default,
477 rename = "allowUpdate",
478 skip_serializing_if = "Option::is_none"
479 )]
480 pub allow_update: Option<String>,
481 #[serde(
483 default,
484 rename = "allowDelete",
485 skip_serializing_if = "Option::is_none"
486 )]
487 pub allow_delete: Option<String>,
488 #[serde(
491 default,
492 rename = "allowWrite",
493 skip_serializing_if = "Option::is_none"
494 )]
495 pub allow_write: Option<String>,
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501
502 #[test]
503 fn exit_code_values() {
504 assert_eq!(ExitCode::Ok.as_i32(), 0);
505 assert_eq!(ExitCode::Error.as_i32(), 1);
506 assert_eq!(ExitCode::Usage.as_i32(), 64);
507 assert_eq!(ExitCode::Unavailable.as_i32(), 69);
508 }
509
510 #[test]
511 fn severity_display() {
512 assert_eq!(format!("{}", Severity::Error), "error");
513 assert_eq!(format!("{}", Severity::Warning), "warning");
514 assert_eq!(format!("{}", Severity::Info), "info");
515 }
516
517 #[test]
518 fn diagnostic_display_without_hint() {
519 let d = Diagnostic {
520 severity: Severity::Error,
521 code: "TEST".into(),
522 message: "something failed".into(),
523 span: None,
524 hint: None,
525 };
526 assert_eq!(format!("{d}"), "[error] TEST: something failed");
527 }
528
529 #[test]
530 fn diagnostic_display_with_hint() {
531 let d = Diagnostic {
532 severity: Severity::Warning,
533 code: "WARN".into(),
534 message: "check this".into(),
535 span: None,
536 hint: Some("try again".into()),
537 };
538 assert_eq!(
539 format!("{d}"),
540 "[warning] WARN: check this (hint: try again)"
541 );
542 }
543
544 #[test]
545 fn manifest_version_constant() {
546 assert_eq!(MANIFEST_VERSION, 1);
547 }
548}