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}