cqlite_core/config_json.rs
1//! JSON deserialization entry points for [`Config`] (issue #1696).
2//!
3//! Split out of `config.rs` under the campsite rule (epic #1116): that file is
4//! already over the size target, and these two constructors are one cohesive
5//! responsibility — turning a JSON document into a `Config` while REPORTING the
6//! keys CQLite has removed, which is the only signal a non-Rust authoring surface
7//! can get (see [`crate::config_removed_keys`]).
8
9use super::Config;
10
11impl Config {
12 /// Deserialize a JSON `Config` document, reporting every key #1696 REMOVED
13 /// that it still names.
14 ///
15 /// # Why this exists (#1696 roborev F1)
16 ///
17 /// Deleting a decorative field from this struct is a compile error for an
18 /// embedder writing Rust, which is the loudest signal available — but serde
19 /// DISCARDS unknown fields, so a JSON or dict authoring surface (the Python
20 /// bindings' `cqlite.open(path, config=...)` bridge) silently accepted a
21 /// pre-change document naming `performance`, `storage.block_size`,
22 /// `query.parallel` and the rest, and ignored it. The rule #1696 states —
23 /// *a removed knob must produce a LOUD signal at the layer where it is set* —
24 /// was therefore false at exactly the layer that cannot get a compile error.
25 ///
26 /// The posture matches the CLI's file surface, crate-wide and deliberately:
27 /// **parse-and-ignore PLUS a named warning**, never `deny_unknown_fields`,
28 /// which would hard-fail a caller whose config predates the removal with no
29 /// migration path.
30 ///
31 /// The warning is logged at WARN via `tracing`. A caller that must SURFACE it
32 /// (the bindings raise a Python `UserWarning`) or assert it wants
33 /// [`Self::from_json_str_reporting_removed`].
34 ///
35 /// # This constructor is OPTIONAL, so it does not enforce the rule
36 ///
37 /// `Config` derives `Deserialize`, so an embedder can call
38 /// `serde_json::from_str::<Config>` directly and bypass this entirely — serde
39 /// then DISCARDS the removed keys in silence. Enforcement at the serde
40 /// boundary itself is **issue #3520** (#1696 roborev r2 F3, scoped out
41 /// deliberately); do not read this constructor as universal coverage.
42 ///
43 /// # Errors
44 ///
45 /// The document is not valid JSON, or does not deserialize into a `Config`.
46 /// Note that `Config` is not `#[serde(default)]`, so the document must be
47 /// COMPLETE. This does NOT run [`Self::validate`] — the caller owns validating
48 /// the config it finally uses, possibly after folding in overrides.
49 pub fn from_json_str(json: &str) -> crate::Result<Self> {
50 let (config, warning) = Self::from_json_str_reporting_removed(json, "this configuration")?;
51 if let Some(warning) = warning {
52 tracing::warn!("{warning}");
53 }
54 Ok(config)
55 }
56
57 /// As [`Self::from_json_str`], but RETURNS the removed-key warning instead of
58 /// logging it, labelled with `source` (e.g. `"config dict"`).
59 ///
60 /// # ORDER
61 ///
62 /// The deserialize runs FIRST and the scan only on success, so this
63 /// constructor never returns a removed-key report for a document that did
64 /// not become a `Config`. Nothing is lost: serde drops the removed keys from
65 /// `Config`, but nothing drops them from the text they were read out of.
66 ///
67 /// This ordering is a property of the RETURN SHAPE, not a precondition of the
68 /// text: since #1696 roborev r5 F1 the warning asserts nothing about whether
69 /// the load succeeds, precisely so that no placement of it can be wrong (see
70 /// [`crate::config_removed_keys::deprecation_warning`]).
71 ///
72 /// # Errors
73 ///
74 /// See [`Self::from_json_str`].
75 pub fn from_json_str_reporting_removed(
76 json: &str,
77 source: &str,
78 ) -> crate::Result<(Self, Option<String>)> {
79 let config: Self = serde_json::from_str(json)
80 .map_err(|e| crate::Error::configuration(format!("invalid {source}: {e}")))?;
81 let warning = crate::config_removed_keys::warning_for_json(source, json);
82 Ok((config, warning))
83 }
84}