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 /// Renames and opt-outs of inferred relationships (ADR 0014), by table
57 /// (its SQL name, schema-qualified name, or module name) and then by the
58 /// relationship's inferred name. Empty by default.
59 ///
60 /// ```
61 /// use rok_sqlc::RelationshipChange;
62 /// let mut config = rok_sqlc::Config::default();
63 /// let invoices = config.relationships.entry("invoices".into()).or_default();
64 /// invoices.insert("created_by_user".into(), RelationshipChange::Rename("author".into()));
65 /// invoices.insert("approved_by_user".into(), RelationshipChange::OptOut);
66 /// ```
67 pub relationships: BTreeMap<String, BTreeMap<String, RelationshipChange>>,
68 /// Module renames, by table (its schema-qualified name or its SQL name;
69 /// `public.` may be left out): the table generates as this module, and
70 /// its row struct, typed id, and key struct are named after it. This is
71 /// how same-named tables of two schemas both generate. Each new name must
72 /// be a snake_case Rust identifier, and every entry must name a table of
73 /// the schema. Empty by default.
74 ///
75 /// ```
76 /// let mut config = rok_sqlc::Config::default();
77 /// config.modules.insert("audit.users".into(), "audit_users".into());
78 /// ```
79 pub modules: BTreeMap<String, String>,
80}
81
82/// What configuration does to one inferred relationship (the
83/// `[relationships]` section of the config file).
84#[derive(Debug, Clone, PartialEq, Eq)]
85#[non_exhaustive]
86pub enum RelationshipChange {
87 /// Generate the relationship under this name instead: its loader becomes
88 /// `load_<name>`. The name must be a snake_case Rust identifier and must
89 /// not collide with the table's other relationships.
90 Rename(String),
91 /// Generate nothing for the relationship, nor for its other side (the
92 /// inverse of a belongs-to, or the belongs-to of an inverse).
93 OptOut,
94}
95
96/// An opt-in derive for generated types.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98#[non_exhaustive]
99pub enum Derive {
100 /// `serde::Serialize` and `serde::Deserialize`. Typed ids serialize as
101 /// their inner value and enum variants as their database label. The
102 /// consuming crate needs `serde` with its `derive` feature, and the
103 /// `serde` feature of `uuid`, `chrono`, and `rust_decimal` where the
104 /// generated code uses them; the generated `mod.rs` lists them.
105 Serde,
106}
107
108impl Derive {
109 /// The name the config file, the JSON payload, and `--derive` use.
110 pub fn name(self) -> &'static str {
111 match self {
112 Derive::Serde => "serde",
113 }
114 }
115}
116
117impl Default for Config {
118 fn default() -> Self {
119 Self {
120 schema: Vec::new(),
121 queries: Vec::new(),
122 output: PathBuf::from("src/db"),
123 resource: None,
124 dry_run: false,
125 check: false,
126 force: false,
127 lenient: false,
128 conventions: Conventions::default(),
129 derive: Vec::new(),
130 relationships: BTreeMap::new(),
131 modules: BTreeMap::new(),
132 }
133 }
134}
135
136/// Opt-in conventions: columns that mean the same thing in every table that
137/// has them (ADR 0010). Each convention applies only to the tables that have
138/// the named column, and all are off by default.
139///
140/// ```
141/// let mut conventions = rok_sqlc::Conventions::rok();
142/// conventions.soft_delete_column = Some("removed_at".into());
143/// let mut config = rok_sqlc::Config::default();
144/// config.conventions = conventions;
145/// ```
146#[derive(Debug, Clone, Default, PartialEq, Eq)]
147#[non_exhaustive]
148pub struct Conventions {
149 /// The tenant column, which scopes every generated operation on a table
150 /// to one tenant given by the caller.
151 pub tenant_column: Option<String>,
152 /// The PostgreSQL setting the generated session helper sets to the
153 /// current tenant, for row-level security policies.
154 pub tenant_setting: Option<String>,
155 /// The PostgreSQL setting the generated session helper sets to the
156 /// acting user, for audit triggers.
157 pub user_setting: Option<String>,
158 /// The soft-delete column: a nullable timestamp recording when a row was
159 /// deleted.
160 pub soft_delete_column: Option<String>,
161 /// Managed columns, maintained by the database (defaults and triggers):
162 /// left out of create inputs and update patches, kept in rows.
163 pub managed_columns: Vec<String>,
164 /// The version column, for optimistic concurrency on update.
165 pub version_column: Option<String>,
166}
167
168impl Conventions {
169 /// The preset names [`Conventions::preset`] knows.
170 pub const PRESETS: &[&str] = &["rok"];
171
172 /// The Rok ERP's conventions: tenant column `organization_id` (with the
173 /// settings `app.organization_id` and `app.user_id`), soft-delete column
174 /// `deleted_at`, managed columns `updated_at` and `row_version`, and
175 /// version column `row_version`.
176 pub fn rok() -> Self {
177 Self {
178 tenant_column: Some("organization_id".to_owned()),
179 tenant_setting: Some("app.organization_id".to_owned()),
180 user_setting: Some("app.user_id".to_owned()),
181 soft_delete_column: Some("deleted_at".to_owned()),
182 managed_columns: vec!["updated_at".to_owned(), "row_version".to_owned()],
183 version_column: Some("row_version".to_owned()),
184 }
185 }
186
187 /// The preset with this name (one of [`Conventions::PRESETS`]), or
188 /// `None` for an unknown name.
189 pub fn preset(name: &str) -> Option<Self> {
190 match name {
191 "rok" => Some(Self::rok()),
192 _ => None,
193 }
194 }
195}