#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
mod handlers;
mod store;
use cratefield_core::{
Config, ConfigError, Migrations, Module, ModuleConfig, ModuleContext, PersonalDataSet, Port,
SqlMigration,
};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub(crate) enum Collections {
List(Vec<String>),
Any,
}
#[derive(Debug, Clone)]
pub(crate) struct Settings {
pub collections: Collections,
}
const MIGRATION_INIT: SqlMigration = SqlMigration::new(
"0001",
"init",
include_str!("../migrations/sqlite/0001_init.sql"),
);
pub struct Cms {
settings: Settings,
}
impl Default for Cms {
fn default() -> Self {
Self::new()
}
}
impl Cms {
#[must_use]
pub fn new() -> Self {
Self {
settings: Settings {
collections: Collections::List(Vec::new()),
},
}
}
#[must_use]
pub fn collections(mut self, collections: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.settings.collections =
Collections::List(collections.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn any_collection(mut self) -> Self {
self.settings.collections = Collections::Any;
self
}
}
fn is_slug(s: &str) -> bool {
!s.is_empty()
&& s.len() <= 64
&& s.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
&& !s.starts_with('-')
&& !s.ends_with('-')
&& !s.contains("--")
}
impl Module for Cms {
fn name(&self) -> &'static str {
"cms"
}
fn version(&self) -> &'static str {
env!("CARGO_PKG_VERSION")
}
fn requires(&self) -> &'static [Port] {
&[Port::Db]
}
fn tables(&self) -> &'static [&'static str] {
&["cms_item", "cms_revision"]
}
fn personal_data(&self) -> &'static [PersonalDataSet] {
const SETS: &[PersonalDataSet] = &[
PersonalDataSet::none(
"cms_item",
"The pages and posts this venture publishes, one row each: the title, the body \
and the structured fields an editor is working on. It is filed under a \
collection and a slug, and no column in it names a person.",
),
PersonalDataSet::none(
"cms_revision",
"What each page and post said at every publish, kept so the venture can always \
show what was public on a given day. The same content as above, and the same \
again: no column in it names a person.",
),
];
SETS
}
fn migrations(&self) -> Migrations {
const MIGRATIONS: [SqlMigration; 1] = [MIGRATION_INIT];
const _: () = cratefield_core::assert_migration_set(&MIGRATIONS);
Migrations {
sqlite: &MIGRATIONS,
postgres: &[],
}
}
fn validate_config(&self, cfg: &dyn Config) -> Result<(), ConfigError> {
let module = ModuleConfig::new("cms", cfg);
let mut errors = ConfigError::default();
if let Some(list) = cfg.get(&module.key("COLLECTIONS"))
&& list != "*"
&& list.split(',').map(str::trim).all(|slug| !is_slug(slug))
{
errors.push(format!(
"cms: {} must be a comma-separated list of kebab-case slugs or `*`",
module.key("COLLECTIONS")
));
}
errors.into_result()
}
fn router(&self, ctx: ModuleContext) -> axum::Router {
handlers::router(Arc::new(ctx), self.settings.clone())
}
fn surface(&self) -> cratefield_core::Surface {
handlers::surface(&self.settings)
}
}