1use alloc::{string::String, vec::Vec};
3
4use ciborium::value::Value;
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8use crate::{cbor::canonical, cbor_bytes::CborBytes, schema_id::SchemaSource};
9
10#[derive(Clone, Debug, PartialEq, Eq)]
12#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13pub enum QaSpecSource {
14 InlineCbor(CborBytes),
16 #[cfg(feature = "json-compat")]
17 InlineJson(String),
19 RefUri(String),
21 RefPackPath(String),
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
27#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
28pub struct ExampleAnswers {
29 pub title: String,
31 pub answers_cbor: CborBytes,
33 pub notes: Option<String>,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
40pub enum SetupOutput {
41 ConfigOnly,
43 TemplateScaffold {
45 template_ref: String,
47 output_layout: String,
49 },
50}
51
52#[derive(Clone, Debug, PartialEq, Eq)]
54#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
55pub struct SetupContract {
56 pub qa_spec: QaSpecSource,
58 pub answers_schema: Option<SchemaSource>,
60 pub examples: Vec<ExampleAnswers>,
62 pub outputs: Vec<SetupOutput>,
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub enum CanonicalPolicy {
69 Off,
71 RequireCanonical,
73 Canonicalize,
75}
76
77#[derive(Debug, Error)]
79pub enum ValidateAnswersError {
80 #[error("CBOR decode failed: {0}")]
82 Decode(String),
83 #[error("answers must be a CBOR map/object")]
85 NotMap,
86 #[error(transparent)]
88 Canonical(#[from] canonical::CanonicalError),
89}
90
91pub fn validate_answers(
93 schema: &SchemaSource,
94 answers_cbor: &CborBytes,
95 policy: CanonicalPolicy,
96) -> Result<CborBytes, ValidateAnswersError> {
97 let _ = schema;
98 let value: Value = ciborium::de::from_reader(answers_cbor.as_slice())
99 .map_err(|err| ValidateAnswersError::Decode(err.to_string()))?;
100
101 if !matches!(value, Value::Map(_)) {
102 return Err(ValidateAnswersError::NotMap);
103 }
104
105 match policy {
106 CanonicalPolicy::Off => Ok(answers_cbor.clone()),
107 CanonicalPolicy::RequireCanonical => {
108 answers_cbor.ensure_canonical()?;
109 Ok(answers_cbor.clone())
110 }
111 CanonicalPolicy::Canonicalize => {
112 let canonical_bytes = canonical::canonicalize(answers_cbor.as_slice())?;
113 Ok(CborBytes(canonical_bytes))
114 }
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use alloc::collections::BTreeMap;
122
123 fn schema_bytes() -> Vec<u8> {
124 let mut map = BTreeMap::new();
125 map.insert("key", "value");
126 match canonical::to_canonical_cbor(&map) {
127 Ok(bytes) => bytes,
128 Err(err) => panic!("schema canonicalization failed: {err:?}"),
129 }
130 }
131
132 fn schema_blob() -> CborBytes {
133 CborBytes(schema_bytes())
134 }
135
136 #[test]
137 fn validate_accepts_map_off_policy() {
138 let bytes = schema_blob();
139 let source = SchemaSource::InlineCbor(bytes.clone());
140 let result = match validate_answers(&source, &bytes, CanonicalPolicy::Off) {
141 Ok(value) => value,
142 Err(err) => panic!("validation failed: {err:?}"),
143 };
144 assert_eq!(result.as_slice(), bytes.as_slice());
145 }
146
147 #[test]
148 fn validate_rejects_non_map() {
149 let bytes = match canonical::to_canonical_cbor(&"string") {
150 Ok(value) => value,
151 Err(err) => panic!("canonicalize string failed: {err:?}"),
152 };
153 let source = SchemaSource::InlineCbor(CborBytes(bytes.clone()));
154 assert!(matches!(
155 validate_answers(&source, &CborBytes(bytes), CanonicalPolicy::Off),
156 Err(ValidateAnswersError::NotMap)
157 ));
158 }
159
160 #[test]
161 fn canonicalize_policy_rewrites_indefinite_map() {
162 let indefinite = vec![0xBF, 0x61, b'a', 0x01, 0xFF];
163 let source = SchemaSource::InlineCbor(CborBytes::new(indefinite.clone()));
164 let canonical_bytes = match validate_answers(
165 &source,
166 &CborBytes(indefinite),
167 CanonicalPolicy::Canonicalize,
168 ) {
169 Ok(bytes) => bytes,
170 Err(err) => panic!("validation failed: {err:?}"),
171 };
172 if let Err(err) = canonical::ensure_canonical(canonical_bytes.as_slice()) {
173 panic!("ensure canonical failed: {err:?}");
174 }
175 }
176
177 #[test]
178 fn require_canonical_rejects_indefinite() {
179 let indefinite = vec![0xBF, 0x61, b'a', 0x01, 0xFF];
180 let source = SchemaSource::InlineCbor(CborBytes::new(indefinite.clone()));
181 assert!(matches!(
182 validate_answers(
183 &source,
184 &CborBytes(indefinite),
185 CanonicalPolicy::RequireCanonical
186 ),
187 Err(ValidateAnswersError::Canonical(_))
188 ));
189 }
190}