cratestack-core 0.4.3

Rust-native schema-first framework for typed HTTP APIs, generated clients, and backend services.
Documentation

cratestack-core

Core types, traits, and error handling shared across the CrateStack workspace.

Overview

cratestack-core provides the foundational types that the rest of the workspace depends on:

  • Error handling: CoolError with HTTP status mapping and operator vs. public message split
  • Auth context: CoolContext, PrincipalContext, CoolAuthIdentity, AuthProvider
  • Schema AST: Schema, Model, Field, Procedure, MixinDecl, TypeDecl, EnumDecl
  • Audit: AuditEvent, AuditOperation, AuditActor, AuditSink, NoopAuditSink, MulticastAuditSink
  • Signed envelope: HmacEnvelope (HS256), KeyProvider, StaticKeyProvider, NonceStore, InMemoryNonceStore
  • Codec/envelope traits: CoolCodec, CoolEnvelope, NoEnvelope
  • Event bus: CoolEventBus, ModelEvent<T>, ModelEventKind, CoolEventEnvelope
  • Transaction isolation: TransactionIsolation
  • Decimal scalar: Decimal (compile-time backend)
  • Validators: validate_length, validate_range_i64, validate_range_decimal, validate_email, validate_uri, validate_iso4217

Installation

[dependencies]
cratestack-core = "0.2.2"

A Decimal backend feature must be selected. decimal-rust-decimal is the default; decimal-bigdecimal is reserved and not yet implemented (selecting it today is a compile error).

Error Handling

CoolError returns a safe public message to clients while keeping operator-only detail for tracing.

use cratestack_core::CoolError;
use http::StatusCode;

let err = CoolError::BadRequest("missing query parameter".to_owned());
assert_eq!(err.status_code(), StatusCode::BAD_REQUEST);
assert_eq!(err.public_message(), "missing query parameter");

// 5xx variants return a fixed canned public message; the inner string flows to `detail` only.
let err = CoolError::Database("connection refused".to_owned());
assert_eq!(err.public_message(), "internal error");
assert_eq!(err.detail(), Some("connection refused"));

Variants: BadRequest, NotAcceptable, Unauthorized, UnsupportedMediaType, Forbidden, NotFound, Conflict, Validation, PreconditionFailed, Codec, Database, Internal. The codec/database/internal variants are 5xx-mapped.

Auth Context

CoolContext carries the authenticated principal and arbitrary host-provided extensions.

use cratestack_core::{CoolContext, Value};

let ctx = CoolContext::anonymous();

let ctx = CoolContext::from_principal(Some(serde_json::json!({
    "id": "usr_123",
    "role": "admin",
    "tenant": { "id": "org_456" }
})))?;

assert_eq!(ctx.auth_field("id"), Some(&Value::String("usr_123".to_owned())));
assert_eq!(ctx.tenant_id(), Some("org_456"));

AuthProvider

Host applications implement AuthProvider to resolve auth from HTTP requests:

use cratestack_core::{AuthProvider, CoolContext, CoolError, RequestContext};

#[derive(Clone)]
struct MyAuthProvider;

impl AuthProvider for MyAuthProvider {
    type Error = CoolError;

    async fn authenticate(&self, request: &RequestContext<'_>) -> Result<CoolContext, Self::Error> {
        let token = request.headers
            .get("authorization")
            .and_then(|v| v.to_str().ok())
            .ok_or_else(|| CoolError::Unauthorized("missing token".to_owned()))?;
        // Validate and project into CoolContext...
        Ok(CoolContext::anonymous())
    }
}

AuthProvider is also implemented blanket for any Fn(&HeaderMap) -> Result<CoolContext, E> closure.

Audit Events

use cratestack_core::{AuditEvent, AuditOperation, AuditActor};
use chrono::Utc;

let event = AuditEvent {
    event_id: uuid::Uuid::new_v4(),
    schema_name: "banking".to_owned(),
    model: "Account".to_owned(),
    operation: AuditOperation::Update,
    primary_key: serde_json::json!({"id": "acc_123"}),
    actor: AuditActor {
        id: Some("usr_456".to_owned()),
        claims: Default::default(),
        ip: Some("192.168.1.1".to_owned()),
    },
    tenant: None,
    before: Some(serde_json::json!({"balance": 1000})),
    after: Some(serde_json::json!({"balance": 900})),
    request_id: Some("trace-abc".to_owned()),
    occurred_at: Utc::now(),
};

AuditSink is an async trait. The bundled implementations are NoopAuditSink and MulticastAuditSink. The in-database table written by cratestack-sqlx is treated as the canonical record; sinks are best-effort projections.

Decimal Backend

[dependencies]
cratestack-core = { version = "0.2.2", features = ["decimal-rust-decimal"] }
use cratestack_core::Decimal;

let amount: Decimal = "123.45".parse()?;

Transaction Isolation

use cratestack_core::TransactionIsolation;

let isolation = TransactionIsolation::parse("serializable")?;
assert_eq!(isolation.as_sql(), "SERIALIZABLE");

Accepts read_committed / read committed, repeatable_read / repeatable read, and serializable.

Signed Envelope (HMAC-SHA-256)

HmacEnvelope<K: KeyProvider> implements CoolEnvelope for HS256-signed messages. Production multi-replica deployments back the NonceStore with Redis so replay rejection holds cluster-wide.

See Also

License

MIT