1use std::collections::HashMap;
9
10use crate::resolved_value::{contains_dynamic_resolved, is_resolved_null};
11use crate::resolver::ResolvedValue;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum MemberPresence {
16 Always,
18 Never,
20 When(Vec<(String, bool)>),
23 Complex,
27}
28
29#[must_use]
31pub fn member_presence(member: &ResolvedValue) -> MemberPresence {
32 match member {
33 ResolvedValue::Conditional { condition, if_true, if_false } => {
34 conditional_presence(condition, member_presence(if_true), member_presence(if_false))
35 }
36 ResolvedValue::Enum { variants } => {
37 let null_variants = variants.iter().filter(|variant| is_resolved_null(variant)).count();
38 if null_variants == 0 {
39 MemberPresence::Always
40 } else if null_variants == variants.len() {
41 MemberPresence::Never
42 } else {
43 MemberPresence::Complex
44 }
45 }
46 other if is_resolved_null(other) => MemberPresence::Never,
47 _ => MemberPresence::Always,
48 }
49}
50
51fn conditional_presence(condition: &str, if_true: MemberPresence, if_false: MemberPresence) -> MemberPresence {
52 let conjoin_literal =
53 |mut literals: Vec<(String, bool)>, expected: bool| match literals.iter().find(|(name, _)| name == condition) {
54 Some((_, existing)) if *existing != expected => MemberPresence::Never,
55 Some(_) => MemberPresence::When(literals),
56 None => {
57 literals.insert(0, (condition.to_string(), expected));
58 MemberPresence::When(literals)
59 }
60 };
61 match (if_true, if_false) {
62 (MemberPresence::Always, MemberPresence::Always) => MemberPresence::Always,
63 (MemberPresence::Never, MemberPresence::Never) => MemberPresence::Never,
64 (MemberPresence::Always, MemberPresence::Never) => MemberPresence::When(vec![(condition.to_string(), true)]),
65 (MemberPresence::Never, MemberPresence::Always) => MemberPresence::When(vec![(condition.to_string(), false)]),
66 (MemberPresence::When(literals), MemberPresence::Never) => conjoin_literal(literals, true),
67 (MemberPresence::Never, MemberPresence::When(literals)) => conjoin_literal(literals, false),
68 _ => MemberPresence::Complex,
69 }
70}
71
72#[derive(Debug, Clone)]
75pub struct ContainerMember<'a> {
76 pub value: &'a ResolvedValue,
77 pub presence: MemberPresence,
78 pub is_dynamic: bool,
79}
80
81#[must_use]
83pub fn classify_members(members: &[ResolvedValue]) -> Vec<ContainerMember<'_>> {
84 members
85 .iter()
86 .map(|value| ContainerMember {
87 value,
88 presence: member_presence(value),
89 is_dynamic: contains_dynamic_resolved(value),
90 })
91 .collect()
92}
93
94pub(crate) fn absent_alternatives(
97 member: &ResolvedValue,
98 established_literals: &[(String, bool)],
99) -> Vec<Vec<(String, bool)>> {
100 let mut alternatives = Vec::new();
101 let mut literals = established_literals.to_vec();
102 collect_absent_alternatives(member, &mut literals, &mut alternatives);
103 alternatives
104}
105
106fn collect_absent_alternatives(
107 value: &ResolvedValue,
108 literals: &mut Vec<(String, bool)>,
109 alternatives: &mut Vec<Vec<(String, bool)>>,
110) {
111 match value {
112 ResolvedValue::Conditional { condition, if_true, if_false } => {
113 for (branch, holds) in [(if_true, true), (if_false, false)] {
114 match literals.iter().find(|(name, _)| name == condition) {
115 Some((_, decided)) if *decided == holds => {
116 collect_absent_alternatives(branch, literals, alternatives);
117 }
118 Some(_) => {}
119 None => {
120 literals.push((condition.clone(), holds));
121 collect_absent_alternatives(branch, literals, alternatives);
122 literals.pop();
123 }
124 }
125 }
126 }
127 ResolvedValue::Enum { variants } => {
128 for variant in variants {
129 collect_absent_alternatives(variant, literals, alternatives);
130 }
131 }
132 other if is_resolved_null(other) => alternatives.push(literals.clone()),
133 _ => {}
134 }
135}
136
137pub fn extend_assignment(assignment: &mut HashMap<String, bool>, literals: &[(String, bool)]) -> bool {
141 let mut additions = HashMap::with_capacity(literals.len());
142 for (condition, value) in literals {
143 if assignment.get(condition).is_some_and(|existing| existing != value)
144 || additions.insert(condition.as_str(), *value).is_some_and(|existing| existing != *value)
145 {
146 return false;
147 }
148 }
149 assignment.extend(additions.into_iter().map(|(condition, value)| (condition.to_string(), value)));
150 true
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 fn concrete(value: serde_json::Value) -> ResolvedValue {
158 ResolvedValue::Concrete { value: value.into() }
159 }
160
161 fn no_value() -> ResolvedValue {
162 concrete(serde_json::Value::Null)
163 }
164
165 fn conditional(condition: &str, if_true: ResolvedValue, if_false: ResolvedValue) -> ResolvedValue {
166 ResolvedValue::Conditional {
167 condition: condition.to_string(),
168 if_true: Box::new(if_true),
169 if_false: Box::new(if_false),
170 }
171 }
172
173 #[test]
174 fn optional_member_over_no_value_is_present_when_its_condition_holds() {
175 let member = conditional("HasTag", concrete(serde_json::json!({"Key": "k"})), no_value());
176 assert_eq!(member_presence(&member), MemberPresence::When(vec![("HasTag".to_string(), true)]));
177 }
178
179 #[test]
180 fn reversed_branches_negate_the_literal() {
181 let member = conditional("Skip", no_value(), concrete(serde_json::json!("x")));
182 assert_eq!(member_presence(&member), MemberPresence::When(vec![("Skip".to_string(), false)]));
183 }
184
185 #[test]
186 fn nested_optional_member_conjoins_literals_outermost_first() {
187 let inner = conditional("Inner", concrete(serde_json::json!("x")), no_value());
188 let member = conditional("Outer", inner, no_value());
189 assert_eq!(
190 member_presence(&member),
191 MemberPresence::When(vec![("Outer".to_string(), true), ("Inner".to_string(), true)])
192 );
193 }
194
195 #[test]
196 fn conditional_choosing_between_two_values_is_always_present() {
197 let member = conditional("Pick", concrete(serde_json::json!("a")), concrete(serde_json::json!("b")));
198 assert_eq!(member_presence(&member), MemberPresence::Always);
199 }
200
201 #[test]
202 fn conditional_whose_branches_are_both_partly_present_is_complex() {
203 let left = conditional("A", concrete(serde_json::json!("a")), no_value());
204 let right = conditional("B", concrete(serde_json::json!("b")), no_value());
205 assert_eq!(member_presence(&conditional("Pick", left, right)), MemberPresence::Complex);
206 }
207
208 #[test]
209 fn enum_mixing_null_and_values_is_complex_and_all_null_is_never() {
210 let mixed = ResolvedValue::Enum { variants: vec![no_value(), concrete(serde_json::json!("a"))] };
211 assert_eq!(member_presence(&mixed), MemberPresence::Complex);
212 let all_null = ResolvedValue::Enum { variants: vec![no_value(), no_value()] };
213 assert_eq!(member_presence(&all_null), MemberPresence::Never);
214 }
215
216 #[test]
217 fn absent_alternatives_lists_the_null_leaves() {
218 let member = conditional("HasTag", concrete(serde_json::json!({"Key": "k"})), no_value());
219 assert_eq!(absent_alternatives(&member, &[]), vec![vec![("HasTag".to_string(), false)]]);
220 assert!(absent_alternatives(&concrete(serde_json::json!([])), &[]).is_empty());
221 assert_eq!(absent_alternatives(&no_value(), &[]), vec![Vec::new()]);
222 }
223
224 #[test]
225 fn classify_members_marks_reference_values_dynamic() {
226 let members = vec![
227 ResolvedValue::Reference { target: "P".to_string(), kind: crate::resolver::RefKind::Ref },
228 concrete(serde_json::json!("a")),
229 ];
230 let classified = classify_members(&members);
231 assert!(classified[0].is_dynamic && classified[0].presence == MemberPresence::Always);
232 assert!(!classified[1].is_dynamic);
233 }
234
235 #[test]
236 fn extend_assignment_rejects_contradicting_literals_without_mutating() {
237 let mut assignment = HashMap::from([("A".to_string(), true)]);
238 assert!(!extend_assignment(&mut assignment, &[("B".to_string(), true), ("A".to_string(), false)]));
239 assert_eq!(assignment.len(), 1);
240 assert!(extend_assignment(&mut assignment, &[("B".to_string(), true)]));
241 assert_eq!(assignment.get("B"), Some(&true));
242 }
243
244 #[test]
245 fn repeated_condition_that_requires_both_values_is_never_present() {
246 let impossible =
247 conditional("Same", conditional("Same", no_value(), concrete(serde_json::json!("dead"))), no_value());
248 assert_eq!(member_presence(&impossible), MemberPresence::Never);
249 }
250
251 #[test]
252 fn absent_alternatives_honor_established_condition_decisions() {
253 let member = conditional("Same", concrete(serde_json::json!("present")), no_value());
254 assert!(absent_alternatives(&member, &[("Same".to_string(), true)]).is_empty());
255 assert_eq!(
256 absent_alternatives(&member, &[("Same".to_string(), false)]),
257 vec![vec![("Same".to_string(), false)]]
258 );
259 }
260
261 #[test]
262 fn extend_assignment_rejects_contradictions_within_new_literals() {
263 let mut assignment = HashMap::new();
264 assert!(!extend_assignment(&mut assignment, &[("A".to_string(), true), ("A".to_string(), false)]));
265 assert!(assignment.is_empty());
266 }
267}