1use serde_json::{Map, Value};
2
3use crate::spec::form::FormSpec;
4use crate::spec::question::QuestionSpec;
5use crate::store::StoreTarget;
6use crate::visibility::VisibilityMap;
7
8#[derive(Debug, Clone)]
10pub struct ProgressContext {
11 answers: Map<String, Value>,
12 config: Value,
13 state: Value,
14 payload_out: Value,
15 secrets: Value,
16}
17
18impl ProgressContext {
19 pub fn new(answers: Value, ctx: &Value) -> Self {
20 let answers_map = answers.as_object().cloned().unwrap_or_default();
21 let default = || Value::Object(Map::new());
22 Self {
23 answers: answers_map,
24 config: ctx.get("config").cloned().unwrap_or_else(default),
25 state: ctx.get("state").cloned().unwrap_or_else(default),
26 payload_out: ctx.get("payload_out").cloned().unwrap_or_else(default),
27 secrets: ctx.get("secrets").cloned().unwrap_or_else(default),
28 }
29 }
30
31 fn has_target(&self, target: StoreTarget, key: &str) -> bool {
32 match target {
33 StoreTarget::Answers => self.answers.contains_key(key),
34 StoreTarget::Config => self.config.get(key).is_some(),
35 StoreTarget::State => self.state.get(key).is_some(),
36 StoreTarget::PayloadOut => self.payload_out.get(key).is_some(),
37 StoreTarget::Secrets => self.secrets.get(key).is_some(),
38 }
39 }
40
41 pub fn answered_count(&self, spec: &FormSpec, visibility: &VisibilityMap) -> usize {
42 spec.questions
43 .iter()
44 .filter(|question| {
45 visibility.get(&question.id).copied().unwrap_or(true)
46 && is_answered(question, self, spec.progress_policy.as_ref())
47 })
48 .count()
49 }
50}
51
52pub fn next_question(
53 spec: &FormSpec,
54 ctx: &ProgressContext,
55 visibility: &VisibilityMap,
56) -> Option<String> {
57 let progress_policy = spec.progress_policy.as_ref().copied().unwrap_or_default();
58
59 for question in &spec.questions {
60 if !visibility.get(&question.id).copied().unwrap_or(true) {
61 continue;
62 }
63
64 if should_skip(question, ctx, &progress_policy) {
65 continue;
66 }
67
68 return Some(question.id.clone());
69 }
70
71 None
72}
73
74fn should_skip(
75 question: &QuestionSpec,
76 ctx: &ProgressContext,
77 policy: &crate::spec::form::ProgressPolicy,
78) -> bool {
79 if question
80 .policy
81 .skip_if_present_in
82 .iter()
83 .any(|target| ctx.has_target(*target, &question.id))
84 {
85 return true;
86 }
87
88 if policy.skip_answered && is_answered(question, ctx, Some(policy)) {
89 return true;
90 }
91
92 false
93}
94
95fn is_answered(
96 question: &QuestionSpec,
97 ctx: &ProgressContext,
98 policy: Option<&crate::spec::form::ProgressPolicy>,
99) -> bool {
100 let has_answer = ctx.answers.contains_key(&question.id);
101 let defaults_policy = policy
102 .copied()
103 .unwrap_or_else(crate::spec::form::ProgressPolicy::default);
104
105 if has_answer {
106 return true;
107 }
108
109 if defaults_policy.autofill_defaults && question.default_value.is_some() {
110 if question.policy.editable_if_from_default {
111 return false;
112 }
113 return defaults_policy.treat_default_as_answered;
114 }
115
116 false
117}