1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5pub mod clock;
6pub mod errors;
7pub mod util;
8
9pub use clock::{Clock, MockClock, SystemClock};
10
11pub const VERSION: &str = env!("CARGO_PKG_VERSION");
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ExitCode {
19 Ok = 0,
20 Error = 1,
21 Usage = 64,
22 Unavailable = 69,
23}
24
25impl ExitCode {
26 pub const fn as_i32(self) -> i32 {
27 self as i32
28 }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "lowercase")]
37pub enum Severity {
38 Error,
39 Warning,
40 Info,
41}
42
43impl fmt::Display for Severity {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 Severity::Error => f.write_str("error"),
47 Severity::Warning => f.write_str("warning"),
48 Severity::Info => f.write_str("info"),
49 }
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct Span {
56 pub file: String,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub line: Option<u32>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub column: Option<u32>,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68pub struct Diagnostic {
69 pub severity: Severity,
70 pub code: String,
71 pub message: String,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub span: Option<Span>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub hint: Option<String>,
76}
77
78impl fmt::Display for Diagnostic {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 write!(f, "[{}] {}: {}", self.severity, self.code, self.message)?;
81 if let Some(hint) = &self.hint {
82 write!(f, " (hint: {hint})")?;
83 }
84 Ok(())
85 }
86}
87
88pub const MANIFEST_VERSION: u32 = 1;
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
95pub struct AppManifest {
96 pub manifest_version: u32,
97 pub name: String,
98 pub version: String,
99 pub entities: Vec<ManifestEntity>,
100 pub routes: Vec<ManifestRoute>,
101 #[serde(default)]
102 pub queries: Vec<ManifestQuery>,
103 #[serde(default)]
104 pub actions: Vec<ManifestAction>,
105 #[serde(default)]
106 pub policies: Vec<ManifestPolicy>,
107 #[serde(default)]
118 pub auth: ManifestAuthConfig,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
125pub struct ManifestAuthConfig {
126 #[serde(default)]
127 pub user: ManifestAuthUserConfig,
128 #[serde(default)]
129 pub session: ManifestAuthSessionConfig,
130 #[serde(default, skip_serializing_if = "Vec::is_empty")]
133 pub trusted_origins: Vec<String>,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137pub struct ManifestAuthUserConfig {
138 #[serde(default = "default_user_entity")]
142 pub entity: String,
143 #[serde(default, skip_serializing_if = "Vec::is_empty")]
147 pub expose: Vec<String>,
148 #[serde(default, skip_serializing_if = "Vec::is_empty")]
153 pub hide: Vec<String>,
154}
155
156impl Default for ManifestAuthUserConfig {
157 fn default() -> Self {
158 Self {
159 entity: default_user_entity(),
160 expose: Vec::new(),
161 hide: Vec::new(),
162 }
163 }
164}
165
166fn default_user_entity() -> String {
167 "User".into()
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
171pub struct ManifestAuthSessionConfig {
172 #[serde(default = "default_session_lifetime")]
174 pub expires_in: u64,
175 #[serde(default)]
180 pub cookie_cache: ManifestAuthCookieCacheConfig,
181}
182
183impl Default for ManifestAuthSessionConfig {
184 fn default() -> Self {
185 Self {
186 expires_in: default_session_lifetime(),
187 cookie_cache: ManifestAuthCookieCacheConfig::default(),
188 }
189 }
190}
191
192fn default_session_lifetime() -> u64 {
193 30 * 24 * 60 * 60
194}
195
196#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202pub struct ManifestAuthCookieCacheConfig {
203 #[serde(default)]
204 pub enabled: bool,
205 #[serde(default = "default_cookie_cache_max_age")]
209 pub max_age: u64,
210 #[serde(default = "default_cookie_cache_claims")]
213 pub claims: Vec<String>,
214}
215
216impl Default for ManifestAuthCookieCacheConfig {
217 fn default() -> Self {
218 Self {
219 enabled: false,
220 max_age: default_cookie_cache_max_age(),
221 claims: default_cookie_cache_claims(),
222 }
223 }
224}
225
226fn default_cookie_cache_max_age() -> u64 {
227 5 * 60
228}
229
230fn default_cookie_cache_claims() -> Vec<String> {
231 vec!["is_admin".into(), "tenant_id".into()]
232}
233
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235pub struct ManifestEntity {
236 pub name: String,
237 pub fields: Vec<ManifestField>,
238 pub indexes: Vec<ManifestIndex>,
239 #[serde(default, skip_serializing_if = "Vec::is_empty")]
240 pub relations: Vec<ManifestRelation>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub search: Option<ManifestSearchConfig>,
246 #[serde(default = "default_crdt_enabled")]
254 pub crdt: bool,
255}
256
257fn default_crdt_enabled() -> bool {
258 true
259}
260
261impl Default for ManifestEntity {
262 fn default() -> Self {
263 Self {
264 name: String::new(),
265 fields: Vec::new(),
266 indexes: Vec::new(),
267 relations: Vec::new(),
268 search: None,
269 crdt: true,
270 }
271 }
272}
273
274#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
282pub struct ManifestSearchConfig {
283 #[serde(default)]
284 pub text: Vec<String>,
285 #[serde(default)]
286 pub facets: Vec<String>,
287 #[serde(default)]
288 pub sortable: Vec<String>,
289}
290
291impl ManifestSearchConfig {
292 pub fn is_empty(&self) -> bool {
293 self.text.is_empty() && self.facets.is_empty() && self.sortable.is_empty()
294 }
295}
296
297#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
298pub struct ManifestRelation {
299 pub name: String,
300 pub target: String,
301 pub field: String,
302 #[serde(default)]
303 pub many: bool,
304}
305
306#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
307pub struct ManifestField {
308 pub name: String,
309 #[serde(rename = "type")]
310 pub field_type: String,
311 pub optional: bool,
312 pub unique: bool,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
321 pub crdt: Option<CrdtAnnotation>,
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
336#[serde(rename_all = "kebab-case")]
337pub enum CrdtAnnotation {
338 Lww,
340 Text,
342 Counter,
346 List,
348 #[serde(rename = "movable-list")]
351 MovableList,
352 Tree,
355}
356
357impl CrdtAnnotation {
358 pub fn as_str(self) -> &'static str {
361 match self {
362 Self::Lww => "lww",
363 Self::Text => "text",
364 Self::Counter => "counter",
365 Self::List => "list",
366 Self::MovableList => "movable-list",
367 Self::Tree => "tree",
368 }
369 }
370}
371
372impl std::fmt::Display for CrdtAnnotation {
373 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374 f.write_str(self.as_str())
375 }
376}
377
378#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
379pub struct ManifestIndex {
380 pub name: String,
381 pub fields: Vec<String>,
382 pub unique: bool,
383}
384
385#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
386pub struct ManifestRoute {
387 pub path: String,
388 pub mode: String,
389 #[serde(skip_serializing_if = "Option::is_none")]
390 pub query: Option<String>,
391 #[serde(skip_serializing_if = "Option::is_none")]
392 pub auth: Option<String>,
393}
394
395#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
396pub struct ManifestQuery {
397 pub name: String,
398 #[serde(default, skip_serializing_if = "Vec::is_empty")]
399 pub input: Vec<ManifestField>,
400}
401
402#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
403pub struct ManifestAction {
404 pub name: String,
405 #[serde(default, skip_serializing_if = "Vec::is_empty")]
406 pub input: Vec<ManifestField>,
407}
408
409#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
418pub struct ManifestPolicy {
419 pub name: String,
420 #[serde(skip_serializing_if = "Option::is_none")]
421 pub entity: Option<String>,
422 #[serde(skip_serializing_if = "Option::is_none")]
423 pub action: Option<String>,
424 #[serde(default, skip_serializing_if = "String::is_empty")]
425 pub allow: String,
426 #[serde(default, rename = "allowRead", skip_serializing_if = "Option::is_none")]
428 pub allow_read: Option<String>,
429 #[serde(
432 default,
433 rename = "allowInsert",
434 skip_serializing_if = "Option::is_none"
435 )]
436 pub allow_insert: Option<String>,
437 #[serde(
439 default,
440 rename = "allowUpdate",
441 skip_serializing_if = "Option::is_none"
442 )]
443 pub allow_update: Option<String>,
444 #[serde(
446 default,
447 rename = "allowDelete",
448 skip_serializing_if = "Option::is_none"
449 )]
450 pub allow_delete: Option<String>,
451 #[serde(
454 default,
455 rename = "allowWrite",
456 skip_serializing_if = "Option::is_none"
457 )]
458 pub allow_write: Option<String>,
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn exit_code_values() {
467 assert_eq!(ExitCode::Ok.as_i32(), 0);
468 assert_eq!(ExitCode::Error.as_i32(), 1);
469 assert_eq!(ExitCode::Usage.as_i32(), 64);
470 assert_eq!(ExitCode::Unavailable.as_i32(), 69);
471 }
472
473 #[test]
474 fn severity_display() {
475 assert_eq!(format!("{}", Severity::Error), "error");
476 assert_eq!(format!("{}", Severity::Warning), "warning");
477 assert_eq!(format!("{}", Severity::Info), "info");
478 }
479
480 #[test]
481 fn diagnostic_display_without_hint() {
482 let d = Diagnostic {
483 severity: Severity::Error,
484 code: "TEST".into(),
485 message: "something failed".into(),
486 span: None,
487 hint: None,
488 };
489 assert_eq!(format!("{d}"), "[error] TEST: something failed");
490 }
491
492 #[test]
493 fn diagnostic_display_with_hint() {
494 let d = Diagnostic {
495 severity: Severity::Warning,
496 code: "WARN".into(),
497 message: "check this".into(),
498 span: None,
499 hint: Some("try again".into()),
500 };
501 assert_eq!(
502 format!("{d}"),
503 "[warning] WARN: check this (hint: try again)"
504 );
505 }
506
507 #[test]
508 fn manifest_version_constant() {
509 assert_eq!(MANIFEST_VERSION, 1);
510 }
511}