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 #[serde(default)]
297 pub language: Option<String>,
298}
299
300impl ManifestSearchConfig {
301 pub fn is_empty(&self) -> bool {
302 self.text.is_empty() && self.facets.is_empty() && self.sortable.is_empty()
303 }
304
305 pub fn language_or_default(&self) -> &str {
309 self.language.as_deref().unwrap_or("english")
310 }
311}
312
313#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
314pub struct ManifestRelation {
315 pub name: String,
316 pub target: String,
317 pub field: String,
318 #[serde(default)]
319 pub many: bool,
320}
321
322#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
323pub struct ManifestField {
324 pub name: String,
325 #[serde(rename = "type")]
326 pub field_type: String,
327 pub optional: bool,
328 pub unique: bool,
329 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub crdt: Option<CrdtAnnotation>,
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
352#[serde(rename_all = "kebab-case")]
353pub enum CrdtAnnotation {
354 Lww,
356 Text,
358 Counter,
362 List,
364 #[serde(rename = "movable-list")]
367 MovableList,
368 Tree,
371}
372
373impl CrdtAnnotation {
374 pub fn as_str(self) -> &'static str {
377 match self {
378 Self::Lww => "lww",
379 Self::Text => "text",
380 Self::Counter => "counter",
381 Self::List => "list",
382 Self::MovableList => "movable-list",
383 Self::Tree => "tree",
384 }
385 }
386}
387
388impl std::fmt::Display for CrdtAnnotation {
389 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
390 f.write_str(self.as_str())
391 }
392}
393
394#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
395pub struct ManifestIndex {
396 pub name: String,
397 pub fields: Vec<String>,
398 pub unique: bool,
399}
400
401#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
402pub struct ManifestRoute {
403 pub path: String,
404 pub mode: String,
405 #[serde(skip_serializing_if = "Option::is_none")]
406 pub query: Option<String>,
407 #[serde(skip_serializing_if = "Option::is_none")]
408 pub auth: Option<String>,
409}
410
411#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
412pub struct ManifestQuery {
413 pub name: String,
414 #[serde(default, skip_serializing_if = "Vec::is_empty")]
415 pub input: Vec<ManifestField>,
416}
417
418#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
419pub struct ManifestAction {
420 pub name: String,
421 #[serde(default, skip_serializing_if = "Vec::is_empty")]
422 pub input: Vec<ManifestField>,
423}
424
425#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
434pub struct ManifestPolicy {
435 pub name: String,
436 #[serde(skip_serializing_if = "Option::is_none")]
437 pub entity: Option<String>,
438 #[serde(skip_serializing_if = "Option::is_none")]
439 pub action: Option<String>,
440 #[serde(default, skip_serializing_if = "String::is_empty")]
441 pub allow: String,
442 #[serde(default, rename = "allowRead", skip_serializing_if = "Option::is_none")]
444 pub allow_read: Option<String>,
445 #[serde(
448 default,
449 rename = "allowInsert",
450 skip_serializing_if = "Option::is_none"
451 )]
452 pub allow_insert: Option<String>,
453 #[serde(
455 default,
456 rename = "allowUpdate",
457 skip_serializing_if = "Option::is_none"
458 )]
459 pub allow_update: Option<String>,
460 #[serde(
462 default,
463 rename = "allowDelete",
464 skip_serializing_if = "Option::is_none"
465 )]
466 pub allow_delete: Option<String>,
467 #[serde(
470 default,
471 rename = "allowWrite",
472 skip_serializing_if = "Option::is_none"
473 )]
474 pub allow_write: Option<String>,
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 #[test]
482 fn exit_code_values() {
483 assert_eq!(ExitCode::Ok.as_i32(), 0);
484 assert_eq!(ExitCode::Error.as_i32(), 1);
485 assert_eq!(ExitCode::Usage.as_i32(), 64);
486 assert_eq!(ExitCode::Unavailable.as_i32(), 69);
487 }
488
489 #[test]
490 fn severity_display() {
491 assert_eq!(format!("{}", Severity::Error), "error");
492 assert_eq!(format!("{}", Severity::Warning), "warning");
493 assert_eq!(format!("{}", Severity::Info), "info");
494 }
495
496 #[test]
497 fn diagnostic_display_without_hint() {
498 let d = Diagnostic {
499 severity: Severity::Error,
500 code: "TEST".into(),
501 message: "something failed".into(),
502 span: None,
503 hint: None,
504 };
505 assert_eq!(format!("{d}"), "[error] TEST: something failed");
506 }
507
508 #[test]
509 fn diagnostic_display_with_hint() {
510 let d = Diagnostic {
511 severity: Severity::Warning,
512 code: "WARN".into(),
513 message: "check this".into(),
514 span: None,
515 hint: Some("try again".into()),
516 };
517 assert_eq!(
518 format!("{d}"),
519 "[warning] WARN: check this (hint: try again)"
520 );
521 }
522
523 #[test]
524 fn manifest_version_constant() {
525 assert_eq!(MANIFEST_VERSION, 1);
526 }
527}