Skip to main content

vivacity_core/
phparray.rs

1//! PHP array operations on JSON values, as the merge plugin and Composer
2//! apply them to manifest sections: a JSON object is a string-keyed PHP
3//! array, a JSON array an integer-keyed one.
4
5use serde_json::{Map, Value};
6
7/// `array_merge($a, $b)`: string keys of `b` overwrite `a`'s (in `a`'s
8/// position, new ones appended); integer-keyed entries are appended and
9/// renumbered. Two lists concatenate; a list and an object cannot meet in
10/// the schema, the object wins then.
11pub fn array_merge(a: &Value, b: &Value) -> Value {
12    match (empty_as_object(a), empty_as_object(b)) {
13        (Value::Object(oa), Value::Object(ob)) => {
14            let mut out = oa.clone();
15            for (k, v) in ob {
16                out.insert(k.clone(), v.clone());
17            }
18            Value::Object(out)
19        }
20        (Value::Array(la), Value::Array(lb)) => {
21            let mut out = la.clone();
22            out.extend(lb.iter().cloned());
23            Value::Array(out)
24        }
25        (Value::Object(_), _) => a.clone(),
26        (_, other) => other.clone(),
27    }
28}
29
30/// `array_merge_recursive($a, $b)`: a string key present in both merges
31/// recursively when both values are arrays, otherwise the two values
32/// become a list (`[a, b]`; a list on one side takes the other side's
33/// value appended, in order); integer keys append.
34pub fn array_merge_recursive(a: &Value, b: &Value) -> Value {
35    match (empty_as_object(a), empty_as_object(b)) {
36        (Value::Object(oa), Value::Object(ob)) => {
37            let mut out: Map<String, Value> = oa.clone();
38            for (k, vb) in ob {
39                match out.get(k).cloned() {
40                    None => {
41                        out.insert(k.clone(), vb.clone());
42                    }
43                    Some(va) => {
44                        let merged = match (&va, vb) {
45                            (Value::Object(_), Value::Object(_))
46                            | (Value::Array(_), Value::Array(_)) => array_merge_recursive(&va, vb),
47                            (Value::Array(la), scalar) => {
48                                let mut l = la.clone();
49                                l.push(scalar.clone());
50                                Value::Array(l)
51                            }
52                            (scalar, Value::Array(lb)) => {
53                                let mut l = vec![scalar.clone()];
54                                l.extend(lb.iter().cloned());
55                                Value::Array(l)
56                            }
57                            (sa, sb) => Value::Array(vec![sa.clone(), sb.clone()]),
58                        };
59                        out.insert(k.clone(), merged);
60                    }
61                }
62            }
63            Value::Object(out)
64        }
65        (Value::Array(la), Value::Array(lb)) => {
66            let mut out = la.clone();
67            out.extend(lb.iter().cloned());
68            Value::Array(out)
69        }
70        (Value::Array(la), other) => {
71            let mut out = la.clone();
72            out.push(other.clone());
73            Value::Array(out)
74        }
75        (other, Value::Array(lb)) => {
76            let mut out = vec![other.clone()];
77            out.extend(lb.iter().cloned());
78            Value::Array(out)
79        }
80        (x, y) => Value::Array(vec![x.clone(), y.clone()]),
81    }
82}
83
84/// `NestedArray::mergeDeepArray([$a, $b])` of the merge plugin: integer
85/// keys append, a string key whose values are both arrays merges
86/// recursively, otherwise the later value wins.
87pub fn merge_deep(a: &Value, b: &Value) -> Value {
88    match (empty_as_object(a), empty_as_object(b)) {
89        (Value::Object(oa), Value::Object(ob)) => {
90            let mut out = oa.clone();
91            for (k, vb) in ob {
92                let merged = match out.get(k) {
93                    Some(va)
94                        if matches!(
95                            (va, vb),
96                            (Value::Object(_), Value::Object(_))
97                                | (Value::Array(_), Value::Array(_))
98                                | (Value::Object(_), Value::Array(_))
99                                | (Value::Array(_), Value::Object(_))
100                        ) =>
101                    {
102                        merge_deep(va, vb)
103                    }
104                    _ => vb.clone(),
105                };
106                out.insert(k.clone(), merged);
107            }
108            Value::Object(out)
109        }
110        (Value::Array(la), Value::Array(lb)) => {
111            let mut out = la.clone();
112            out.extend(lb.iter().cloned());
113            Value::Array(out)
114        }
115        // A PHP array is one thing: a list met by a map appends the list's
116        // items (integer keys) into the map's entries — the schema never
117        // does this; the second value wins.
118        (_, other) => other.clone(),
119    }
120}
121
122/// A JSON `[]` is PHP's empty array, the same thing as `{}`: read as an
123/// empty object so that it merges as a map (`"autoload": []` in a root).
124fn empty_as_object(v: &Value) -> &Value {
125    static EMPTY: std::sync::OnceLock<Value> = std::sync::OnceLock::new();
126    match v {
127        Value::Array(a) if a.is_empty() => EMPTY.get_or_init(|| Value::Object(Map::new())),
128        other => other,
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use serde_json::json;
136
137    #[test]
138    fn empty_list_is_an_empty_array() {
139        assert_eq!(
140            array_merge_recursive(&json!([]), &json!({"a": 1})),
141            json!({"a": 1})
142        );
143        assert_eq!(array_merge(&json!({"a": 1}), &json!([])), json!({"a": 1}));
144        assert_eq!(
145            merge_deep(&json!([]), &json!({"a": [1]})),
146            json!({"a": [1]})
147        );
148    }
149
150    #[test]
151    fn merge_recursive_like_php() {
152        // psr-4: same key twice → list; three times → the list grows.
153        let root = json!({"psr-4": {"App\\": "src/"}, "files": ["a.php"]});
154        let inc = json!({"psr-4": {"App\\": "modules/x/src/", "Mod\\": "modules/x/lib/"}, "files": ["modules/x/b.php"], "exclude-from-classmap": ["modules/x/tests/"]});
155        let m = array_merge_recursive(&root, &inc);
156        assert_eq!(
157            m,
158            json!({"psr-4": {"App\\": ["src/", "modules/x/src/"], "Mod\\": "modules/x/lib/"}, "files": ["a.php", "modules/x/b.php"], "exclude-from-classmap": ["modules/x/tests/"]})
159        );
160        let inc2 = json!({"psr-4": {"App\\": ["y/", "z/"]}});
161        let m2 = array_merge_recursive(&m, &inc2);
162        assert_eq!(
163            m2["psr-4"]["App\\"],
164            json!(["src/", "modules/x/src/", "y/", "z/"])
165        );
166        // scalar met by a list: scalar first.
167        assert_eq!(
168            array_merge_recursive(&json!({"k": "a"}), &json!({"k": ["b", "c"]})),
169            json!({"k": ["a", "b", "c"]})
170        );
171    }
172
173    #[test]
174    fn merge_and_deep_like_php() {
175        assert_eq!(
176            array_merge(
177                &json!({"a": 1, "b": {"x": 1}}),
178                &json!({"b": {"y": 2}, "c": 3})
179            ),
180            json!({"a": 1, "b": {"y": 2}, "c": 3})
181        );
182        assert_eq!(
183            merge_deep(
184                &json!({"a": 1, "b": {"x": 1, "l": [1]}}),
185                &json!({"b": {"y": 2, "l": [2]}, "a": 9})
186            ),
187            json!({"a": 9, "b": {"x": 1, "l": [1, 2], "y": 2}})
188        );
189    }
190}