Skip to main content

cratestack_core/
lib.rs

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}