derived_cms/
lib.rs

1//! Generate a CMS, complete with admin interface and headless API from Rust type definitions.
2//! Works in cunjunction with [serde] and [ormlite] and uses [axum] as a web server.
3//!
4//! Example
5//!
6//! ```rust,no_run
7//! # use axum::extract::State;
8//! use chrono::{DateTime, Utc};
9//! use derived_cms::{App, Entity, EntityBase, Input, app::AppError, context::{Context, ContextTrait}, entity, property::{Markdown, Text, Json}};
10//! use ormlite::{Model, sqlite::Sqlite};
11//! use serde::{Deserialize, Serialize, Serializer};
12//! # use serde_with::{serde_as, DisplayFromStr};
13//! # use thiserror::Error;
14//! use ts_rs::TS;
15//! use uuid::Uuid;
16//!
17//! #[derive(Debug, Deserialize, Serialize, Entity, Model, TS)]
18//! #[ts(export)]
19//! struct Post {
20//!     #[cms(id, skip_input)]
21//!     #[ormlite(primary_key)]
22//!     #[serde(default = "Uuid::new_v4")]
23//!     id: Uuid,
24//!     title: Text,
25//!     date: DateTime<Utc>,
26//!     #[cms(skip_column)]
27//!     #[serde(default)]
28//!     content: Json<Vec<Block>>,
29//!     #[serde(default)]
30//!     draft: bool,
31//! }
32//!
33//! type Ctx = Context<ormlite::Pool<sqlx::Sqlite>>;
34//!
35//! # #[serde_as]
36//! # #[derive(Debug, Error, Serialize)]
37//! # enum MyError {
38//! #     #[error(transparent)]
39//! #     Ormlite(
40//! #         #[from]
41//! #         #[serde_as(as = "DisplayFromStr")]
42//! #         ormlite::Error
43//! #     ),
44//! #     #[error(transparent)]
45//! #     Sqlx(
46//! #         #[from]
47//! #         #[serde_as(as = "DisplayFromStr")]
48//! #         sqlx::Error
49//! #     ),
50//! # }
51//! #
52//! # impl From<MyError> for AppError {
53//! #     fn from(value: MyError) -> Self {
54//! #         match value {
55//! #             MyError::Ormlite(e) => Self::new("Database error".to_string(), format!("{e:#}")),
56//! #             MyError::Sqlx(e) => Self::new("Database error".to_string(), format!("{e:#}")),
57//! #         }
58//! #     }
59//! # }
60//! #
61//! impl entity::Get<Ctx> for Post {
62//!     type RequestExt = State<Ctx>;
63//!     type Error = MyError;
64//!
65//!     async fn get(
66//!         id: &<Self as EntityBase<Ctx>>::Id,
67//!         ext: Self::RequestExt,
68//!     ) -> Result<Option<Self>, Self::Error> {
69//!         match Self::fetch_one(id, ext.ext()).await {
70//!             Ok(v) => Ok(Some(v)),
71//!             Err(ormlite::Error::SqlxError(sqlx::Error::RowNotFound)) => Ok(None),
72//!             Err(e) => Err(e)?,
73//!         }
74//!     }
75//! }
76//!
77//! impl entity::List<Ctx> for Post {
78//!     type RequestExt = State<Ctx>;
79//!     type Error = MyError;
80//!
81//!     async fn list(ext: Self::RequestExt) -> Result<impl IntoIterator<Item = Self>, Self::Error> {
82//!         Ok(Self::select().fetch_all(ext.ext()).await?)
83//!     }
84//! }
85//!
86//! impl entity::Create<Ctx> for Post {
87//!     type RequestExt = State<Ctx>;
88//!     type Error = MyError;
89//!
90//!     async fn create(
91//!         data: <Self as EntityBase<Ctx>>::Create,
92//!         ext: Self::RequestExt,
93//!     ) -> Result<Self, Self::Error> {
94//!         Ok(Self::insert(data, ext.ext()).await?)
95//!     }
96//! }
97//!
98//! impl entity::Update<Ctx> for Post {
99//!     type RequestExt = State<Ctx>;
100//!     type Error = MyError;
101//!
102//!     async fn update(
103//!         id: &<Self as EntityBase<Ctx>>::Id,
104//!         mut data: <Self as EntityBase<Ctx>>::Update,
105//!         ext: Self::RequestExt,
106//!     ) -> Result<Self, Self::Error> {
107//!         data.id = *id;
108//!         Ok(data.update_all_fields(ext.ext()).await?)
109//!     }
110//! }
111//!
112//! impl entity::Delete<Ctx> for Post {
113//!     type RequestExt = State<Ctx>;
114//!     type Error = MyError;
115//!
116//!     async fn delete(
117//!         id: &<Self as EntityBase<Ctx>>::Id,
118//!         ext: Self::RequestExt,
119//!     ) -> Result<(), Self::Error> {
120//!         sqlx::query("DELETE FROM post WHERE id = ?")
121//!             .bind(id)
122//!             .execute(ext.ext())
123//!             .await?;
124//!         Ok(())
125//!     }
126//! }
127//!
128//! #[derive(Debug, Deserialize, Serialize, Input, TS)]
129//! #[ts(export)]
130//! #[serde(rename_all = "snake_case", tag = "type", content = "data")]
131//! pub enum Block {
132//!     Separator,
133//!     Text(Markdown),
134//! }
135//!
136//! #[tokio::main]
137//! async fn main() {
138//!     let db = sqlx::Pool::<Sqlite>::connect("sqlite://.tmp/db.sqlite")
139//!         .await
140//!         .unwrap();
141//!     let app = App::new().entity::<Post>().with_state(db).build("uploads");
142//!     let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
143//!     axum::serve(listener, app).await.unwrap();
144//! }
145//! ```
146//!
147//! ## REST API
148//!
149//! A REST API is automatically generated for all `Entities`.
150//!
151//! List of generated endpoints, with [`name`](EntityBase::name) and [`name-plural`](EntityBase::name_plural)
152//! converted to [kebab-case](convert_case::Case::Kebab):
153//!
154//! - `GET /api/v1/:name-plural`:
155//!   - allows filtering by exact value in the query string, e. g. `?slug=asdf`. This currently
156//!     only works for fields whose SQL representation is a string.
157//!   - returns an array of [entities](Entity), serialized using [serde_json].
158//! - `GET /api/v1/:name/:id`
159//!   - get an [Entity] by it's [id](ormlite::TableMeta::primary_key).
160//!   - returns the requested of [Entity], serialized using [serde_json].
161//! - `POST /api/v1/:name-plural`
162//!   - create a new [Entity] from the request body JSON.
163//!   - returns the newly created [Entity] as JSON.
164//! - `POST /api/v1/:name/:id`
165//!   - replaces the [Entity] with the specified [id](ormlite::TableMeta::primary_key) with the
166//!     request body JSON.
167//!   - returns the updated [Entity] as JSON.
168//! - `DELETE /api/v1/:name/:id`
169//!   - deletes the [Entity] with the specified [id](ormlite::TableMeta::primary_key)
170//!   - returns the deleted Entity as JSON.
171
172pub use app::App;
173pub use column::Column;
174pub use entity::{Entity, EntityBase};
175pub use input::Input;
176
177pub mod app;
178pub mod column;
179pub mod context;
180pub mod easymde;
181mod endpoints;
182pub mod entity;
183pub mod input;
184pub mod property;
185pub mod render;
186
187#[doc(hidden)]
188pub mod derive {
189    pub use generic_array;
190    pub use i18n_embed;
191    pub use maud;
192    pub use ormlite;
193}
194
195#[cfg(feature = "sqlite")]
196pub type DB = sqlx::Sqlite;
197#[cfg(feature = "postgres")]
198pub type DB = sqlx::Postgres;