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#[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#[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#[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#[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}