1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use stack_ids::{
4 ArtifactAdmissionPolicyId, DisclosureBudgetId, DisclosurePolicyId, ExperimentBudgetId,
5 RefuterSuiteId, TrustRootSetId,
6};
7
8pub const REFUTER_SUITE_V1_SCHEMA: &str = "refuter_suite_v1";
9pub const EXPERIMENT_BUDGET_V1_SCHEMA: &str = "experiment_budget_v1";
10pub const ARTIFACT_ADMISSION_POLICY_V1_SCHEMA: &str = "artifact_admission_policy_v1";
11pub const DISCLOSURE_POLICY_V1_SCHEMA: &str = "disclosure_policy_v1";
12pub const DISCLOSURE_BUDGET_V1_SCHEMA: &str = "disclosure_budget_v1";
13
14fn require_schema_version(
15 found: &str,
16 expected: &'static str,
17 field: &'static str,
18) -> Result<(), &'static str> {
19 if found != expected {
20 return Err(field);
21 }
22 Ok(())
23}
24
25fn require_non_empty(value: &str, field: &'static str) -> Result<(), &'static str> {
26 if value.trim().is_empty() {
27 return Err(field);
28 }
29 Ok(())
30}
31
32#[derive(
33 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
34)]
35#[serde(rename_all = "snake_case")]
36pub enum ExperimentBudgetClassV1 {
37 Small,
38 Medium,
39 Large,
40}
41
42#[derive(
43 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
44)]
45#[serde(rename_all = "snake_case")]
46pub enum ExperimentMaxExactnessV1 {
47 BoundedExact,
48}
49
50#[derive(
51 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
52)]
53#[serde(rename_all = "snake_case")]
54pub enum RefuterAllowanceV1 {
55 #[serde(rename = "up_to_1")]
56 UpTo1,
57 #[serde(rename = "up_to_3")]
58 UpTo3,
59}
60
61#[derive(
62 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
63)]
64#[serde(rename_all = "snake_case")]
65pub enum OracleAllowanceV1 {
66 OneExactSlice,
67}
68
69#[derive(
70 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
71)]
72#[serde(rename_all = "snake_case")]
73pub enum ReplayAllowanceV1 {
74 FullReplayFamily,
75}
76
77#[derive(
78 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
79)]
80#[serde(rename_all = "snake_case")]
81pub enum HumanReviewAllowanceV1 {
82 OneRequiredIfRollout,
83}
84
85#[derive(
86 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
87)]
88#[serde(rename_all = "snake_case")]
89pub enum BudgetExhaustionBehaviorV1 {
90 EmitTypedDegradationAndHoldAdvisory,
91}
92
93#[derive(
94 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
95)]
96#[serde(rename_all = "snake_case")]
97pub enum PolicyReplayabilityClassV1 {
98 ReplayableWithTicket,
99}
100
101#[derive(
102 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
103)]
104#[serde(rename_all = "snake_case")]
105pub enum PolicyDowngradeBehaviorV1 {
106 AdvisoryOnlyIfReplayMissing,
107 AdvisoryOnlyIfExactMaterialCannotBeShared,
108}
109
110#[derive(
111 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
112)]
113#[serde(rename_all = "snake_case")]
114pub enum DisclosureReplayVisibilityV1 {
115 TicketRequired,
116}
117
118#[derive(
119 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
120)]
121#[serde(rename_all = "snake_case")]
122pub enum DisclosureRevealClassV1 {
123 #[serde(rename = "structured-redacted")]
124 StructuredRedacted,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
128pub struct RefuterSuiteV1 {
129 pub schema_version: String,
130 pub refuter_suite_id: RefuterSuiteId,
131 pub treatment_definition_check: bool,
132 pub negative_control_option: bool,
133 pub placebo_option: bool,
134 pub dummy_outcome_option: bool,
135 pub subset_stability_option: bool,
136 pub alternative_comparison_option: bool,
137 #[serde(default, skip_serializing_if = "Vec::is_empty")]
138 pub admissibility_notes: Vec<String>,
139 pub cost_metadata: String,
140}
141
142impl RefuterSuiteV1 {
143 #[allow(clippy::too_many_arguments)]
144 pub fn new(
146 refuter_suite_id: RefuterSuiteId,
147 treatment_definition_check: bool,
148 negative_control_option: bool,
149 placebo_option: bool,
150 dummy_outcome_option: bool,
151 subset_stability_option: bool,
152 alternative_comparison_option: bool,
153 admissibility_notes: Vec<String>,
154 cost_metadata: impl Into<String>,
155 ) -> Result<Self, &'static str> {
156 let value = Self {
157 schema_version: REFUTER_SUITE_V1_SCHEMA.to_string(),
158 refuter_suite_id,
159 treatment_definition_check,
160 negative_control_option,
161 placebo_option,
162 dummy_outcome_option,
163 subset_stability_option,
164 alternative_comparison_option,
165 admissibility_notes,
166 cost_metadata: cost_metadata.into(),
167 };
168 value.validate()?;
169 Ok(value)
170 }
171
172 pub fn validate(&self) -> Result<(), &'static str> {
175 require_schema_version(
176 &self.schema_version,
177 REFUTER_SUITE_V1_SCHEMA,
178 "schema_version",
179 )?;
180 require_non_empty(&self.cost_metadata, "cost_metadata")?;
181 if !self.treatment_definition_check
182 && !self.negative_control_option
183 && !self.placebo_option
184 && !self.dummy_outcome_option
185 && !self.subset_stability_option
186 && !self.alternative_comparison_option
187 {
188 return Err("refuter_suite_options");
189 }
190 Ok(())
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
195pub struct ExperimentBudgetV1 {
196 pub schema_version: String,
197 pub experiment_budget_id: ExperimentBudgetId,
198 pub budget_class: ExperimentBudgetClassV1,
199 pub max_exactness: ExperimentMaxExactnessV1,
200 pub refuter_allowance: RefuterAllowanceV1,
201 pub oracle_allowance: OracleAllowanceV1,
202 pub replay_allowance: ReplayAllowanceV1,
203 pub human_review_allowance: HumanReviewAllowanceV1,
204 pub exhaustion_behavior: BudgetExhaustionBehaviorV1,
205}
206
207impl ExperimentBudgetV1 {
208 #[allow(clippy::too_many_arguments)]
209 pub fn new(
211 experiment_budget_id: ExperimentBudgetId,
212 budget_class: ExperimentBudgetClassV1,
213 max_exactness: ExperimentMaxExactnessV1,
214 refuter_allowance: RefuterAllowanceV1,
215 oracle_allowance: OracleAllowanceV1,
216 replay_allowance: ReplayAllowanceV1,
217 human_review_allowance: HumanReviewAllowanceV1,
218 exhaustion_behavior: BudgetExhaustionBehaviorV1,
219 ) -> Result<Self, &'static str> {
220 let value = Self {
221 schema_version: EXPERIMENT_BUDGET_V1_SCHEMA.to_string(),
222 experiment_budget_id,
223 budget_class,
224 max_exactness,
225 refuter_allowance,
226 oracle_allowance,
227 replay_allowance,
228 human_review_allowance,
229 exhaustion_behavior,
230 };
231 value.validate()?;
232 Ok(value)
233 }
234
235 pub fn validate(&self) -> Result<(), &'static str> {
237 require_schema_version(
238 &self.schema_version,
239 EXPERIMENT_BUDGET_V1_SCHEMA,
240 "schema_version",
241 )
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
246pub struct ArtifactAdmissionPolicyV1 {
247 pub schema_version: String,
248 pub artifact_admission_policy_id: ArtifactAdmissionPolicyId,
249 #[serde(default, skip_serializing_if = "Vec::is_empty")]
250 pub allowed_artifact_families: Vec<String>,
251 #[serde(default, skip_serializing_if = "Vec::is_empty")]
252 pub required_trust_root_sets: Vec<TrustRootSetId>,
253 #[serde(default, skip_serializing_if = "Vec::is_empty")]
254 pub required_transparency_obligations: Vec<String>,
255 pub required_replayability_class: PolicyReplayabilityClassV1,
256 #[serde(default, skip_serializing_if = "Vec::is_empty")]
257 pub disclosure_constraints: Vec<String>,
258 pub downgrade_behavior: PolicyDowngradeBehaviorV1,
259 #[serde(default, skip_serializing_if = "Vec::is_empty")]
260 pub disqualifying_failures: Vec<String>,
261}
262
263impl ArtifactAdmissionPolicyV1 {
264 #[allow(clippy::too_many_arguments)]
265 pub fn new(
267 artifact_admission_policy_id: ArtifactAdmissionPolicyId,
268 allowed_artifact_families: Vec<String>,
269 required_trust_root_sets: Vec<TrustRootSetId>,
270 required_transparency_obligations: Vec<String>,
271 required_replayability_class: PolicyReplayabilityClassV1,
272 disclosure_constraints: Vec<String>,
273 downgrade_behavior: PolicyDowngradeBehaviorV1,
274 disqualifying_failures: Vec<String>,
275 ) -> Result<Self, &'static str> {
276 let value = Self {
277 schema_version: ARTIFACT_ADMISSION_POLICY_V1_SCHEMA.to_string(),
278 artifact_admission_policy_id,
279 allowed_artifact_families,
280 required_trust_root_sets,
281 required_transparency_obligations,
282 required_replayability_class,
283 disclosure_constraints,
284 downgrade_behavior,
285 disqualifying_failures,
286 };
287 value.validate()?;
288 Ok(value)
289 }
290
291 pub fn validate(&self) -> Result<(), &'static str> {
293 require_schema_version(
294 &self.schema_version,
295 ARTIFACT_ADMISSION_POLICY_V1_SCHEMA,
296 "schema_version",
297 )
298 }
299}
300
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
302pub struct DisclosurePolicyV1 {
303 pub schema_version: String,
304 pub disclosure_policy_id: DisclosurePolicyId,
305 #[serde(default, skip_serializing_if = "Vec::is_empty")]
306 pub allowed_consumers: Vec<String>,
307 #[serde(default, skip_serializing_if = "Vec::is_empty")]
308 pub redaction_rules: Vec<String>,
309 pub replay_visibility: DisclosureReplayVisibilityV1,
310 pub retention_window: String,
311 pub downgrade_behavior: PolicyDowngradeBehaviorV1,
312}
313
314impl DisclosurePolicyV1 {
315 pub fn new(
317 disclosure_policy_id: DisclosurePolicyId,
318 allowed_consumers: Vec<String>,
319 redaction_rules: Vec<String>,
320 replay_visibility: DisclosureReplayVisibilityV1,
321 retention_window: impl Into<String>,
322 downgrade_behavior: PolicyDowngradeBehaviorV1,
323 ) -> Result<Self, &'static str> {
324 let value = Self {
325 schema_version: DISCLOSURE_POLICY_V1_SCHEMA.to_string(),
326 disclosure_policy_id,
327 allowed_consumers,
328 redaction_rules,
329 replay_visibility,
330 retention_window: retention_window.into(),
331 downgrade_behavior,
332 };
333 value.validate()?;
334 Ok(value)
335 }
336
337 pub fn validate(&self) -> Result<(), &'static str> {
339 require_schema_version(
340 &self.schema_version,
341 DISCLOSURE_POLICY_V1_SCHEMA,
342 "schema_version",
343 )?;
344 require_non_empty(&self.retention_window, "retention_window")?;
345 Ok(())
346 }
347}
348
349#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
350pub struct DisclosureBudgetV1 {
351 pub schema_version: String,
352 pub disclosure_budget_id: DisclosureBudgetId,
353 pub allowed_reveal_class: DisclosureRevealClassV1,
354 pub current_spend: String,
355 pub redaction_ceiling: String,
356 pub escalation_path: String,
357}
358
359impl DisclosureBudgetV1 {
360 pub fn new(
362 disclosure_budget_id: DisclosureBudgetId,
363 allowed_reveal_class: DisclosureRevealClassV1,
364 current_spend: impl Into<String>,
365 redaction_ceiling: impl Into<String>,
366 escalation_path: impl Into<String>,
367 ) -> Result<Self, &'static str> {
368 let value = Self {
369 schema_version: DISCLOSURE_BUDGET_V1_SCHEMA.to_string(),
370 disclosure_budget_id,
371 allowed_reveal_class,
372 current_spend: current_spend.into(),
373 redaction_ceiling: redaction_ceiling.into(),
374 escalation_path: escalation_path.into(),
375 };
376 value.validate()?;
377 Ok(value)
378 }
379
380 pub fn validate(&self) -> Result<(), &'static str> {
383 require_schema_version(
384 &self.schema_version,
385 DISCLOSURE_BUDGET_V1_SCHEMA,
386 "schema_version",
387 )?;
388 require_non_empty(&self.current_spend, "current_spend")?;
389 require_non_empty(&self.redaction_ceiling, "redaction_ceiling")?;
390 require_non_empty(&self.escalation_path, "escalation_path")?;
391 Ok(())
392 }
393}