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