Skip to main content

icydb_config/
model.rs

1use std::{
2    collections::BTreeMap,
3    path::{Path, PathBuf},
4};
5
6pub(crate) const DEFAULT_SQL_READONLY_ENABLED: bool = false;
7pub(crate) const DEFAULT_SQL_DDL_ENABLED: bool = false;
8pub(crate) const DEFAULT_SQL_FIXTURES_ENABLED: bool = false;
9pub(crate) const DEFAULT_SQL_INTROSPECTION_LOCAL_ENABLED: bool = true;
10pub(crate) const DEFAULT_SQL_INTROSPECTION_IC_ENABLED: bool = false;
11pub(crate) const DEFAULT_SQL_UPDATE_POLICY: Option<GeneratedSqlUpdatePolicy> = None;
12pub(crate) const DEFAULT_METRICS_ENABLED: bool = false;
13pub(crate) const DEFAULT_METRICS_EXTENDED_ENABLED: bool = false;
14pub(crate) const DEFAULT_SNAPSHOT_ENABLED: bool = false;
15pub(crate) const DEFAULT_SCHEMA_ENABLED: bool = false;
16
17/// Resolved IcyDB config and the path it came from, if a manifest exists.
18#[derive(Clone, Debug, Default, Eq, PartialEq)]
19pub struct ResolvedIcydbConfig {
20    config_path: Option<PathBuf>,
21    config: GeneratedIcydbConfig,
22}
23
24impl ResolvedIcydbConfig {
25    /// Return the resolved config path, or `None` when no config file exists.
26    #[must_use]
27    pub fn config_path(&self) -> Option<&Path> {
28        self.config_path.as_deref()
29    }
30
31    /// Borrow the validated generated config model.
32    #[must_use]
33    pub const fn config(&self) -> &GeneratedIcydbConfig {
34        &self.config
35    }
36
37    pub(crate) const fn new(config_path: Option<PathBuf>, config: GeneratedIcydbConfig) -> Self {
38        Self {
39            config_path,
40            config,
41        }
42    }
43}
44
45/// Validated IcyDB project config ready for build-script consumption.
46#[derive(Clone, Debug, Default, Eq, PartialEq)]
47pub struct GeneratedIcydbConfig {
48    canisters: BTreeMap<String, GeneratedCanisterConfig>,
49    build_target: GeneratedBuildTarget,
50}
51
52impl GeneratedIcydbConfig {
53    /// Borrow validated per-canister config entries.
54    #[must_use]
55    pub const fn canisters(&self) -> &BTreeMap<String, GeneratedCanisterConfig> {
56        &self.canisters
57    }
58
59    /// Return the build target used to resolve target-sensitive generated settings.
60    #[must_use]
61    pub const fn build_target(&self) -> GeneratedBuildTarget {
62        self.build_target
63    }
64
65    /// Return whether read-only SQL should be generated for one canister.
66    #[must_use]
67    pub fn canister_sql_readonly_enabled(&self, canister_name: &str) -> bool {
68        self.canister_enabled(
69            canister_name,
70            DEFAULT_SQL_READONLY_ENABLED,
71            GeneratedCanisterConfig::sql_readonly,
72        )
73    }
74
75    /// Return whether the SQL DDL endpoint should be generated for one canister.
76    #[must_use]
77    pub fn canister_sql_ddl_enabled(&self, canister_name: &str) -> bool {
78        self.canister_enabled(
79            canister_name,
80            DEFAULT_SQL_DDL_ENABLED,
81            GeneratedCanisterConfig::sql_ddl,
82        )
83    }
84
85    /// Return whether SQL fixture lifecycle endpoints should be generated for one canister.
86    #[must_use]
87    pub fn canister_sql_fixtures_enabled(&self, canister_name: &str) -> bool {
88        self.canister_enabled(
89            canister_name,
90            DEFAULT_SQL_FIXTURES_ENABLED,
91            GeneratedCanisterConfig::sql_fixtures,
92        )
93    }
94
95    /// Return whether generated read-only SQL should admit operational introspection.
96    #[must_use]
97    pub fn canister_sql_introspection_enabled(&self, canister_name: &str) -> bool {
98        self.canister_sql_introspection_policy(canister_name)
99            .enabled_for(self.build_target)
100    }
101
102    /// Return the local/IC introspection policy for one canister.
103    #[must_use]
104    pub fn canister_sql_introspection_policy(
105        &self,
106        canister_name: &str,
107    ) -> GeneratedSqlIntrospectionPolicy {
108        self.canisters.get(canister_name).map_or_else(
109            GeneratedSqlIntrospectionPolicy::default,
110            GeneratedCanisterConfig::sql_introspection_policy,
111        )
112    }
113
114    /// Return the configured generated SQL update endpoint policy, if any.
115    #[must_use]
116    pub fn canister_sql_update_policy(
117        &self,
118        canister_name: &str,
119    ) -> Option<GeneratedSqlUpdatePolicy> {
120        self.canisters.get(canister_name).map_or(
121            DEFAULT_SQL_UPDATE_POLICY,
122            GeneratedCanisterConfig::sql_update_policy,
123        )
124    }
125
126    /// Return whether metrics report endpoints should be generated for one canister.
127    #[must_use]
128    pub fn canister_metrics_enabled(&self, canister_name: &str) -> bool {
129        self.canister_enabled(
130            canister_name,
131            DEFAULT_METRICS_ENABLED,
132            GeneratedCanisterConfig::metrics,
133        )
134    }
135
136    /// Return whether extended metrics report endpoints should be generated for one canister.
137    #[must_use]
138    pub fn canister_metrics_extended_enabled(&self, canister_name: &str) -> bool {
139        self.canister_enabled(
140            canister_name,
141            DEFAULT_METRICS_EXTENDED_ENABLED,
142            GeneratedCanisterConfig::metrics_extended,
143        )
144    }
145
146    /// Return whether storage snapshot endpoints should be generated for one canister.
147    #[must_use]
148    pub fn canister_snapshot_enabled(&self, canister_name: &str) -> bool {
149        self.canister_enabled(
150            canister_name,
151            DEFAULT_SNAPSHOT_ENABLED,
152            GeneratedCanisterConfig::snapshot,
153        )
154    }
155
156    /// Return whether schema report endpoints should be generated for one canister.
157    #[must_use]
158    pub fn canister_schema_enabled(&self, canister_name: &str) -> bool {
159        self.canister_enabled(
160            canister_name,
161            DEFAULT_SCHEMA_ENABLED,
162            GeneratedCanisterConfig::schema,
163        )
164    }
165
166    pub(crate) const fn new(canisters: BTreeMap<String, GeneratedCanisterConfig>) -> Self {
167        Self {
168            canisters,
169            build_target: GeneratedBuildTarget::Unknown,
170        }
171    }
172
173    pub(crate) const fn with_build_target(mut self, build_target: GeneratedBuildTarget) -> Self {
174        self.build_target = build_target;
175
176        self
177    }
178
179    fn canister_enabled(
180        &self,
181        canister_name: &str,
182        default: bool,
183        is_enabled: impl FnOnce(&GeneratedCanisterConfig) -> bool,
184    ) -> bool {
185        self.canisters
186            .get(canister_name)
187            .map_or(default, is_enabled)
188    }
189}
190
191/// Validated generated settings for one canister.
192#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
193pub struct GeneratedCanisterConfig {
194    sql: GeneratedCanisterSqlConfig,
195    metrics: GeneratedCanisterMetricsConfig,
196    snapshot: bool,
197    schema: bool,
198}
199
200impl GeneratedCanisterConfig {
201    pub(crate) const fn new(
202        sql: GeneratedCanisterSqlConfig,
203        metrics: GeneratedCanisterMetricsConfig,
204        snapshot: bool,
205        schema: bool,
206    ) -> Self {
207        Self {
208            sql,
209            metrics,
210            snapshot,
211            schema,
212        }
213    }
214
215    /// Return whether generated actor glue should export read-only SQL endpoints.
216    #[must_use]
217    pub const fn sql_readonly(&self) -> bool {
218        self.sql.readonly
219    }
220
221    /// Return whether generated actor glue should export the SQL DDL endpoint.
222    #[must_use]
223    pub const fn sql_ddl(&self) -> bool {
224        self.sql.ddl
225    }
226
227    /// Return whether generated actor glue should export SQL fixture lifecycle endpoints.
228    #[must_use]
229    pub const fn sql_fixtures(&self) -> bool {
230        self.sql.fixtures
231    }
232
233    /// Return the local/IC policy for operational SQL introspection.
234    #[must_use]
235    pub const fn sql_introspection_policy(&self) -> GeneratedSqlIntrospectionPolicy {
236        self.sql.introspection_policy
237    }
238
239    /// Return the generated SQL update endpoint policy, if explicitly selected.
240    #[must_use]
241    pub const fn sql_update_policy(&self) -> Option<GeneratedSqlUpdatePolicy> {
242        self.sql.update_policy
243    }
244
245    /// Return whether generated actor glue should export metrics report endpoints.
246    #[must_use]
247    pub const fn metrics(&self) -> bool {
248        self.metrics.enabled
249    }
250
251    /// Return whether generated actor glue should export extended metrics report endpoints.
252    #[must_use]
253    pub const fn metrics_extended(&self) -> bool {
254        self.metrics.extended
255    }
256
257    /// Return whether generated actor glue should export storage snapshot endpoints.
258    #[must_use]
259    pub const fn snapshot(&self) -> bool {
260        self.snapshot
261    }
262
263    /// Return whether generated actor glue should export schema report endpoints.
264    #[must_use]
265    pub const fn schema(&self) -> bool {
266        self.schema
267    }
268}
269
270/// Validated generated SQL endpoint switches for one canister.
271#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
272pub(crate) struct GeneratedCanisterSqlConfig {
273    readonly: bool,
274    ddl: bool,
275    fixtures: bool,
276    introspection_policy: GeneratedSqlIntrospectionPolicy,
277    update_policy: Option<GeneratedSqlUpdatePolicy>,
278}
279
280impl GeneratedCanisterSqlConfig {
281    pub(crate) const fn new(
282        readonly: bool,
283        ddl: bool,
284        fixtures: bool,
285        introspection_policy: GeneratedSqlIntrospectionPolicy,
286        update_policy: Option<GeneratedSqlUpdatePolicy>,
287    ) -> Self {
288        Self {
289            readonly,
290            ddl,
291            fixtures,
292            introspection_policy,
293            update_policy,
294        }
295    }
296}
297
298/// Build target used to resolve target-sensitive generated canister settings.
299#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
300pub enum GeneratedBuildTarget {
301    /// Local ICP replica target.
302    Local,
303    /// IC network target.
304    Ic,
305    /// Unknown target, resolved with IC/fail-closed defaults.
306    #[default]
307    Unknown,
308}
309
310impl GeneratedBuildTarget {
311    /// Parse the build target value passed through the build-script environment.
312    #[must_use]
313    pub fn from_env_value(value: &str) -> Self {
314        match value {
315            "local" => Self::Local,
316            "ic" => Self::Ic,
317            _ => Self::Unknown,
318        }
319    }
320
321    /// Return the build-script environment value for this target.
322    #[must_use]
323    pub const fn env_value(self) -> &'static str {
324        match self {
325            Self::Local => "local",
326            Self::Ic => "ic",
327            Self::Unknown => "unknown",
328        }
329    }
330}
331
332/// Local/IC policy for generated read-only SQL operational introspection.
333#[derive(Clone, Copy, Debug, Eq, PartialEq)]
334pub struct GeneratedSqlIntrospectionPolicy {
335    local: bool,
336    ic: bool,
337}
338
339impl GeneratedSqlIntrospectionPolicy {
340    /// Build one SQL introspection policy from explicit target booleans.
341    #[must_use]
342    pub const fn new(local: bool, ic: bool) -> Self {
343        Self { local, ic }
344    }
345
346    /// Return whether local builds should admit SQL introspection.
347    #[must_use]
348    pub const fn local(self) -> bool {
349        self.local
350    }
351
352    /// Return whether IC builds should admit SQL introspection.
353    #[must_use]
354    pub const fn ic(self) -> bool {
355        self.ic
356    }
357
358    /// Return whether this policy admits SQL introspection for one build target.
359    #[must_use]
360    pub const fn enabled_for(self, build_target: GeneratedBuildTarget) -> bool {
361        match build_target {
362            GeneratedBuildTarget::Local => self.local,
363            GeneratedBuildTarget::Ic => self.ic,
364            GeneratedBuildTarget::Unknown => false,
365        }
366    }
367}
368
369impl Default for GeneratedSqlIntrospectionPolicy {
370    fn default() -> Self {
371        Self::new(
372            DEFAULT_SQL_INTROSPECTION_LOCAL_ENABLED,
373            DEFAULT_SQL_INTROSPECTION_IC_ENABLED,
374        )
375    }
376}
377
378/// Generated SQL update endpoint policy selected by `icydb.toml`.
379#[derive(Clone, Copy, Debug, Eq, PartialEq)]
380pub enum GeneratedSqlUpdatePolicy {
381    /// Expose only public-safe primary-key `UPDATE` through `__icydb_update`.
382    PublicPrimaryKeyOnly,
383    /// Expose only public-safe bounded deterministic `UPDATE` through `__icydb_update`.
384    PublicBoundedDeterministic,
385}
386
387/// Validated generated metrics endpoint switches for one canister.
388#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
389pub(crate) struct GeneratedCanisterMetricsConfig {
390    enabled: bool,
391    extended: bool,
392}
393
394impl GeneratedCanisterMetricsConfig {
395    pub(crate) const fn new(enabled: bool, extended: bool) -> Self {
396        Self { enabled, extended }
397    }
398}