Skip to main content

qa_spec/
store.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use serde_json::{Map, Value};
4use thiserror::Error;
5
6use crate::secrets::{SecretAccessResult, SecretAction, evaluate};
7use crate::spec::form::SecretsPolicy;
8
9/// Targets that store operations can write into.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
11#[serde(rename_all = "snake_case")]
12pub enum StoreTarget {
13    Answers,
14    State,
15    Config,
16    PayloadOut,
17    Secrets,
18}
19
20/// Single store operation.
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
22pub struct StoreOp {
23    pub target: StoreTarget,
24    pub path: String,
25    pub value: Value,
26}
27
28/// Context mutated by store operations.
29#[derive(Debug, Clone)]
30pub struct StoreContext {
31    pub answers: Value,
32    pub state: Value,
33    pub config: Value,
34    pub payload_out: Value,
35    pub secrets: Value,
36}
37
38impl StoreContext {
39    pub fn from_value(ctx: &Value) -> Self {
40        let default = || Value::Object(Map::new());
41        Self {
42            answers: ctx.get("answers").cloned().unwrap_or_else(default),
43            state: ctx.get("state").cloned().unwrap_or_else(default),
44            config: ctx.get("config").cloned().unwrap_or_else(default),
45            payload_out: ctx.get("payload_out").cloned().unwrap_or_else(default),
46            secrets: ctx.get("secrets").cloned().unwrap_or_else(default),
47        }
48    }
49
50    pub fn apply_ops(
51        &mut self,
52        ops: &[StoreOp],
53        policy: Option<&SecretsPolicy>,
54        host_available: bool,
55    ) -> Result<(), StoreError> {
56        for op in ops {
57            match op.target {
58                StoreTarget::Answers => set_path(&mut self.answers, &op.path, op.value.clone())?,
59                StoreTarget::State => set_path(&mut self.state, &op.path, op.value.clone())?,
60                StoreTarget::Config => set_path(&mut self.config, &op.path, op.value.clone())?,
61                StoreTarget::PayloadOut => {
62                    set_path(&mut self.payload_out, &op.path, op.value.clone())?
63                }
64                StoreTarget::Secrets => {
65                    let key = secret_key(&op.path)?;
66                    match evaluate(policy, &key, SecretAction::Write, host_available) {
67                        SecretAccessResult::Allowed => {
68                            set_path(&mut self.secrets, &op.path, op.value.clone())?;
69                        }
70                        SecretAccessResult::Denied(code) => {
71                            return Err(StoreError::SecretAccessDenied { key, code });
72                        }
73                        SecretAccessResult::HostUnavailable => {
74                            return Err(StoreError::SecretHostUnavailable);
75                        }
76                    }
77                }
78            }
79        }
80        Ok(())
81    }
82
83    pub fn to_value(&self) -> Value {
84        let mut map = Map::new();
85        map.insert("answers".into(), self.answers.clone());
86        map.insert("state".into(), self.state.clone());
87        map.insert("config".into(), self.config.clone());
88        map.insert("payload_out".into(), self.payload_out.clone());
89        map.insert("secrets".into(), self.secrets.clone());
90        Value::Object(map)
91    }
92}
93
94/// Errors raised while applying store operations.
95#[derive(Debug, Error)]
96pub enum StoreError {
97    #[error("invalid pointer '{0}'")]
98    InvalidPointer(String),
99    #[error("secret access denied for '{key}' ({code})")]
100    SecretAccessDenied { key: String, code: &'static str },
101    #[error("secret host unavailable")]
102    SecretHostUnavailable,
103}
104
105fn set_path(root: &mut Value, pointer: &str, value: Value) -> Result<(), StoreError> {
106    if pointer.is_empty() {
107        *root = value;
108        return Ok(());
109    }
110
111    let segments = pointer
112        .trim_start_matches('/')
113        .split('/')
114        .map(decode_segment)
115        .collect::<Vec<_>>();
116
117    let mut current = root;
118    for (idx, segment) in segments.iter().enumerate() {
119        if idx + 1 == segments.len() {
120            ensure_object(current).insert(segment.clone(), value);
121            return Ok(());
122        }
123        current = ensure_object(current)
124            .entry(segment.clone())
125            .or_insert_with(|| Value::Object(Map::new()));
126    }
127
128    Err(StoreError::InvalidPointer(pointer.to_string()))
129}
130
131fn ensure_object(value: &mut Value) -> &mut Map<String, Value> {
132    if !value.is_object() {
133        *value = Value::Object(Map::new());
134    }
135    value.as_object_mut().expect("value is object")
136}
137
138fn decode_segment(segment: &str) -> String {
139    segment.replace("~1", "/").replace("~0", "~")
140}
141
142fn secret_key(pointer: &str) -> Result<String, StoreError> {
143    let trimmed = pointer.trim_start_matches('/');
144    if trimmed.is_empty() {
145        return Err(StoreError::InvalidPointer(pointer.to_string()));
146    }
147    Ok(trimmed.to_string())
148}