Skip to main content

jsonschema_schema/schema/
add.rs

1use alloc::collections::BTreeMap;
2use core::ops::Add;
3
4use indexmap::IndexMap;
5
6use crate::extensions::IntellijSchemaExt;
7use crate::extensions::TombiSchemaExt;
8
9use super::Schema;
10
11/// Merge two `IndexMap` values with left-bias: entries from `source`
12/// are added only if the key does not already exist in `target`.
13fn merge_index_map<V>(
14    mut target: IndexMap<String, V>,
15    source: IndexMap<String, V>,
16) -> IndexMap<String, V> {
17    for (k, v) in source {
18        target.entry(k).or_insert(v);
19    }
20    target
21}
22
23/// Merge two `Option<BTreeMap>` values with left-bias: entries from `source`
24/// are added only if the key does not already exist in `target`.
25fn merge_option_btree_map<V>(
26    target: Option<BTreeMap<String, V>>,
27    source: Option<BTreeMap<String, V>>,
28) -> Option<BTreeMap<String, V>> {
29    match (target, source) {
30        (Some(mut t), Some(s)) => {
31            for (k, v) in s {
32                t.entry(k).or_insert(v);
33            }
34            Some(t)
35        }
36        (t, s) => t.or(s),
37    }
38}
39
40/// Merge two `Option<Vec<String>>` values by taking the union (deduplicated).
41fn union_option_vec(
42    target: Option<Vec<String>>,
43    source: Option<Vec<String>>,
44) -> Option<Vec<String>> {
45    match (target, source) {
46        (Some(mut t), Some(s)) => {
47            for item in s {
48                if !t.contains(&item) {
49                    t.push(item);
50                }
51            }
52            Some(t)
53        }
54        (t, s) => t.or(s),
55    }
56}
57
58impl Add for Schema {
59    type Output = Self;
60
61    /// Merge two schemas with left-bias.
62    ///
63    /// - **Map fields** (`properties`, `pattern_properties`, `defs`, `dependent_schemas`):
64    ///   merge — rhs entries added only if key doesn't exist in self.
65    /// - **`required`**: union (deduplicate).
66    /// - **`extra`** (`BTreeMap` catch-all): merge — rhs entries added only if key doesn't exist.
67    /// - **All other `Option<T>` fields**: `self.field.or(rhs.field)` — left wins.
68    /// - **`bool` fields**: `self.field || rhs.field` — true wins.
69    #[allow(clippy::too_many_lines)]
70    fn add(self, rhs: Self) -> Self {
71        let extra = {
72            let mut merged = self.extra;
73            for (k, v) in rhs.extra {
74                merged.entry(k).or_insert(v);
75            }
76            merged
77        };
78
79        let x_tombi = TombiSchemaExt {
80            toml_version: self.x_tombi.toml_version.or(rhs.x_tombi.toml_version),
81            table_keys_order: self
82                .x_tombi
83                .table_keys_order
84                .or(rhs.x_tombi.table_keys_order),
85            additional_key_label: self
86                .x_tombi
87                .additional_key_label
88                .or(rhs.x_tombi.additional_key_label),
89            array_values_order: self
90                .x_tombi
91                .array_values_order
92                .or(rhs.x_tombi.array_values_order),
93        };
94
95        Self {
96            // Core
97            schema: self.schema.or(rhs.schema),
98            id: self.id.or(rhs.id),
99            ref_: self.ref_.or(rhs.ref_),
100            anchor: self.anchor.or(rhs.anchor),
101            dynamic_ref: self.dynamic_ref.or(rhs.dynamic_ref),
102            dynamic_anchor: self.dynamic_anchor.or(rhs.dynamic_anchor),
103            comment: self.comment.or(rhs.comment),
104            defs: merge_option_btree_map(self.defs, rhs.defs),
105            vocabulary: self.vocabulary.or(rhs.vocabulary),
106
107            // Metadata
108            title: self.title.or(rhs.title),
109            description: self.description.or(rhs.description),
110            default: self.default.or(rhs.default),
111            deprecated: self.deprecated || rhs.deprecated,
112            read_only: self.read_only || rhs.read_only,
113            write_only: self.write_only || rhs.write_only,
114            examples: self.examples.or(rhs.examples),
115
116            // Type
117            type_: self.type_.or(rhs.type_),
118            enum_: self.enum_.or(rhs.enum_),
119            markdown_enum_descriptions: self
120                .markdown_enum_descriptions
121                .or(rhs.markdown_enum_descriptions),
122            const_: self.const_.or(rhs.const_),
123
124            // Object — map fields are merged
125            properties: merge_index_map(self.properties, rhs.properties),
126            pattern_properties: merge_index_map(self.pattern_properties, rhs.pattern_properties),
127            additional_properties: self.additional_properties.or(rhs.additional_properties),
128            required: union_option_vec(self.required, rhs.required),
129            property_names: self.property_names.or(rhs.property_names),
130            min_properties: self.min_properties.or(rhs.min_properties),
131            max_properties: self.max_properties.or(rhs.max_properties),
132            unevaluated_properties: self.unevaluated_properties.or(rhs.unevaluated_properties),
133
134            // Array
135            items: self.items.or(rhs.items),
136            prefix_items: self.prefix_items.or(rhs.prefix_items),
137            contains: self.contains.or(rhs.contains),
138            min_contains: self.min_contains.or(rhs.min_contains),
139            max_contains: self.max_contains.or(rhs.max_contains),
140            min_items: self.min_items.or(rhs.min_items),
141            max_items: self.max_items.or(rhs.max_items),
142            unique_items: self.unique_items || rhs.unique_items,
143            unevaluated_items: self.unevaluated_items.or(rhs.unevaluated_items),
144
145            // Number
146            minimum: self.minimum.or(rhs.minimum),
147            maximum: self.maximum.or(rhs.maximum),
148            exclusive_minimum: self.exclusive_minimum.or(rhs.exclusive_minimum),
149            exclusive_maximum: self.exclusive_maximum.or(rhs.exclusive_maximum),
150            multiple_of: self.multiple_of.or(rhs.multiple_of),
151
152            // String
153            min_length: self.min_length.or(rhs.min_length),
154            max_length: self.max_length.or(rhs.max_length),
155            pattern: self.pattern.or(rhs.pattern),
156            format: self.format.or(rhs.format),
157
158            // Composition — NOT merged
159            all_of: self.all_of.or(rhs.all_of),
160            any_of: self.any_of.or(rhs.any_of),
161            one_of: self.one_of.or(rhs.one_of),
162            not: self.not.or(rhs.not),
163
164            // Conditional
165            if_: self.if_.or(rhs.if_),
166            then_: self.then_.or(rhs.then_),
167            else_: self.else_.or(rhs.else_),
168
169            // Dependencies
170            dependent_required: self.dependent_required.or(rhs.dependent_required),
171            dependent_schemas: merge_index_map(self.dependent_schemas, rhs.dependent_schemas),
172
173            // Content
174            content_media_type: self.content_media_type.or(rhs.content_media_type),
175            content_encoding: self.content_encoding.or(rhs.content_encoding),
176            content_schema: self.content_schema.or(rhs.content_schema),
177
178            // Extensions
179            markdown_description: self.markdown_description.or(rhs.markdown_description),
180            x_lintel: self.x_lintel.or(rhs.x_lintel),
181            x_taplo: self.x_taplo.or(rhs.x_taplo),
182            x_taplo_info: self.x_taplo_info.or(rhs.x_taplo_info),
183            x_tombi,
184            x_intellij: IntellijSchemaExt {
185                html_description: self
186                    .x_intellij
187                    .html_description
188                    .or(rhs.x_intellij.html_description),
189                language_injection: self
190                    .x_intellij
191                    .language_injection
192                    .or(rhs.x_intellij.language_injection),
193                enum_metadata: self
194                    .x_intellij
195                    .enum_metadata
196                    .or(rhs.x_intellij.enum_metadata),
197            },
198
199            extra,
200        }
201    }
202}