1use std::collections::{BTreeMap, BTreeSet};
2
3use thiserror::Error;
4
5use crate::{Expr, FormSpec, QuestionSpec, spec::validation::CrossFieldValidation};
6
7#[derive(Debug, Error)]
8pub enum IncludeError {
9 #[error("missing include target '{form_ref}'")]
10 MissingIncludeTarget { form_ref: String },
11 #[error("include cycle detected: {chain:?}")]
12 IncludeCycleDetected { chain: Vec<String> },
13 #[error("duplicate question id after include expansion: '{question_id}'")]
14 DuplicateQuestionId { question_id: String },
15}
16
17pub fn expand_includes(
19 root: &FormSpec,
20 registry: &BTreeMap<String, FormSpec>,
21) -> Result<FormSpec, IncludeError> {
22 let mut chain = Vec::new();
23 let mut seen = BTreeSet::new();
24 expand_form(root, "", registry, &mut chain, &mut seen)
25}
26
27fn expand_form(
28 form: &FormSpec,
29 prefix: &str,
30 registry: &BTreeMap<String, FormSpec>,
31 chain: &mut Vec<String>,
32 seen_ids: &mut BTreeSet<String>,
33) -> Result<FormSpec, IncludeError> {
34 if chain.contains(&form.id) {
35 let start = chain.iter().position(|id| id == &form.id).unwrap_or(0);
36 let mut cycle = chain[start..].to_vec();
37 cycle.push(form.id.clone());
38 return Err(IncludeError::IncludeCycleDetected { chain: cycle });
39 }
40 chain.push(form.id.clone());
41
42 let mut out = form.clone();
43 out.questions.clear();
44 out.validations.clear();
45 out.includes.clear();
46
47 for question in &form.questions {
48 let question = apply_prefix_question(question, prefix);
49 if !seen_ids.insert(question.id.clone()) {
50 return Err(IncludeError::DuplicateQuestionId {
51 question_id: question.id,
52 });
53 }
54 out.questions.push(question);
55 }
56
57 for validation in &form.validations {
58 out.validations
59 .push(apply_prefix_validation(validation, prefix));
60 }
61
62 for include in &form.includes {
63 let included =
64 registry
65 .get(&include.form_ref)
66 .ok_or_else(|| IncludeError::MissingIncludeTarget {
67 form_ref: include.form_ref.clone(),
68 })?;
69 let nested_prefix = combine_prefix(prefix, include.prefix.as_deref());
70 let expanded = expand_form(included, &nested_prefix, registry, chain, seen_ids)?;
71 out.questions.extend(expanded.questions);
72 out.validations.extend(expanded.validations);
73 }
74
75 chain.pop();
76 Ok(out)
77}
78
79fn apply_prefix_validation(
80 validation: &CrossFieldValidation,
81 prefix: &str,
82) -> CrossFieldValidation {
83 if prefix.is_empty() {
84 return validation.clone();
85 }
86 let mut out = validation.clone();
87 out.id = out.id.map(|id| prefix_key(prefix, &id));
88 out.fields = out
89 .fields
90 .iter()
91 .map(|field| prefix_key(prefix, field))
92 .collect();
93 out.condition = prefix_expr(out.condition, prefix);
94 out
95}
96
97fn apply_prefix_question(question: &QuestionSpec, prefix: &str) -> QuestionSpec {
98 if prefix.is_empty() {
99 return question.clone();
100 }
101 let mut out = question.clone();
102 out.id = prefix_key(prefix, &out.id);
103 out.visible_if = out.visible_if.map(|expr| prefix_expr(expr, prefix));
104 out.computed = out.computed.map(|expr| prefix_expr(expr, prefix));
105 if let Some(list) = &mut out.list {
106 list.fields = list
107 .fields
108 .iter()
109 .map(|field| apply_prefix_question(field, prefix))
110 .collect();
111 }
112 out
113}
114
115fn prefix_expr(expr: Expr, prefix: &str) -> Expr {
116 match expr {
117 Expr::Answer { path } => Expr::Answer {
118 path: prefix_path(prefix, &path),
119 },
120 Expr::IsSet { path } => Expr::IsSet {
121 path: prefix_path(prefix, &path),
122 },
123 Expr::And { expressions } => Expr::And {
124 expressions: expressions
125 .into_iter()
126 .map(|expr| prefix_expr(expr, prefix))
127 .collect(),
128 },
129 Expr::Or { expressions } => Expr::Or {
130 expressions: expressions
131 .into_iter()
132 .map(|expr| prefix_expr(expr, prefix))
133 .collect(),
134 },
135 Expr::Not { expression } => Expr::Not {
136 expression: Box::new(prefix_expr(*expression, prefix)),
137 },
138 Expr::Eq { left, right } => Expr::Eq {
139 left: Box::new(prefix_expr(*left, prefix)),
140 right: Box::new(prefix_expr(*right, prefix)),
141 },
142 Expr::Ne { left, right } => Expr::Ne {
143 left: Box::new(prefix_expr(*left, prefix)),
144 right: Box::new(prefix_expr(*right, prefix)),
145 },
146 Expr::Lt { left, right } => Expr::Lt {
147 left: Box::new(prefix_expr(*left, prefix)),
148 right: Box::new(prefix_expr(*right, prefix)),
149 },
150 Expr::Lte { left, right } => Expr::Lte {
151 left: Box::new(prefix_expr(*left, prefix)),
152 right: Box::new(prefix_expr(*right, prefix)),
153 },
154 Expr::Gt { left, right } => Expr::Gt {
155 left: Box::new(prefix_expr(*left, prefix)),
156 right: Box::new(prefix_expr(*right, prefix)),
157 },
158 Expr::Gte { left, right } => Expr::Gte {
159 left: Box::new(prefix_expr(*left, prefix)),
160 right: Box::new(prefix_expr(*right, prefix)),
161 },
162 other => other,
163 }
164}
165
166fn prefix_path(prefix: &str, path: &str) -> String {
167 if path.is_empty() || path.starts_with('/') || prefix.is_empty() {
168 return path.to_string();
169 }
170 format!("{}.{}", prefix, path)
171}
172
173fn prefix_key(prefix: &str, key: &str) -> String {
174 if prefix.is_empty() {
175 key.to_string()
176 } else {
177 format!("{}.{}", prefix, key)
178 }
179}
180
181fn combine_prefix(parent: &str, child: Option<&str>) -> String {
182 match (parent.is_empty(), child.unwrap_or("").is_empty()) {
183 (true, true) => String::new(),
184 (false, true) => parent.to_string(),
185 (true, false) => child.unwrap_or_default().to_string(),
186 (false, false) => format!("{}.{}", parent, child.unwrap_or_default()),
187 }
188}