Skip to main content

qubit_config/options/
config_read_options.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10
11use qubit_datatype::{
12    BlankStringPolicy, BooleanConversionOptions, CollectionConversionOptions,
13    DataConversionOptions, DurationConversionOptions, DurationUnit, EmptyItemPolicy,
14    StringConversionOptions,
15};
16use serde::de::Error as DeError;
17use serde::{Deserialize, Deserializer, Serialize, Serializer};
18
19/// Runtime options that control how configuration values are read and parsed.
20///
21#[derive(Debug, Clone, PartialEq, Eq, Default)]
22pub struct ConfigReadOptions {
23    /// Common scalar, collection, boolean, and duration conversion options.
24    conversion: DataConversionOptions,
25    /// Whether unresolved `${...}` placeholders may be read from process
26    /// environment variables.
27    env_variable_substitution_enabled: bool,
28}
29
30impl Serialize for ConfigReadOptions {
31    /// Serializes all runtime read options.
32    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
33    where
34        S: Serializer,
35    {
36        ConfigReadOptionsSerde::from(self).serialize(serializer)
37    }
38}
39
40impl<'de> Deserialize<'de> for ConfigReadOptions {
41    /// Deserializes runtime read options.
42    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
43    where
44        D: Deserializer<'de>,
45    {
46        ConfigReadOptionsSerde::deserialize(deserializer)?
47            .try_into()
48            .map_err(D::Error::custom)
49    }
50}
51
52impl ConfigReadOptions {
53    /// Creates options suitable for environment-variable style values.
54    ///
55    /// # Returns
56    ///
57    /// Options that trim strings, treat blank scalar strings as missing, accept
58    /// common boolean aliases, and split scalar strings on commas while
59    /// skipping empty collection items. Environment-variable substitution is
60    /// still disabled; enable it explicitly with
61    /// [`Self::with_env_variable_substitution_enabled`].
62    #[must_use]
63    pub fn env_friendly() -> Self {
64        Self {
65            conversion: DataConversionOptions::env_friendly(),
66            env_variable_substitution_enabled: false,
67        }
68    }
69
70    /// Gets the underlying data conversion options.
71    ///
72    /// # Returns
73    ///
74    /// Options used by the shared `qubit-datatype` conversion layer.
75    #[inline]
76    pub fn conversion_options(&self) -> &DataConversionOptions {
77        &self.conversion
78    }
79
80    /// Returns whether `${...}` substitution may read process environment
81    /// variables when a value is missing from config.
82    ///
83    /// # Returns
84    ///
85    /// `true` when environment fallback is enabled.
86    #[inline]
87    pub fn is_env_variable_substitution_enabled(&self) -> bool {
88        self.env_variable_substitution_enabled
89    }
90
91    /// Returns a copy with environment-variable substitution enabled or
92    /// disabled.
93    ///
94    /// # Parameters
95    ///
96    /// * `enabled` - Whether unresolved config placeholders may fall back to
97    ///   process environment variables.
98    ///
99    /// # Returns
100    ///
101    /// Updated options.
102    #[must_use]
103    pub fn with_env_variable_substitution_enabled(mut self, enabled: bool) -> Self {
104        self.env_variable_substitution_enabled = enabled;
105        self
106    }
107
108    /// Returns a copy with a different blank string policy.
109    ///
110    /// # Parameters
111    ///
112    /// * `policy` - New blank string policy.
113    ///
114    /// # Returns
115    ///
116    /// Updated options.
117    #[must_use]
118    pub fn with_blank_string_policy(mut self, policy: BlankStringPolicy) -> Self {
119        self.conversion = self.conversion.with_blank_string_policy(policy);
120        self
121    }
122
123    /// Returns a copy with a different empty collection item policy.
124    ///
125    /// # Parameters
126    ///
127    /// * `policy` - New empty item policy.
128    ///
129    /// # Returns
130    ///
131    /// Updated options.
132    #[must_use]
133    pub fn with_empty_item_policy(mut self, policy: EmptyItemPolicy) -> Self {
134        self.conversion = self.conversion.with_empty_item_policy(policy);
135        self
136    }
137
138    /// Returns a copy with different string conversion options.
139    ///
140    /// # Parameters
141    ///
142    /// * `string` - New string conversion options.
143    ///
144    /// # Returns
145    ///
146    /// Updated options.
147    #[must_use]
148    pub fn with_string_options(mut self, string: StringConversionOptions) -> Self {
149        self.conversion = self.conversion.with_string_options(string);
150        self
151    }
152
153    /// Returns a copy with different boolean conversion options.
154    ///
155    /// # Parameters
156    ///
157    /// * `boolean` - New boolean conversion options.
158    ///
159    /// # Returns
160    ///
161    /// Updated options.
162    #[must_use]
163    pub fn with_boolean_options(mut self, boolean: BooleanConversionOptions) -> Self {
164        self.conversion = self.conversion.with_boolean_options(boolean);
165        self
166    }
167
168    /// Returns a copy with different collection conversion options.
169    ///
170    /// # Parameters
171    ///
172    /// * `collection` - New collection conversion options.
173    ///
174    /// # Returns
175    ///
176    /// Updated options.
177    #[must_use]
178    pub fn with_collection_options(mut self, collection: CollectionConversionOptions) -> Self {
179        self.conversion = self.conversion.with_collection_options(collection);
180        self
181    }
182
183    /// Returns a copy with different duration conversion options.
184    ///
185    /// # Parameters
186    ///
187    /// * `duration` - New duration conversion options.
188    ///
189    /// # Returns
190    ///
191    /// Updated options.
192    #[must_use]
193    pub fn with_duration_options(mut self, duration: DurationConversionOptions) -> Self {
194        self.conversion = self.conversion.with_duration_options(duration);
195        self
196    }
197}
198
199impl AsRef<DataConversionOptions> for ConfigReadOptions {
200    /// Borrows the underlying data conversion options.
201    #[inline]
202    fn as_ref(&self) -> &DataConversionOptions {
203        &self.conversion
204    }
205}
206
207impl From<DataConversionOptions> for ConfigReadOptions {
208    /// Creates config read options from data conversion options.
209    ///
210    /// Environment-variable fallback for `${...}` substitution remains disabled.
211    #[inline]
212    fn from(conversion: DataConversionOptions) -> Self {
213        Self {
214            conversion,
215            env_variable_substitution_enabled: false,
216        }
217    }
218}
219
220/// Serde representation of [`ConfigReadOptions`].
221#[derive(Debug, Clone, Serialize, Deserialize)]
222struct ConfigReadOptionsSerde {
223    /// Common scalar, collection, boolean, and duration conversion options.
224    #[serde(default)]
225    conversion: DataConversionOptionsSerde,
226    /// Whether unresolved `${...}` placeholders may fall back to environment variables.
227    #[serde(default)]
228    env_variable_substitution_enabled: bool,
229}
230
231impl From<&ConfigReadOptions> for ConfigReadOptionsSerde {
232    /// Converts read options to their serde representation.
233    fn from(options: &ConfigReadOptions) -> Self {
234        Self {
235            conversion: DataConversionOptionsSerde::from(&options.conversion),
236            env_variable_substitution_enabled: options.env_variable_substitution_enabled,
237        }
238    }
239}
240
241impl TryFrom<ConfigReadOptionsSerde> for ConfigReadOptions {
242    type Error = String;
243
244    /// Converts the serde representation back to read options.
245    fn try_from(value: ConfigReadOptionsSerde) -> Result<Self, Self::Error> {
246        Ok(Self {
247            conversion: value.conversion.try_into()?,
248            env_variable_substitution_enabled: value.env_variable_substitution_enabled,
249        })
250    }
251}
252
253/// Serde representation of [`DataConversionOptions`].
254#[derive(Debug, Clone, Serialize, Deserialize)]
255struct DataConversionOptionsSerde {
256    /// String source conversion behavior.
257    #[serde(default)]
258    string: StringConversionOptionsSerde,
259    /// Boolean string literal conversion behavior.
260    #[serde(default)]
261    boolean: BooleanConversionOptionsSerde,
262    /// Scalar string collection conversion behavior.
263    #[serde(default)]
264    collection: CollectionConversionOptionsSerde,
265    /// Duration conversion behavior.
266    #[serde(default)]
267    duration: DurationConversionOptionsSerde,
268}
269
270impl Default for DataConversionOptionsSerde {
271    /// Creates the serde representation of default conversion options.
272    fn default() -> Self {
273        Self::from(&DataConversionOptions::default())
274    }
275}
276
277impl From<&DataConversionOptions> for DataConversionOptionsSerde {
278    /// Converts conversion options to their serde representation.
279    fn from(options: &DataConversionOptions) -> Self {
280        Self {
281            string: StringConversionOptionsSerde::from(&options.string),
282            boolean: BooleanConversionOptionsSerde::from(&options.boolean),
283            collection: CollectionConversionOptionsSerde::from(&options.collection),
284            duration: DurationConversionOptionsSerde::from(&options.duration),
285        }
286    }
287}
288
289impl TryFrom<DataConversionOptionsSerde> for DataConversionOptions {
290    type Error = String;
291
292    /// Converts the serde representation back to conversion options.
293    fn try_from(value: DataConversionOptionsSerde) -> Result<Self, Self::Error> {
294        Ok(Self {
295            string: value.string.into(),
296            boolean: value.boolean.try_into()?,
297            collection: value.collection.into(),
298            duration: value.duration.into(),
299        })
300    }
301}
302
303/// Serde representation of [`StringConversionOptions`].
304#[derive(Debug, Clone, Serialize, Deserialize)]
305struct StringConversionOptionsSerde {
306    /// Whether strings are trimmed before conversion.
307    #[serde(default)]
308    trim: bool,
309    /// How blank strings are interpreted after optional trimming.
310    #[serde(default)]
311    blank_string_policy: BlankStringPolicySerde,
312}
313
314impl Default for StringConversionOptionsSerde {
315    /// Creates the serde representation of default string conversion options.
316    fn default() -> Self {
317        Self::from(&StringConversionOptions::default())
318    }
319}
320
321impl From<&StringConversionOptions> for StringConversionOptionsSerde {
322    /// Converts string conversion options to their serde representation.
323    fn from(options: &StringConversionOptions) -> Self {
324        Self {
325            trim: options.trim,
326            blank_string_policy: options.blank_string_policy.into(),
327        }
328    }
329}
330
331impl From<StringConversionOptionsSerde> for StringConversionOptions {
332    /// Converts the serde representation back to string conversion options.
333    fn from(value: StringConversionOptionsSerde) -> Self {
334        Self {
335            trim: value.trim,
336            blank_string_policy: value.blank_string_policy.into(),
337        }
338    }
339}
340
341/// Serde representation of [`BooleanConversionOptions`].
342#[derive(Debug, Clone, Serialize, Deserialize)]
343struct BooleanConversionOptionsSerde {
344    /// String literals accepted as `true`.
345    #[serde(default = "default_true_literals")]
346    true_literals: Vec<String>,
347    /// String literals accepted as `false`.
348    #[serde(default = "default_false_literals")]
349    false_literals: Vec<String>,
350    /// Whether literal matching is case-sensitive.
351    #[serde(default)]
352    case_sensitive: bool,
353}
354
355impl Default for BooleanConversionOptionsSerde {
356    /// Creates the serde representation of default boolean conversion options.
357    fn default() -> Self {
358        Self::from(&BooleanConversionOptions::default())
359    }
360}
361
362impl From<&BooleanConversionOptions> for BooleanConversionOptionsSerde {
363    /// Converts boolean conversion options to their serde representation.
364    fn from(options: &BooleanConversionOptions) -> Self {
365        Self {
366            true_literals: options.true_literals().to_vec(),
367            false_literals: options.false_literals().to_vec(),
368            case_sensitive: options.case_sensitive,
369        }
370    }
371}
372
373impl TryFrom<BooleanConversionOptionsSerde> for BooleanConversionOptions {
374    type Error = String;
375
376    /// Converts the serde representation back to boolean conversion options.
377    fn try_from(value: BooleanConversionOptionsSerde) -> Result<Self, Self::Error> {
378        let mut options = BooleanConversionOptions::strict();
379        let strict = BooleanConversionOptions::strict();
380        ensure_literal_prefix(
381            &value.true_literals,
382            strict.true_literals(),
383            "true_literals",
384        )?;
385        ensure_literal_prefix(
386            &value.false_literals,
387            strict.false_literals(),
388            "false_literals",
389        )?;
390        for literal in value
391            .true_literals
392            .iter()
393            .skip(strict.true_literals().len())
394        {
395            options = options.with_true_literal(literal);
396        }
397        for literal in value
398            .false_literals
399            .iter()
400            .skip(strict.false_literals().len())
401        {
402            options = options.with_false_literal(literal);
403        }
404        Ok(options.with_case_sensitive(value.case_sensitive))
405    }
406}
407
408/// Serde representation of [`CollectionConversionOptions`].
409#[derive(Debug, Clone, Serialize, Deserialize)]
410struct CollectionConversionOptionsSerde {
411    /// Whether a scalar string can be split into collection items.
412    #[serde(default)]
413    split_scalar_strings: bool,
414    /// Delimiters used to split scalar strings.
415    #[serde(default = "default_delimiters")]
416    delimiters: Vec<char>,
417    /// Whether split items are trimmed before element conversion.
418    #[serde(default)]
419    trim_items: bool,
420    /// How empty split items are interpreted.
421    #[serde(default)]
422    empty_item_policy: EmptyItemPolicySerde,
423}
424
425impl Default for CollectionConversionOptionsSerde {
426    /// Creates the serde representation of default collection conversion options.
427    fn default() -> Self {
428        Self::from(&CollectionConversionOptions::default())
429    }
430}
431
432impl From<&CollectionConversionOptions> for CollectionConversionOptionsSerde {
433    /// Converts collection conversion options to their serde representation.
434    fn from(options: &CollectionConversionOptions) -> Self {
435        Self {
436            split_scalar_strings: options.split_scalar_strings,
437            delimiters: options.delimiters.clone(),
438            trim_items: options.trim_items,
439            empty_item_policy: options.empty_item_policy.into(),
440        }
441    }
442}
443
444impl From<CollectionConversionOptionsSerde> for CollectionConversionOptions {
445    /// Converts the serde representation back to collection conversion options.
446    fn from(value: CollectionConversionOptionsSerde) -> Self {
447        Self {
448            split_scalar_strings: value.split_scalar_strings,
449            delimiters: value.delimiters,
450            trim_items: value.trim_items,
451            empty_item_policy: value.empty_item_policy.into(),
452        }
453    }
454}
455
456/// Serde representation of [`DurationConversionOptions`].
457#[derive(Debug, Clone, Serialize, Deserialize)]
458struct DurationConversionOptionsSerde {
459    /// Unit used for suffixless strings and integer conversions.
460    #[serde(default)]
461    unit: DurationUnitSerde,
462    /// Whether formatted duration strings include the unit suffix.
463    #[serde(default = "default_append_unit_suffix")]
464    append_unit_suffix: bool,
465}
466
467impl Default for DurationConversionOptionsSerde {
468    /// Creates the serde representation of default duration conversion options.
469    fn default() -> Self {
470        Self::from(&DurationConversionOptions::default())
471    }
472}
473
474impl From<&DurationConversionOptions> for DurationConversionOptionsSerde {
475    /// Converts duration conversion options to their serde representation.
476    fn from(options: &DurationConversionOptions) -> Self {
477        Self {
478            unit: options.unit.into(),
479            append_unit_suffix: options.append_unit_suffix,
480        }
481    }
482}
483
484impl From<DurationConversionOptionsSerde> for DurationConversionOptions {
485    /// Converts the serde representation back to duration conversion options.
486    fn from(value: DurationConversionOptionsSerde) -> Self {
487        Self {
488            unit: value.unit.into(),
489            append_unit_suffix: value.append_unit_suffix,
490        }
491    }
492}
493
494/// Serde representation of [`BlankStringPolicy`].
495#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
496#[serde(rename_all = "snake_case")]
497enum BlankStringPolicySerde {
498    /// Keep blank strings as real string values.
499    Preserve,
500    /// Treat blank strings as missing values.
501    TreatAsMissing,
502    /// Reject blank strings as invalid input.
503    Reject,
504}
505
506impl Default for BlankStringPolicySerde {
507    /// Creates the default blank string policy representation.
508    fn default() -> Self {
509        BlankStringPolicy::Preserve.into()
510    }
511}
512
513impl From<BlankStringPolicy> for BlankStringPolicySerde {
514    /// Converts a blank string policy to its serde representation.
515    fn from(value: BlankStringPolicy) -> Self {
516        match value {
517            BlankStringPolicy::Preserve => Self::Preserve,
518            BlankStringPolicy::TreatAsMissing => Self::TreatAsMissing,
519            BlankStringPolicy::Reject => Self::Reject,
520        }
521    }
522}
523
524impl From<BlankStringPolicySerde> for BlankStringPolicy {
525    /// Converts the serde representation back to a blank string policy.
526    fn from(value: BlankStringPolicySerde) -> Self {
527        match value {
528            BlankStringPolicySerde::Preserve => Self::Preserve,
529            BlankStringPolicySerde::TreatAsMissing => Self::TreatAsMissing,
530            BlankStringPolicySerde::Reject => Self::Reject,
531        }
532    }
533}
534
535/// Serde representation of [`EmptyItemPolicy`].
536#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
537#[serde(rename_all = "snake_case")]
538enum EmptyItemPolicySerde {
539    /// Keep empty items and pass them to the element converter.
540    Keep,
541    /// Drop empty items before element conversion.
542    Skip,
543    /// Reject empty items as invalid input.
544    Reject,
545}
546
547impl Default for EmptyItemPolicySerde {
548    /// Creates the default empty item policy representation.
549    fn default() -> Self {
550        EmptyItemPolicy::Keep.into()
551    }
552}
553
554impl From<EmptyItemPolicy> for EmptyItemPolicySerde {
555    /// Converts an empty item policy to its serde representation.
556    fn from(value: EmptyItemPolicy) -> Self {
557        match value {
558            EmptyItemPolicy::Keep => Self::Keep,
559            EmptyItemPolicy::Skip => Self::Skip,
560            EmptyItemPolicy::Reject => Self::Reject,
561        }
562    }
563}
564
565impl From<EmptyItemPolicySerde> for EmptyItemPolicy {
566    /// Converts the serde representation back to an empty item policy.
567    fn from(value: EmptyItemPolicySerde) -> Self {
568        match value {
569            EmptyItemPolicySerde::Keep => Self::Keep,
570            EmptyItemPolicySerde::Skip => Self::Skip,
571            EmptyItemPolicySerde::Reject => Self::Reject,
572        }
573    }
574}
575
576/// Serde representation of [`DurationUnit`].
577#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
578#[serde(rename_all = "snake_case")]
579enum DurationUnitSerde {
580    /// Nanoseconds.
581    Nanoseconds,
582    /// Microseconds.
583    Microseconds,
584    /// Milliseconds.
585    Milliseconds,
586    /// Seconds.
587    Seconds,
588    /// Minutes.
589    Minutes,
590    /// Hours.
591    Hours,
592    /// Days.
593    Days,
594}
595
596impl Default for DurationUnitSerde {
597    /// Creates the default duration unit representation.
598    fn default() -> Self {
599        DurationUnit::default().into()
600    }
601}
602
603impl From<DurationUnit> for DurationUnitSerde {
604    /// Converts a duration unit to its serde representation.
605    fn from(value: DurationUnit) -> Self {
606        match value {
607            DurationUnit::Nanoseconds => Self::Nanoseconds,
608            DurationUnit::Microseconds => Self::Microseconds,
609            DurationUnit::Milliseconds => Self::Milliseconds,
610            DurationUnit::Seconds => Self::Seconds,
611            DurationUnit::Minutes => Self::Minutes,
612            DurationUnit::Hours => Self::Hours,
613            DurationUnit::Days => Self::Days,
614        }
615    }
616}
617
618impl From<DurationUnitSerde> for DurationUnit {
619    /// Converts the serde representation back to a duration unit.
620    fn from(value: DurationUnitSerde) -> Self {
621        match value {
622            DurationUnitSerde::Nanoseconds => Self::Nanoseconds,
623            DurationUnitSerde::Microseconds => Self::Microseconds,
624            DurationUnitSerde::Milliseconds => Self::Milliseconds,
625            DurationUnitSerde::Seconds => Self::Seconds,
626            DurationUnitSerde::Minutes => Self::Minutes,
627            DurationUnitSerde::Hours => Self::Hours,
628            DurationUnitSerde::Days => Self::Days,
629        }
630    }
631}
632
633/// Returns the default true literals.
634fn default_true_literals() -> Vec<String> {
635    BooleanConversionOptions::default().true_literals().to_vec()
636}
637
638/// Returns the default false literals.
639fn default_false_literals() -> Vec<String> {
640    BooleanConversionOptions::default()
641        .false_literals()
642        .to_vec()
643}
644
645/// Returns the default scalar string collection delimiters.
646fn default_delimiters() -> Vec<char> {
647    CollectionConversionOptions::default().delimiters
648}
649
650/// Returns whether formatted durations include unit suffixes by default.
651fn default_append_unit_suffix() -> bool {
652    DurationConversionOptions::default().append_unit_suffix
653}
654
655/// Ensures serialized boolean literals came from a supported public constructor.
656fn ensure_literal_prefix(
657    actual: &[String],
658    expected: &[String],
659    field: &str,
660) -> Result<(), String> {
661    if actual.len() < expected.len()
662        || !actual
663            .iter()
664            .zip(expected.iter())
665            .all(|(left, right)| left == right)
666    {
667        return Err(format!(
668            "{field} must start with the default literals: {:?}",
669            expected
670        ));
671    }
672    Ok(())
673}