Skip to main content

cinderblock_core/
lib.rs

1//! Core crate for the cinderblock framework — a declarative, resource-oriented
2//! application framework for Rust.
3//!
4//! This crate provides the [`resource!`] macro, the [`Resource`] trait, CRUD
5//! operation traits ([`Create`], [`Update`], [`Destroy`], [`ReadAction`]), the
6//! runtime [`Context`], and a built-in [`InMemoryDataLayer`](data_layer::in_memory::InMemoryDataLayer)
7//! for prototyping.
8//!
9//! # The `resource!` macro
10//!
11//! The [`resource!`] macro is the primary entry point for defining domain
12//! models. It accepts a declarative DSL and generates:
13//!
14//! - A **struct** with the declared attributes (derives `Serialize`,
15//!   `Deserialize`, `Clone`, `Debug`).
16//! - A [`Resource`] trait impl with primary key metadata and the configured
17//!   data layer.
18//! - For each action, a **marker struct** and the corresponding CRUD trait
19//!   impl. Create and update actions also generate an **input struct**.
20//! - Extension dispatch — each declared extension receives the full DSL
21//!   tokens so it can generate its own code (e.g. route handlers, SQL
22//!   queries).
23//!
24//! ## DSL reference
25//!
26//! ```rust,ignore
27//! use cinderblock_core::resource;
28//!
29//! resource! {
30//!     // A dotted name identifying the resource. The last segment becomes the
31//!     // struct name; all segments are available at runtime via `Resource::NAME`.
32//!     name = Helpdesk.Support.Ticket;
33//!
34//!     // Optional: override the data layer. Defaults to `InMemoryDataLayer`.
35//!     // data_layer = cinderblock_sqlx::sqlite::SqliteDataLayer;
36//!
37//!     attributes {
38//!         // Each attribute is `name Type` followed by either `;` or an options block.
39//!         ticket_id Uuid {
40//!             primary_key true;   // Marks this as the primary key (default: false).
41//!             writable false;     // Excludes from create/update input structs (default: true).
42//!             generated true;     // Indicates the PK is auto-generated (default: false).
43//!             default || Uuid::new_v4();  // Closure producing a default value.
44//!         }
45//!
46//!         // Simple form — writable, not a primary key, no default.
47//!         subject String;
48//!         status TicketStatus;
49//!     }
50//!
51//!     actions {
52//!         // ── Read actions ──
53//!         //
54//!         // A read action returns `Vec<Resource>`. It can optionally declare
55//!         // arguments (typed query parameters) and filters.
56//!
57//!         // Minimal read — no filters, no arguments. Arguments type is `()`.
58//!         read all;
59//!
60//!         // Read with a compile-time literal filter.
61//!         read open_tickets {
62//!             filter { status == TicketStatus::Open };
63//!         };
64//!
65//!         // Read with a runtime argument bound to a filter.
66//!         // Generates a `ByStatusArguments` struct with a `status` field.
67//!         read by_status {
68//!             argument { status: TicketStatus };
69//!             filter { status == arg(status) };
70//!         };
71//!
72//!         // Optional arguments use `Option<T>`. When `None`, the filter is
73//!         // skipped entirely at runtime.
74//!         read search {
75//!             argument { status: Option<TicketStatus> };
76//!             filter { status == arg(status) };
77//!         };
78//!
79//!         // ── Create actions ──
80//!         //
81//!         // A create action generates an input struct from the resource's
82//!         // writable attributes and a `Create<A>` impl that builds a new
83//!         // resource instance.
84//!
85//!         // Accepts all writable attributes. Generates `OpenInput { subject, status }`.
86//!         create open;
87//!
88//!         // Restrict which fields the input struct includes.
89//!         // Generates `AssignInput { subject }`.
90//!         create assign {
91//!             accept [subject];
92//!         };
93//!
94//!         // ── Update actions ──
95//!         //
96//!         // An update action fetches the resource by primary key, applies
97//!         // changes, and persists the result. It generates an input struct
98//!         // and an `Update<A>` impl.
99//!
100//!         // Accepts all writable attributes.
101//!         update edit;
102//!
103//!         // Accept no fields from the caller, but apply a programmatic
104//!         // mutation via `change_ref`. Multiple `change_ref` blocks are
105//!         // applied in order.
106//!         update close {
107//!             accept [];
108//!             change_ref |ticket| {
109//!                 ticket.status = TicketStatus::Closed;
110//!             };
111//!         };
112//!
113//!         // ── Destroy actions ──
114//!         //
115//!         // A destroy action deletes the resource by primary key.
116//!         destroy remove;
117//!     }
118//!
119//!     // Optional: declare extensions. Each extension module receives the
120//!     // full resource DSL and its own configuration block, then generates
121//!     // additional code (e.g. route handlers, SQL queries).
122//!     extensions {
123//!         cinderblock_json_api {
124//!             route = { method = GET; path = "/"; action = all; };
125//!             route = { method = POST; path = "/"; action = open; };
126//!         };
127//!
128//!         cinderblock_sqlx {
129//!             table = "tickets";
130//!         };
131//!     }
132//! }
133//! ```
134//!
135//! ## Generated items
136//!
137//! For a resource named `Helpdesk.Support.Ticket` with actions `open`
138//! (create), `close` (update), `open_tickets` (read), and `remove` (destroy),
139//! the macro generates:
140//!
141//! | Generated item | Kind | Description |
142//! |---|---|---|
143//! | `Ticket` | struct | The resource struct with all declared attributes |
144//! | `Open` | struct (marker) | Create action marker |
145//! | `OpenInput` | struct | Input fields for the `open` create action |
146//! | `Close` | struct (marker) | Update action marker |
147//! | `CloseInput` | struct | Input fields for the `close` update action |
148//! | `OpenTickets` | struct (marker) | Read action marker |
149//! | `Remove` | struct (marker) | Destroy action marker |
150//!
151//! Action names are converted to `PascalCase` for the marker and input struct
152//! names (e.g. `open_tickets` becomes `OpenTickets`, and its input struct
153//! would be `OpenTicketsInput`).
154//!
155//! ## Using the generated types
156//!
157//! ```rust,ignore
158//! use cinderblock_core::Context;
159//!
160//! let ctx = Context::new();
161//!
162//! // Create
163//! let ticket = cinderblock_core::create::<Ticket, Open>(
164//!     OpenInput { subject: "Printer is broken".into(), status: TicketStatus::Open },
165//!     &ctx,
166//! ).await?;
167//!
168//! // Read (with arguments)
169//! let open = cinderblock_core::read::<Ticket, ByStatus>(
170//!     &ctx,
171//!     &ByStatusArguments { status: TicketStatus::Open },
172//! ).await?;
173//!
174//! // Read (no arguments — pass `&()`)
175//! let all_open = cinderblock_core::read::<Ticket, OpenTickets>(&ctx, &()).await?;
176//!
177//! // Update
178//! let closed = cinderblock_core::update::<Ticket, Close>(
179//!     &ticket.ticket_id,
180//!     CloseInput {},
181//!     &ctx,
182//! ).await?;
183//!
184//! // Destroy
185//! let removed = cinderblock_core::destroy::<Ticket, Remove>(
186//!     &ticket.ticket_id,
187//!     &ctx,
188//! ).await?;
189//! ```
190
191use std::{
192    any::{Any, TypeId},
193    collections::HashMap,
194};
195
196pub use cinderblock_core_macros::resource;
197pub use serde;
198
199use crate::data_layer::DataLayer;
200
201pub mod data_layer;
202
203pub type Result<T, E = Box<dyn std::error::Error + Send + Sync>> = std::result::Result<T, E>;
204
205#[derive(Debug, Default)]
206pub struct Context {
207    data_layers: HashMap<TypeId, Box<dyn Any + Sync + Send + 'static>>,
208}
209
210impl Context {
211    /// Generate a new context to be used by cinderblock applications.
212    ///
213    /// # Data layers
214    ///
215    /// This methods adds a [`data_layer::in_memory::InMemoryDataLayer`] by default.
216    pub fn new() -> Self {
217        let mut this = Self::default();
218        this.register_data_layer(data_layer::in_memory::InMemoryDataLayer::new());
219        this
220    }
221
222    /// Register a data layer instance so resources can look it up at runtime.
223    pub fn register_data_layer<DL: std::fmt::Debug + Send + Sync + 'static>(
224        &mut self,
225        data_layer: DL,
226    ) {
227        self.data_layers
228            .insert(data_layer.type_id(), Box::new(data_layer));
229    }
230
231    fn get_data_layer<DL: 'static>(&self) -> &DL {
232        self.data_layers
233            .get(&TypeId::of::<DL>())
234            .expect("Requested data layer was not registered")
235            .downcast_ref()
236            .expect("Could not downcast value stored in data layer")
237    }
238}
239
240/// Marker trait for a resource.
241pub trait Resource:
242    serde::Serialize + serde::de::DeserializeOwned + Send + Sync + Clone + 'static
243{
244    /// Primary key type of the resource. Usually the type of the id for the resource.
245    type PrimaryKey: std::fmt::Display + serde::de::DeserializeOwned + Send + Sync;
246
247    /// Data layer that the resource uses.
248    type DataLayer: DataLayer<Self>;
249
250    /// Name with namespace of the resource. Each part of the array is a segment in the name
251    /// (i.e. MyApp.Blog.Post).
252    const NAME: &'static [&'static str];
253
254    /// Wether the primary key of the resource is generated
255    const PRIMARY_KEY_GENERATED: bool;
256
257    /// Mathos that returns the primary key of the resource
258    fn primary_key(&self) -> &Self::PrimaryKey;
259}
260
261/// Marker trait showing indicating that a struct is a read action.
262pub trait ReadAction {
263    /// Resource returned when calling the action.
264    type Output: Resource;
265
266    /// Arguments used to get the resource. Could be used in filters.
267    type Arguments: Sync;
268}
269
270/// Trait indicating that a [`DataLayer`] can perform [`ReadAction`] `A`.
271pub trait PerformRead<A: ReadAction> {
272    /// Perform the read action on the provided data layer.
273    fn read(&self, args: &A::Arguments) -> impl Future<Output = Result<Vec<A::Output>>>;
274}
275
276/// Trait placed on a [`Resource`] specifying how to create the resource using action `A`.
277pub trait Create<A>: Resource {
278    /// Input used to create the resource.
279    type Input;
280
281    /// Create an instance of the resource using [`Self::Input`].
282    fn from_create_input(input: Self::Input) -> Self;
283}
284
285/// Trait placed on a [`Resource`] specifying how to update a resource using action `A`.
286pub trait Update<A>: Resource {
287    /// Arguments to pass to [`Self::apply_update_input`].
288    type Input;
289
290    /// Update an instance of self using [`Self::Input`].
291    fn apply_update_input(&mut self, input: Self::Input);
292}
293
294/// Marker trait for destroy actions.
295pub trait Destroy<A>: Resource {}
296
297/// Create resource `R` using action `A`.
298pub async fn create<R, A>(input: R::Input, ctx: &Context) -> Result<R>
299where
300    R: Create<A>,
301{
302    let resource = R::from_create_input(input);
303    let dl = ctx.get_data_layer::<R::DataLayer>();
304    dl.create(resource.clone()).await?;
305    Ok(resource)
306}
307
308/// Update resource `R` using action `A`. First
309/// fetches an instance of `R` using the primary key.
310pub async fn update<R, A>(primary_key: &R::PrimaryKey, input: R::Input, ctx: &Context) -> Result<R>
311where
312    R: Update<A>,
313{
314    let dl = ctx.get_data_layer::<R::DataLayer>();
315    let mut resource = dl.read(primary_key).await?;
316    resource.apply_update_input(input);
317    dl.update(resource.clone()).await?;
318    Ok(resource)
319}
320
321/// Read resource `R` using action `A`.
322pub async fn read<R, A>(ctx: &Context, args: &A::Arguments) -> Result<Vec<R>>
323where
324    R: Resource,
325    A: ReadAction<Output = R>,
326    R::DataLayer: PerformRead<A>,
327{
328    let dl = ctx.get_data_layer::<R::DataLayer>();
329    PerformRead::<A>::read(dl, args).await
330}
331
332/// Destroy resource `R` using action `A`.
333pub async fn destroy<R, A>(primary_key: &R::PrimaryKey, ctx: &Context) -> Result<R>
334where
335    R: Destroy<A>,
336{
337    let dl = ctx.get_data_layer::<R::DataLayer>();
338    dl.destroy(primary_key).await
339}