rok_sqlc/config.rs
1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4/// What to generate and how: the one configuration model behind the CLI and
5/// [`generate`](crate::generate).
6///
7/// New fields may be added in minor releases, so build a `Config` from
8/// [`Config::default`] and assign the fields you need:
9///
10/// ```no_run
11/// let mut config = rok_sqlc::Config::default();
12/// config.schema = vec!["migrations/0001_init.sql".into()];
13/// config.output = "src/db".into();
14/// rok_sqlc::generate(&config)?;
15/// # Ok::<(), rok_sqlc::Error>(())
16/// ```
17#[derive(Debug, Clone, PartialEq, Eq)]
18#[non_exhaustive]
19pub struct Config {
20 /// Schema inputs, combined in the order given: a file, or a directory
21 /// whose `*.sql` migrations are read in filename order, skipping
22 /// `*.down.sql`.
23 pub schema: Vec<PathBuf>,
24 /// Annotated query files or directories. A single file becomes the
25 /// generated `queries.rs`. A directory (its `*.sql` files) or several
26 /// inputs become one `queries/<stem>.rs` module per file, declared by a
27 /// generated `queries/mod.rs`; each stem must be a snake_case Rust
28 /// module name, and stems must be unique.
29 ///
30 /// Switching between the two layouts removes the old layout's generated
31 /// files as stale. Left empty, a previously generated `queries.rs` or
32 /// `queries/` is kept and still declared.
33 pub queries: Vec<PathBuf>,
34 /// The directory generated modules are written to. Defaults to `src/db`.
35 pub output: PathBuf,
36 /// Generate the table with this SQL or module name and regenerate the
37 /// resources already generated in the output directory; tables not
38 /// generated yet stay ungenerated.
39 pub resource: Option<String>,
40 /// Plan the run and report it without touching the filesystem.
41 pub dry_run: bool,
42 /// Write and remove nothing; report missing, changed, and stale files.
43 /// Any such drift fails the run. Takes precedence over `dry_run`.
44 pub check: bool,
45 /// Overwrite existing files that were not generated by rok-sqlc.
46 pub force: bool,
47 /// Skip unsupported tables and queries (listed in the report) instead
48 /// of failing.
49 pub lenient: bool,
50 /// Opt-in conventions about what certain columns mean across tables.
51 /// All off by default; [`Conventions::rok`] gives the Rok preset.
52 pub conventions: Conventions,
53 /// Extra derives for the generated rows, typed ids, key structs, enums,
54 /// inputs, and patches (ADR 0013). Empty by default.
55 pub derive: Vec<Derive>,
56 /// HTTP frameworks to generate validated-body extractors for, into
57 /// `http/` of the output: `mod.rs` and a module per framework (ADR 0015).
58 /// Needs [`Derive::Validate`], and reserves the module name `http`. Empty
59 /// by default.
60 pub extractors: Vec<Extractor>,
61 /// Renames and opt-outs of inferred relationships (ADR 0014), by table
62 /// (its SQL name, schema-qualified name, or module name) and then by the
63 /// relationship's inferred name. Empty by default.
64 ///
65 /// ```
66 /// use rok_sqlc::RelationshipChange;
67 /// let mut config = rok_sqlc::Config::default();
68 /// let invoices = config.relationships.entry("invoices".into()).or_default();
69 /// invoices.insert("created_by_user".into(), RelationshipChange::Rename("author".into()));
70 /// invoices.insert("approved_by_user".into(), RelationshipChange::OptOut);
71 /// ```
72 pub relationships: BTreeMap<String, BTreeMap<String, RelationshipChange>>,
73 /// Module renames, by table (its schema-qualified name or its SQL name;
74 /// `public.` may be left out): the table generates as this module, and
75 /// its row struct, typed id, and key struct are named after it. This is
76 /// how same-named tables of two schemas both generate. Each new name must
77 /// be a snake_case Rust identifier, and every entry must name a table of
78 /// the schema. Empty by default.
79 ///
80 /// ```
81 /// let mut config = rok_sqlc::Config::default();
82 /// config.modules.insert("audit.users".into(), "audit_users".into());
83 /// ```
84 pub modules: BTreeMap<String, String>,
85}
86
87/// What configuration does to one inferred relationship (the
88/// `[relationships]` section of the config file).
89#[derive(Debug, Clone, PartialEq, Eq)]
90#[non_exhaustive]
91pub enum RelationshipChange {
92 /// Generate the relationship under this name instead: its loader becomes
93 /// `load_<name>`. The name must be a snake_case Rust identifier and must
94 /// not collide with the table's other relationships.
95 Rename(String),
96 /// Generate nothing for the relationship, nor for its other side (the
97 /// inverse of a belongs-to, or the belongs-to of an inverse).
98 OptOut,
99}
100
101/// An opt-in derive for generated types.
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
103#[non_exhaustive]
104pub enum Derive {
105 /// `serde::Serialize` and `serde::Deserialize`. Typed ids serialize as
106 /// their inner value and enum variants as their database label. The
107 /// consuming crate needs `serde` with its `derive` feature, and the
108 /// `serde` feature of `uuid`, `chrono`, and `rust_decimal` where the
109 /// generated code uses them; the generated `mod.rs` lists them.
110 Serde,
111 /// `validator::Validate` for create inputs and update patches, with
112 /// rules derived from the schema (ADR 0015). Needs [`Derive::Serde`].
113 /// The consuming crate needs `validator` with its `derive` feature.
114 Validate,
115}
116
117impl Derive {
118 /// The name the config file, the JSON payload, and `--derive` use.
119 pub fn name(self) -> &'static str {
120 match self {
121 Derive::Serde => "serde",
122 Derive::Validate => "validate",
123 }
124 }
125}
126
127/// An HTTP framework to generate a validated-body extractor for
128/// ([`Config::extractors`]).
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
130#[non_exhaustive]
131pub enum Extractor {
132 /// axum: `http::axum::ValidatedJson<T>`. The consuming crate needs
133 /// `axum` and `serde_path_to_error`.
134 Axum,
135 /// actix-web: `http::actix_web::ValidatedJson<T>`. The consuming crate
136 /// needs `actix-web`.
137 ActixWeb,
138}
139
140impl Extractor {
141 /// The name the config file, the JSON payload, and `--extractor` use.
142 pub fn name(self) -> &'static str {
143 match self {
144 Extractor::Axum => "axum",
145 Extractor::ActixWeb => "actix_web",
146 }
147 }
148}
149
150impl Default for Config {
151 fn default() -> Self {
152 Self {
153 schema: Vec::new(),
154 queries: Vec::new(),
155 output: PathBuf::from("src/db"),
156 resource: None,
157 dry_run: false,
158 check: false,
159 force: false,
160 lenient: false,
161 conventions: Conventions::default(),
162 derive: Vec::new(),
163 extractors: Vec::new(),
164 relationships: BTreeMap::new(),
165 modules: BTreeMap::new(),
166 }
167 }
168}
169
170/// Opt-in conventions: columns that mean the same thing in every table that
171/// has them (ADR 0010). Each convention applies only to the tables that have
172/// the named column, and all are off by default.
173///
174/// ```
175/// let mut conventions = rok_sqlc::Conventions::rok();
176/// conventions.soft_delete_column = Some("removed_at".into());
177/// let mut config = rok_sqlc::Config::default();
178/// config.conventions = conventions;
179/// ```
180#[derive(Debug, Clone, Default, PartialEq, Eq)]
181#[non_exhaustive]
182pub struct Conventions {
183 /// The tenant column, which scopes every generated operation on a table
184 /// to one tenant given by the caller.
185 pub tenant_column: Option<String>,
186 /// The PostgreSQL setting the generated session helper sets to the
187 /// current tenant, for row-level security policies.
188 pub tenant_setting: Option<String>,
189 /// The PostgreSQL setting the generated session helper sets to the
190 /// acting user, for audit triggers.
191 pub user_setting: Option<String>,
192 /// The soft-delete column: a nullable timestamp recording when a row was
193 /// deleted.
194 pub soft_delete_column: Option<String>,
195 /// Managed columns, maintained by the database (defaults and triggers):
196 /// left out of create inputs and update patches, kept in rows.
197 pub managed_columns: Vec<String>,
198 /// The version column, for optimistic concurrency on update.
199 pub version_column: Option<String>,
200 /// How generated code makes new primary key values for tables keyed by
201 /// one `UUID` column (or, with [`IdGeneration::Ulid`], one text column).
202 /// Such a table's typed id gets a `generate()` constructor, and its
203 /// `create` input takes the key as an `Option`: `None` inserts a new id
204 /// made in Rust (over any database default). Off by default.
205 pub primary_key_ids: Option<IdGeneration>,
206}
207
208/// How generated code makes a new primary key value
209/// ([`Conventions::primary_key_ids`]).
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
211#[non_exhaustive]
212pub enum IdGeneration {
213 /// A random UUID (version 4), for `UUID` keys. Needs the `uuid` crate's
214 /// `v4` feature.
215 UuidV4,
216 /// A time-ordered UUID (version 7), for `UUID` keys. Needs the `uuid`
217 /// crate's `v7` feature.
218 UuidV7,
219 /// A ULID: time-ordered, stored as a UUID in a `UUID` key, or as its
220 /// 26-character text in a text key. Needs the `ulid` crate (with its
221 /// `uuid` feature for `UUID` keys).
222 Ulid,
223}
224
225impl IdGeneration {
226 /// The name the config file and the JSON payload use.
227 pub fn name(self) -> &'static str {
228 match self {
229 IdGeneration::UuidV4 => "uuid_v4",
230 IdGeneration::UuidV7 => "uuid_v7",
231 IdGeneration::Ulid => "ulid",
232 }
233 }
234}
235
236impl Conventions {
237 /// The preset names [`Conventions::preset`] knows.
238 pub const PRESETS: &[&str] = &["rok"];
239
240 /// The Rok ERP's conventions: tenant column `organization_id` (with the
241 /// settings `app.organization_id` and `app.user_id`), soft-delete column
242 /// `deleted_at`, managed columns `updated_at` and `row_version`, and
243 /// version column `row_version`.
244 pub fn rok() -> Self {
245 Self {
246 tenant_column: Some("organization_id".to_owned()),
247 tenant_setting: Some("app.organization_id".to_owned()),
248 user_setting: Some("app.user_id".to_owned()),
249 soft_delete_column: Some("deleted_at".to_owned()),
250 managed_columns: vec!["updated_at".to_owned(), "row_version".to_owned()],
251 version_column: Some("row_version".to_owned()),
252 primary_key_ids: None,
253 }
254 }
255
256 /// The preset with this name (one of [`Conventions::PRESETS`]), or
257 /// `None` for an unknown name.
258 pub fn preset(name: &str) -> Option<Self> {
259 match name {
260 "rok" => Some(Self::rok()),
261 _ => None,
262 }
263 }
264}