cratefield_module_cms/
lib.rs1#![doc = include_str!("../README.md")]
30#![forbid(unsafe_code)]
31
32mod handlers;
33mod store;
34
35use cratefield_core::{
36 Config, ConfigError, Migrations, Module, ModuleConfig, ModuleContext, Port, SqlMigration,
37};
38use std::sync::Arc;
39
40#[derive(Debug, Clone)]
43pub(crate) enum Collections {
44 List(Vec<String>),
46 Any,
48}
49
50#[derive(Debug, Clone)]
52pub(crate) struct Settings {
53 pub collections: Collections,
54}
55
56const MIGRATION_INIT: SqlMigration = SqlMigration {
59 id: "0001",
60 name: "init",
61 sql: include_str!("../migrations/sqlite/0001_init.sql"),
62};
63
64pub struct Cms {
66 settings: Settings,
67}
68
69impl Default for Cms {
70 fn default() -> Self {
71 Self::new()
72 }
73}
74
75impl Cms {
76 #[must_use]
80 pub fn new() -> Self {
81 Self {
82 settings: Settings {
83 collections: Collections::List(Vec::new()),
84 },
85 }
86 }
87
88 #[must_use]
90 pub fn collections(mut self, collections: impl IntoIterator<Item = impl Into<String>>) -> Self {
91 self.settings.collections =
92 Collections::List(collections.into_iter().map(Into::into).collect());
93 self
94 }
95
96 #[must_use]
98 pub fn any_collection(mut self) -> Self {
99 self.settings.collections = Collections::Any;
100 self
101 }
102}
103
104fn is_slug(s: &str) -> bool {
106 !s.is_empty()
107 && s.len() <= 64
108 && s.chars()
109 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
110 && !s.starts_with('-')
111 && !s.ends_with('-')
112 && !s.contains("--")
113}
114
115impl Module for Cms {
116 fn name(&self) -> &'static str {
117 "cms"
118 }
119
120 fn version(&self) -> &'static str {
121 env!("CARGO_PKG_VERSION")
122 }
123
124 fn requires(&self) -> &'static [Port] {
125 &[Port::Db]
126 }
127
128 fn tables(&self) -> &'static [&'static str] {
129 &["cms_item", "cms_revision"]
130 }
131
132 fn migrations(&self) -> Migrations {
133 const MIGRATIONS: [SqlMigration; 1] = [MIGRATION_INIT];
134 Migrations {
135 sqlite: &MIGRATIONS,
136 postgres: &[],
137 }
138 }
139
140 fn validate_config(&self, cfg: &dyn Config) -> Result<(), ConfigError> {
141 let module = ModuleConfig::new("cms", cfg);
142 let mut errors = ConfigError::default();
143 if let Some(list) = cfg.get(&module.key("COLLECTIONS"))
144 && list != "*"
145 && list.split(',').map(str::trim).all(|slug| !is_slug(slug))
146 {
147 errors.push(format!(
148 "cms: {} must be a comma-separated list of kebab-case slugs or `*`",
149 module.key("COLLECTIONS")
150 ));
151 }
152 errors.into_result()
153 }
154
155 fn router(&self, ctx: ModuleContext) -> axum::Router {
156 handlers::router(Arc::new(ctx), self.settings.clone())
157 }
158
159 fn surface(&self) -> cratefield_core::Surface {
160 handlers::surface(&self.settings)
161 }
162}