1use std::collections::BTreeMap;
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::{Arc, RwLock};
5
6use http::StatusCode;
7use serde::{Deserialize, Serialize};
8
9pub type CoolBody = bytes::Bytes;
10pub type CoolEventFuture = Pin<Box<dyn Future<Output = Result<(), CoolError>> + Send + 'static>>;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub struct SourceSpan {
14 pub start: usize,
15 pub end: usize,
16 pub line: usize,
17}
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub struct Schema {
21 pub datasource: Option<Datasource>,
22 pub auth: Option<AuthBlock>,
23 pub config_blocks: Vec<ConfigBlock>,
24 pub mixins: Vec<MixinDecl>,
25 pub models: Vec<Model>,
26 pub types: Vec<TypeDecl>,
27 pub enums: Vec<EnumDecl>,
28 pub procedures: Vec<Procedure>,
29}
30
31impl Schema {
32 pub fn summary(&self) -> OwnedSchemaSummary {
33 OwnedSchemaSummary {
34 mixins: self.mixins.iter().map(|mixin| mixin.name.clone()).collect(),
35 models: self.models.iter().map(|model| model.name.clone()).collect(),
36 types: self.types.iter().map(|ty| ty.name.clone()).collect(),
37 enums: self
38 .enums
39 .iter()
40 .map(|enum_decl| enum_decl.name.clone())
41 .collect(),
42 procedures: self
43 .procedures
44 .iter()
45 .map(|procedure| procedure.name.clone())
46 .collect(),
47 }
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct SchemaSummary {
53 pub mixins: &'static [&'static str],
54 pub models: &'static [&'static str],
55 pub types: &'static [&'static str],
56 pub enums: &'static [&'static str],
57 pub procedures: &'static [&'static str],
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61pub struct OwnedSchemaSummary {
62 pub mixins: Vec<String>,
63 pub models: Vec<String>,
64 pub types: Vec<String>,
65 pub enums: Vec<String>,
66 pub procedures: Vec<String>,
67}
68
69#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
70pub struct Datasource {
71 pub docs: Vec<String>,
72 pub name: String,
73 pub entries: Vec<ConfigEntry>,
74 pub span: SourceSpan,
75}
76
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct AuthBlock {
79 pub docs: Vec<String>,
80 pub name: String,
81 pub fields: Vec<Field>,
82 pub span: SourceSpan,
83}
84
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86pub struct ConfigBlock {
87 pub docs: Vec<String>,
88 pub name: String,
89 pub entries: Vec<String>,
90 pub span: SourceSpan,
91}
92
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
94pub struct ConfigEntry {
95 pub key: String,
96 pub value: String,
97}
98
99#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
100pub struct Model {
101 pub docs: Vec<String>,
102 pub name: String,
103 pub name_span: SourceSpan,
104 pub fields: Vec<Field>,
105 pub attributes: Vec<Attribute>,
106 pub span: SourceSpan,
107}
108
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
110pub struct MixinDecl {
111 pub docs: Vec<String>,
112 pub name: String,
113 pub name_span: SourceSpan,
114 pub fields: Vec<Field>,
115 pub span: SourceSpan,
116}
117
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub struct TypeDecl {
120 pub docs: Vec<String>,
121 pub name: String,
122 pub name_span: SourceSpan,
123 pub fields: Vec<Field>,
124 pub span: SourceSpan,
125}
126
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128pub struct EnumDecl {
129 pub docs: Vec<String>,
130 pub name: String,
131 pub name_span: SourceSpan,
132 pub variants: Vec<EnumVariant>,
133 pub span: SourceSpan,
134}
135
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
137pub struct EnumVariant {
138 pub docs: Vec<String>,
139 pub name: String,
140 pub span: SourceSpan,
141}
142
143#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
144pub struct Field {
145 pub docs: Vec<String>,
146 pub name: String,
147 pub name_span: SourceSpan,
148 pub ty: TypeRef,
149 pub attributes: Vec<Attribute>,
150 pub span: SourceSpan,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154pub struct TypeRef {
155 pub name: String,
156 pub name_span: SourceSpan,
157 pub arity: TypeArity,
158 pub generic_args: Vec<TypeRef>,
159}
160
161impl TypeRef {
162 pub fn is_page(&self) -> bool {
163 self.name == "Page"
164 }
165
166 pub fn page_item(&self) -> Option<&TypeRef> {
167 if self.is_page() {
168 self.generic_args.first()
169 } else {
170 None
171 }
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
176pub enum TypeArity {
177 Required,
178 Optional,
179 List,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
183#[serde(rename_all = "camelCase")]
184pub struct PageInfo {
185 pub limit: Option<i64>,
186 pub offset: Option<i64>,
187 pub has_next_page: bool,
188 pub has_previous_page: bool,
189}
190
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192#[serde(rename_all = "camelCase")]
193pub struct Page<T> {
194 pub items: Vec<T>,
195 pub total_count: Option<i64>,
196 pub page_info: PageInfo,
197}
198
199impl<T> Page<T> {
200 pub fn new(items: Vec<T>, page_info: PageInfo) -> Self {
201 Self {
202 items,
203 total_count: None,
204 page_info,
205 }
206 }
207
208 pub fn with_total_count(mut self, total_count: Option<i64>) -> Self {
209 self.total_count = total_count;
210 self
211 }
212}
213
214#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
215pub struct Procedure {
216 pub docs: Vec<String>,
217 pub name: String,
218 pub name_span: SourceSpan,
219 pub kind: ProcedureKind,
220 pub args: Vec<ProcedureArg>,
221 pub return_type: TypeRef,
222 pub attributes: Vec<Attribute>,
223 pub span: SourceSpan,
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
227pub enum ProcedureKind {
228 Query,
229 Mutation,
230}
231
232#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233pub struct ProcedureArg {
234 pub docs: Vec<String>,
235 pub name: String,
236 pub name_span: SourceSpan,
237 pub ty: TypeRef,
238 pub span: SourceSpan,
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242pub struct Attribute {
243 pub raw: String,
244 pub span: SourceSpan,
245}
246
247#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
248pub struct SelectionQuery {
249 pub fields: Vec<String>,
250 pub includes: Vec<String>,
251 pub include_fields: BTreeMap<String, Vec<String>>,
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
255pub enum ModelEventKind {
256 Created,
257 Updated,
258 Deleted,
259}
260
261impl ModelEventKind {
262 pub const fn as_str(self) -> &'static str {
263 match self {
264 Self::Created => "created",
265 Self::Updated => "updated",
266 Self::Deleted => "deleted",
267 }
268 }
269
270 pub fn parse(value: &str) -> Result<Self, CoolError> {
271 match value {
272 "created" => Ok(Self::Created),
273 "updated" => Ok(Self::Updated),
274 "deleted" => Ok(Self::Deleted),
275 other => Err(CoolError::Validation(format!(
276 "unsupported model event operation `{other}`"
277 ))),
278 }
279 }
280}
281
282#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
283pub struct CoolEventEnvelope {
284 pub event_id: uuid::Uuid,
285 pub model: String,
286 pub operation: ModelEventKind,
287 pub occurred_at: chrono::DateTime<chrono::Utc>,
288 pub data: serde_json::Value,
289}
290
291#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
292pub struct ModelEvent<T> {
293 pub event_id: uuid::Uuid,
294 pub model: String,
295 pub operation: ModelEventKind,
296 pub occurred_at: chrono::DateTime<chrono::Utc>,
297 pub data: T,
298}
299
300impl<T> TryFrom<CoolEventEnvelope> for ModelEvent<T>
301where
302 T: serde::de::DeserializeOwned,
303{
304 type Error = CoolError;
305
306 fn try_from(value: CoolEventEnvelope) -> Result<Self, Self::Error> {
307 Ok(Self {
308 event_id: value.event_id,
309 model: value.model,
310 operation: value.operation,
311 occurred_at: value.occurred_at,
312 data: serde_json::from_value(value.data).map_err(|error| {
313 CoolError::Codec(format!("failed to decode event payload: {error}"))
314 })?,
315 })
316 }
317}
318
319type EventHandler = Arc<dyn Fn(CoolEventEnvelope) -> CoolEventFuture + Send + Sync>;
320
321#[derive(Clone, Default)]
322pub struct CoolEventBus {
323 handlers: Arc<RwLock<BTreeMap<String, Vec<EventHandler>>>>,
324}
325
326impl std::fmt::Debug for CoolEventBus {
327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328 let handler_count = self
329 .handlers
330 .read()
331 .map(|handlers| handlers.values().map(Vec::len).sum::<usize>())
332 .unwrap_or_default();
333 f.debug_struct("CoolEventBus")
334 .field("handler_count", &handler_count)
335 .finish()
336 }
337}
338
339impl CoolEventBus {
340 pub fn subscribe<F>(&self, model: &'static str, operation: ModelEventKind, handler: F)
341 where
342 F: Fn(CoolEventEnvelope) -> CoolEventFuture + Send + Sync + 'static,
343 {
344 let mut handlers = self
345 .handlers
346 .write()
347 .expect("event bus handler registry should not be poisoned");
348 handlers
349 .entry(event_topic(model, operation))
350 .or_default()
351 .push(Arc::new(handler));
352 }
353
354 pub async fn emit(&self, envelope: CoolEventEnvelope) -> Result<(), CoolError> {
355 let handlers = self
356 .handlers
357 .read()
358 .expect("event bus handler registry should not be poisoned")
359 .get(&event_topic(&envelope.model, envelope.operation))
360 .cloned()
361 .unwrap_or_default();
362
363 for handler in handlers {
364 handler(envelope.clone()).await?;
365 }
366
367 Ok(())
368 }
369}
370
371pub fn event_topic(model: &str, operation: ModelEventKind) -> String {
372 format!("{}.{}", model, operation.as_str())
373}
374
375pub fn parse_emit_attribute(raw: &str) -> Result<Vec<ModelEventKind>, String> {
376 let Some(inner) = raw
377 .strip_prefix("@@emit(")
378 .and_then(|value| value.strip_suffix(')'))
379 else {
380 return Err(format!("unsupported event attribute `{raw}`"));
381 };
382
383 let mut operations = Vec::new();
384 for part in inner
385 .split(',')
386 .map(str::trim)
387 .filter(|part| !part.is_empty())
388 {
389 let operation = match part {
390 "created" => ModelEventKind::Created,
391 "updated" => ModelEventKind::Updated,
392 "deleted" => ModelEventKind::Deleted,
393 other => {
394 return Err(format!(
395 "unsupported event operation `{other}` in `{raw}`; expected created, updated, or deleted"
396 ));
397 }
398 };
399 if !operations.contains(&operation) {
400 operations.push(operation);
401 }
402 }
403
404 if operations.is_empty() {
405 return Err(format!(
406 "event attribute `{raw}` must declare at least one operation"
407 ));
408 }
409
410 Ok(operations)
411}
412
413#[derive(Debug, Clone, Copy, PartialEq, Eq)]
414pub struct RouteTransportCapabilities {
415 pub request_types: &'static [&'static str],
416 pub response_types: &'static [&'static str],
417 pub default_response_type: &'static str,
418 pub supports_sequence_response: bool,
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422pub struct RouteTransportDescriptor {
423 pub name: &'static str,
424 pub method: &'static str,
425 pub path: &'static str,
426 pub capabilities: RouteTransportCapabilities,
427}
428
429impl SelectionQuery {
430 pub fn is_empty(&self) -> bool {
431 self.fields.is_empty() && self.includes.is_empty() && self.include_fields.is_empty()
432 }
433}
434
435pub fn canonical_request_string(
436 method: &str,
437 path: &str,
438 canonical_query: Option<&str>,
439 content_type: Option<&str>,
440 body: &[u8],
441) -> String {
442 let query = canonical_query.unwrap_or_default();
443 let content_type = content_type.unwrap_or_default();
444 let body_hex = body
445 .iter()
446 .map(|byte| format!("{byte:02x}"))
447 .collect::<String>();
448 format!("{method}\n{path}\n{query}\n{content_type}\n{body_hex}")
449}
450
451#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
452pub enum Value {
453 Null,
454 Bool(bool),
455 Int(i64),
456 Float(f64),
457 String(String),
458 Bytes(Vec<u8>),
459 List(Vec<Value>),
460 Map(BTreeMap<String, Value>),
461}
462
463#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
464pub struct CoolAuthIdentity {
465 pub fields: BTreeMap<String, Value>,
466}
467
468#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
469pub struct PrincipalFacet {
470 pub fields: BTreeMap<String, Value>,
471}
472
473#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
474pub struct PrincipalContext {
475 pub actor: Option<PrincipalFacet>,
476 pub session: Option<PrincipalFacet>,
477 pub tenant: Option<PrincipalFacet>,
478 pub claims: BTreeMap<String, Value>,
479}
480
481#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
482pub struct CoolContext {
483 pub auth: Option<CoolAuthIdentity>,
484 pub principal: Option<PrincipalContext>,
485 pub extensions: BTreeMap<String, Value>,
486}
487
488#[derive(Debug, Clone, Copy)]
489pub struct RequestContext<'a> {
490 pub method: &'a str,
491 pub path: &'a str,
492 pub query: Option<&'a str>,
493 pub headers: &'a http::HeaderMap,
494 pub body: &'a [u8],
495}
496
497pub trait AuthProvider: Clone + Send + Sync + 'static {
498 type Error: Into<CoolError> + Send;
499
500 fn authenticate(
501 &self,
502 request: &RequestContext<'_>,
503 ) -> impl ::core::future::Future<Output = Result<CoolContext, Self::Error>> + Send;
504}
505
506impl<F, E> AuthProvider for F
507where
508 F: Clone + Send + Sync + 'static + for<'a> Fn(&'a http::HeaderMap) -> Result<CoolContext, E>,
509 E: Into<CoolError> + Send,
510{
511 type Error = E;
512
513 fn authenticate(
514 &self,
515 request: &RequestContext<'_>,
516 ) -> impl ::core::future::Future<Output = Result<CoolContext, Self::Error>> + Send {
517 let result = (self)(request.headers);
518 ::core::future::ready(result)
519 }
520}
521
522impl CoolContext {
523 pub fn anonymous() -> Self {
524 Self::default()
525 }
526
527 pub fn authenticated(fields: impl IntoIterator<Item = (String, Value)>) -> Self {
528 let fields = fields.into_iter().collect::<BTreeMap<_, _>>();
529 Self {
530 auth: Some(CoolAuthIdentity {
531 fields: fields.clone(),
532 }),
533 principal: Some(PrincipalContext::from_claims(fields)),
534 extensions: BTreeMap::new(),
535 }
536 }
537
538 pub fn is_authenticated(&self) -> bool {
539 self.auth.is_some() || self.principal.is_some()
540 }
541
542 pub fn auth_field(&self, name: &str) -> Option<&Value> {
543 if let Some(auth) = self.auth.as_ref()
544 && let Some(value) = auth
545 .fields
546 .get(name)
547 .or_else(|| lookup_value_path_in_map(&auth.fields, name))
548 {
549 return Some(value);
550 }
551
552 self.principal
553 .as_ref()
554 .and_then(|principal| principal.field(name))
555 }
556
557 pub fn from_principal<P: Serialize>(principal: Option<P>) -> Result<Self, CoolError> {
558 let Some(principal) = principal else {
559 return Ok(Self::anonymous());
560 };
561
562 let principal = PrincipalContext::from_principal(principal)?;
563 let auth = principal.as_auth_identity();
564 Ok(Self {
565 auth: Some(auth),
566 principal: Some(principal),
567 extensions: BTreeMap::new(),
568 })
569 }
570
571 pub fn with_principal(principal: PrincipalContext) -> Self {
572 Self {
573 auth: Some(principal.as_auth_identity()),
574 principal: Some(principal),
575 extensions: BTreeMap::new(),
576 }
577 }
578}
579
580impl PrincipalContext {
581 pub fn from_principal<P: Serialize>(principal: P) -> Result<Self, CoolError> {
582 let auth = CoolAuthIdentity::from_principal(principal)?;
583 Ok(Self::from_auth_identity(&auth))
584 }
585
586 pub fn from_claims(claims: BTreeMap<String, Value>) -> Self {
587 Self {
588 actor: None,
589 session: None,
590 tenant: None,
591 claims,
592 }
593 }
594
595 pub fn from_auth_identity(auth: &CoolAuthIdentity) -> Self {
596 let mut claims = auth.fields.clone();
597 let actor = take_principal_facet(&mut claims, "actor");
598 let session = take_principal_facet(&mut claims, "session");
599 let tenant = take_principal_facet(&mut claims, "tenant");
600 Self {
601 actor,
602 session,
603 tenant,
604 claims,
605 }
606 }
607
608 pub fn field(&self, name: &str) -> Option<&Value> {
609 if let Some(value) = self
610 .claims
611 .get(name)
612 .or_else(|| lookup_value_path_in_map(&self.claims, name))
613 {
614 return Some(value);
615 }
616
617 let (root, rest) = name.split_once('.')?;
618 match root {
619 "actor" => lookup_principal_facet_path(self.actor.as_ref(), rest),
620 "session" => lookup_principal_facet_path(self.session.as_ref(), rest),
621 "tenant" => lookup_principal_facet_path(self.tenant.as_ref(), rest),
622 _ => None,
623 }
624 }
625
626 pub fn as_auth_identity(&self) -> CoolAuthIdentity {
627 CoolAuthIdentity {
628 fields: self.legacy_fields(),
629 }
630 }
631
632 pub fn legacy_fields(&self) -> BTreeMap<String, Value> {
633 let mut fields = self.claims.clone();
634 if let Some(actor) = &self.actor {
635 fields.insert("actor".to_owned(), Value::Map(actor.fields.clone()));
636 }
637 if let Some(session) = &self.session {
638 fields.insert("session".to_owned(), Value::Map(session.fields.clone()));
639 }
640 if let Some(tenant) = &self.tenant {
641 fields.insert("tenant".to_owned(), Value::Map(tenant.fields.clone()));
642 }
643 fields
644 }
645}
646
647impl CoolAuthIdentity {
648 pub fn from_principal<P: Serialize>(principal: P) -> Result<Self, CoolError> {
649 let value = serde_json::to_value(principal).map_err(|error| {
650 CoolError::Internal(format!("failed to serialize auth principal: {error}"))
651 })?;
652 let serde_json::Value::Object(object) = value else {
653 return Err(CoolError::Internal(
654 "auth principal must serialize to a JSON object".to_owned(),
655 ));
656 };
657
658 let mut fields = BTreeMap::new();
659 for (key, value) in object {
660 fields.insert(key, json_value_to_cool_value(value)?);
661 }
662
663 Ok(Self { fields })
664 }
665}
666
667fn json_value_to_cool_value(value: serde_json::Value) -> Result<Value, CoolError> {
668 match value {
669 serde_json::Value::Null => Ok(Value::Null),
670 serde_json::Value::Bool(value) => Ok(Value::Bool(value)),
671 serde_json::Value::Number(number) => {
672 if let Some(value) = number.as_i64() {
673 Ok(Value::Int(value))
674 } else if let Some(value) = number.as_f64() {
675 Ok(Value::Float(value))
676 } else {
677 Err(CoolError::Internal(format!(
678 "unsupported auth principal number '{number}'"
679 )))
680 }
681 }
682 serde_json::Value::String(value) => Ok(Value::String(value)),
683 serde_json::Value::Array(values) => values
684 .into_iter()
685 .map(json_value_to_cool_value)
686 .collect::<Result<Vec<_>, _>>()
687 .map(Value::List),
688 serde_json::Value::Object(object) => object
689 .into_iter()
690 .map(|(key, value)| json_value_to_cool_value(value).map(|value| (key, value)))
691 .collect::<Result<BTreeMap<_, _>, _>>()
692 .map(Value::Map),
693 }
694}
695
696fn lookup_value_path_in_map<'a>(map: &'a BTreeMap<String, Value>, path: &str) -> Option<&'a Value> {
697 let mut segments = path.split('.');
698 let first = segments.next()?;
699 let mut current = map.get(first)?;
700 for segment in segments {
701 current = match current {
702 Value::Map(entries) => entries.get(segment)?,
703 _ => return None,
704 };
705 }
706 Some(current)
707}
708
709fn lookup_principal_facet_path<'a>(
710 facet: Option<&'a PrincipalFacet>,
711 path: &str,
712) -> Option<&'a Value> {
713 let facet = facet?;
714 facet
715 .fields
716 .get(path)
717 .or_else(|| lookup_value_path_in_map(&facet.fields, path))
718}
719
720fn take_principal_facet(claims: &mut BTreeMap<String, Value>, key: &str) -> Option<PrincipalFacet> {
721 match claims.remove(key) {
722 Some(Value::Map(fields)) => Some(PrincipalFacet { fields }),
723 Some(value) => {
724 claims.insert(key.to_owned(), value);
725 None
726 }
727 None => None,
728 }
729}
730
731#[cfg(test)]
732mod tests {
733 use super::*;
734
735 #[test]
736 fn auth_field_prefers_exact_key_before_dotted_lookup() {
737 let ctx = CoolContext::authenticated([
738 ("tenant.slug".to_owned(), Value::String("exact".to_owned())),
739 (
740 "tenant".to_owned(),
741 Value::Map(BTreeMap::from([(
742 "slug".to_owned(),
743 Value::String("nested".to_owned()),
744 )])),
745 ),
746 ]);
747
748 assert_eq!(
749 ctx.auth_field("tenant.slug"),
750 Some(&Value::String("exact".to_owned()))
751 );
752 }
753
754 #[test]
755 fn auth_field_resolves_nested_map_paths() {
756 let ctx = CoolContext::from_principal(Some(serde_json::json!({
757 "tenant": {
758 "slug": "acme",
759 "owner": { "id": 7 }
760 }
761 })))
762 .expect("principal should bind");
763
764 assert_eq!(
765 ctx.auth_field("tenant.slug"),
766 Some(&Value::String("acme".to_owned()))
767 );
768 assert_eq!(ctx.auth_field("tenant.owner.id"), Some(&Value::Int(7)));
769 assert!(ctx.auth_field("tenant.owner.missing").is_none());
770 }
771
772 #[test]
773 fn from_principal_promotes_actor_session_and_tenant_facets() {
774 let ctx = CoolContext::from_principal(Some(serde_json::json!({
775 "actor": { "id": "usr_1" },
776 "session": { "id": "sess_1" },
777 "tenant": { "id": "org_1" },
778 "role": "admin"
779 })))
780 .expect("principal should bind");
781
782 let principal = ctx.principal.expect("principal should exist");
783 assert_eq!(
784 principal
785 .actor
786 .as_ref()
787 .and_then(|facet| facet.fields.get("id")),
788 Some(&Value::String("usr_1".to_owned()))
789 );
790 assert_eq!(
791 principal
792 .session
793 .as_ref()
794 .and_then(|facet| facet.fields.get("id")),
795 Some(&Value::String("sess_1".to_owned()))
796 );
797 assert_eq!(
798 principal
799 .tenant
800 .as_ref()
801 .and_then(|facet| facet.fields.get("id")),
802 Some(&Value::String("org_1".to_owned()))
803 );
804 assert_eq!(
805 principal.claims.get("role"),
806 Some(&Value::String("admin".to_owned()))
807 );
808 }
809}
810
811pub fn parse_cuid(value: &str) -> Result<String, CoolError> {
812 if is_valid_cuid(value) {
813 Ok(value.to_owned())
814 } else {
815 Err(CoolError::BadRequest(format!(
816 "invalid cuid '{}': expected a lowercase alphanumeric id starting with 'c'",
817 value,
818 )))
819 }
820}
821
822fn is_valid_cuid(value: &str) -> bool {
823 let mut chars = value.chars();
824 let Some(first) = chars.next() else {
825 return false;
826 };
827 if first != 'c' || value.len() < 2 {
828 return false;
829 }
830 chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit())
831}
832
833#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
834pub struct CoolErrorResponse {
835 pub code: String,
836 pub message: String,
837 pub details: Option<Value>,
838}
839
840#[derive(Debug, thiserror::Error)]
841pub enum CoolError {
842 #[error("bad request: {0}")]
843 BadRequest(String),
844 #[error("not acceptable: {0}")]
845 NotAcceptable(String),
846 #[error("unauthorized: {0}")]
847 Unauthorized(String),
848 #[error("unsupported media type: {0}")]
849 UnsupportedMediaType(String),
850 #[error("forbidden: {0}")]
851 Forbidden(String),
852 #[error("not found: {0}")]
853 NotFound(String),
854 #[error("conflict: {0}")]
855 Conflict(String),
856 #[error("validation: {0}")]
857 Validation(String),
858 #[error("codec: {0}")]
859 Codec(String),
860 #[error("database: {0}")]
861 Database(String),
862 #[error("internal: {0}")]
863 Internal(String),
864}
865
866impl CoolError {
867 pub fn code(&self) -> &'static str {
868 match self {
869 Self::BadRequest(_) => "BAD_REQUEST",
870 Self::NotAcceptable(_) => "NOT_ACCEPTABLE",
871 Self::Unauthorized(_) => "UNAUTHORIZED",
872 Self::UnsupportedMediaType(_) => "UNSUPPORTED_MEDIA_TYPE",
873 Self::Forbidden(_) => "FORBIDDEN",
874 Self::NotFound(_) => "NOT_FOUND",
875 Self::Conflict(_) => "CONFLICT",
876 Self::Validation(_) => "VALIDATION_ERROR",
877 Self::Codec(_) => "CODEC_ERROR",
878 Self::Database(_) => "DATABASE_ERROR",
879 Self::Internal(_) => "INTERNAL_ERROR",
880 }
881 }
882
883 pub fn status_code(&self) -> StatusCode {
884 match self {
885 Self::BadRequest(_) => StatusCode::BAD_REQUEST,
886 Self::NotAcceptable(_) => StatusCode::NOT_ACCEPTABLE,
887 Self::Unauthorized(_) => StatusCode::UNAUTHORIZED,
888 Self::UnsupportedMediaType(_) => StatusCode::UNSUPPORTED_MEDIA_TYPE,
889 Self::Forbidden(_) => StatusCode::FORBIDDEN,
890 Self::NotFound(_) => StatusCode::NOT_FOUND,
891 Self::Conflict(_) => StatusCode::CONFLICT,
892 Self::Validation(_) => StatusCode::UNPROCESSABLE_ENTITY,
893 Self::Codec(_) => StatusCode::BAD_REQUEST,
894 Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
895 Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
896 }
897 }
898
899 pub fn into_response(self) -> CoolErrorResponse {
900 CoolErrorResponse {
901 code: self.code().to_owned(),
902 message: self.to_string(),
903 details: None,
904 }
905 }
906}
907
908pub trait CoolCodec: Clone + Send + Sync + 'static {
909 const CONTENT_TYPE: &'static str;
910
911 fn encode<T: Serialize + ?Sized>(&self, value: &T) -> Result<Vec<u8>, CoolError>;
912
913 fn decode<T: for<'de> Deserialize<'de>>(&self, bytes: &[u8]) -> Result<T, CoolError>;
914}
915
916pub trait CoolEnvelope: Clone + Send + Sync + 'static {
917 fn request_content_type(&self) -> &'static str;
918
919 fn response_content_type(&self) -> &'static str;
920
921 fn open_request(&self, bytes: &[u8], _ctx: &mut CoolContext) -> Result<Vec<u8>, CoolError>;
922
923 fn seal_response(&self, bytes: &[u8], _ctx: &CoolContext) -> Result<Vec<u8>, CoolError>;
924}
925
926#[derive(Debug, Clone, Default)]
927pub struct NoEnvelope;
928
929impl CoolEnvelope for NoEnvelope {
930 fn request_content_type(&self) -> &'static str {
931 "application/octet-stream"
932 }
933
934 fn response_content_type(&self) -> &'static str {
935 "application/octet-stream"
936 }
937
938 fn open_request(&self, bytes: &[u8], _ctx: &mut CoolContext) -> Result<Vec<u8>, CoolError> {
939 Ok(bytes.to_vec())
940 }
941
942 fn seal_response(&self, bytes: &[u8], _ctx: &CoolContext) -> Result<Vec<u8>, CoolError> {
943 Ok(bytes.to_vec())
944 }
945}