1use serde_json::{Map, Value};
2
3use crate::{FormSpec, RenderPayload, StoreOp, ValidationResult, build_render_payload, validate};
4
5#[derive(Debug, Clone)]
7pub struct QaPlanV1 {
8 pub plan_version: u16,
9 pub form_id: String,
10 pub validated_patch: Value,
11 pub validation: ValidationResult,
12 pub payload: RenderPayload,
13 pub effects: Vec<StoreOp>,
14 pub warnings: Vec<String>,
15 pub errors: Vec<String>,
16}
17
18impl QaPlanV1 {
19 pub fn is_valid(&self) -> bool {
20 self.validation.valid
21 }
22}
23
24pub fn plan_submit_patch(
26 spec: &FormSpec,
27 ctx: &Value,
28 answers: &Value,
29 question_id: &str,
30 value: Value,
31) -> QaPlanV1 {
32 let mut patched = answers.as_object().cloned().unwrap_or_default();
33 patched.insert(question_id.to_string(), value);
34 build_plan(spec, ctx, Value::Object(patched))
35}
36
37pub fn plan_submit_all(spec: &FormSpec, ctx: &Value, answers: &Value) -> QaPlanV1 {
39 build_plan(spec, ctx, answers.clone())
40}
41
42pub fn plan_next(spec: &FormSpec, ctx: &Value, answers: &Value) -> QaPlanV1 {
44 build_plan(spec, ctx, normalize_answers(answers))
45}
46
47fn build_plan(spec: &FormSpec, ctx: &Value, answers: Value) -> QaPlanV1 {
48 let validation = validate(spec, &answers);
49 let payload = build_render_payload(spec, ctx, &answers);
50 let effects = if validation.valid {
51 spec.store.clone()
52 } else {
53 Vec::new()
54 };
55
56 let mut errors = Vec::new();
57 if !validation.valid {
58 errors.extend(validation.errors.iter().map(|error| {
59 format!(
60 "{}: {}",
61 error.path.clone().unwrap_or_default(),
62 error.message
63 )
64 }));
65 errors.extend(
66 validation
67 .missing_required
68 .iter()
69 .map(|field| format!("missing required: {}", field)),
70 );
71 errors.extend(
72 validation
73 .unknown_fields
74 .iter()
75 .map(|field| format!("unknown field: {}", field)),
76 );
77 }
78
79 QaPlanV1 {
80 plan_version: 1,
81 form_id: spec.id.clone(),
82 validated_patch: answers,
83 validation,
84 payload,
85 effects,
86 warnings: Vec::new(),
87 errors,
88 }
89}
90
91pub fn execute_plan_effects(
93 plan: &QaPlanV1,
94 store_ctx: &mut crate::StoreContext,
95 secrets_policy: Option<&crate::spec::form::SecretsPolicy>,
96 secrets_host_available: bool,
97) -> Result<(), crate::StoreError> {
98 if !plan.is_valid() {
99 return Ok(());
100 }
101 store_ctx.answers = plan.validated_patch.clone();
102 store_ctx.apply_ops(&plan.effects, secrets_policy, secrets_host_available)
103}
104
105pub fn normalize_answers(answers: &Value) -> Value {
107 Value::Object(
108 answers
109 .as_object()
110 .cloned()
111 .unwrap_or_else(Map::<String, Value>::new),
112 )
113}