1pub mod v1;
6
7pub const OPENAPI_V1: &str = include_str!("../assets/openapi/hyphae-v1.yaml");
9
10pub const CAPABILITIES_SCHEMA_V1: &str =
12 include_str!("../assets/json-schema/capabilities-v1.schema.json");
13
14pub const ERROR_SCHEMA_V1: &str = include_str!("../assets/json-schema/error-v1.schema.json");
16
17pub const HEALTH_SCHEMA_V1: &str = include_str!("../assets/json-schema/health-v1.schema.json");
19
20pub const PUT_REQUEST_SCHEMA_V1: &str =
22 include_str!("../assets/json-schema/put-request-v1.schema.json");
23
24pub const DELETE_REQUEST_SCHEMA_V1: &str =
26 include_str!("../assets/json-schema/delete-request-v1.schema.json");
27
28pub const GET_REQUEST_SCHEMA_V1: &str =
30 include_str!("../assets/json-schema/get-request-v1.schema.json");
31
32pub const GET_RESPONSE_SCHEMA_V1: &str =
34 include_str!("../assets/json-schema/get-response-v1.schema.json");
35
36pub const COMMIT_RECEIPT_SCHEMA_V1: &str =
38 include_str!("../assets/json-schema/commit-receipt-v1.schema.json");
39
40pub const QUERY_REQUEST_SCHEMA_V1: &str =
42 include_str!("../assets/json-schema/query-request-v1.schema.json");
43
44pub const QUERY_RESPONSE_SCHEMA_V1: &str =
46 include_str!("../assets/json-schema/query-response-v1.schema.json");
47
48pub const PROOF_SCHEMA_V1: &str = include_str!("../assets/json-schema/proof-v1.schema.json");
50
51#[cfg(test)]
52mod tests {
53 use std::{error::Error, fs, io, path::Path};
54
55 use serde_json::Value as JsonValue;
56 use serde_yaml_ng::Value as YamlValue;
57
58 use schemars::{JsonSchema, SchemaGenerator};
59
60 use super::{
61 CAPABILITIES_SCHEMA_V1, COMMIT_RECEIPT_SCHEMA_V1, DELETE_REQUEST_SCHEMA_V1,
62 ERROR_SCHEMA_V1, GET_REQUEST_SCHEMA_V1, GET_RESPONSE_SCHEMA_V1, HEALTH_SCHEMA_V1,
63 OPENAPI_V1, PROOF_SCHEMA_V1, PUT_REQUEST_SCHEMA_V1, QUERY_REQUEST_SCHEMA_V1,
64 QUERY_RESPONSE_SCHEMA_V1,
65 v1::{
66 CapabilitiesV1, CommitReceiptV1, DeleteRequestV1, ErrorV1, GetRequestV1, GetResponseV1,
67 HealthV1, ProofV1, PutRequestV1, QueryRequestV1, QueryResponseV1,
68 },
69 };
70
71 #[test]
72 fn openapi_document_is_version_3_1() -> Result<(), Box<dyn Error>> {
73 let document: YamlValue = serde_yaml_ng::from_str(OPENAPI_V1)?;
74 assert_eq!(
75 document.get("openapi").and_then(YamlValue::as_str),
76 Some("3.1.0")
77 );
78 Ok(())
79 }
80
81 #[test]
82 fn openapi_defines_phase_five_surface_and_resolves_external_schemas()
83 -> Result<(), Box<dyn Error>> {
84 let document: YamlValue = serde_yaml_ng::from_str(OPENAPI_V1)?;
85 let paths = document
86 .get("paths")
87 .and_then(YamlValue::as_mapping)
88 .ok_or_else(|| io::Error::other("OpenAPI paths object is missing"))?;
89 for (path, method) in [
90 ("/v1/capabilities", "get"),
91 ("/v1/health/live", "get"),
92 ("/v1/health/ready", "get"),
93 ("/v1/kv/put", "post"),
94 ("/v1/kv/get", "post"),
95 ("/v1/kv/delete", "post"),
96 ("/v1/query", "post"),
97 (
98 "/v1/witnesses/{checkpoint_sequence}/{snapshot_digest}",
99 "get",
100 ),
101 ] {
102 let operation = paths
103 .get(YamlValue::String(path.to_owned()))
104 .and_then(|item| item.get(method));
105 assert!(operation.is_some(), "missing {method} {path}");
106 }
107
108 let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/openapi");
109 validate_external_schema_refs(&document, &base)?;
110 Ok(())
111 }
112
113 #[test]
114 fn packaged_contract_assets_match_workspace_canonical_files() -> Result<(), Box<dyn Error>> {
115 let canonical_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../contracts");
116 if !canonical_root.is_dir() {
117 return Ok(());
118 }
119
120 for (relative, packaged) in [
121 ("openapi/hyphae-v1.yaml", OPENAPI_V1),
122 (
123 "json-schema/capabilities-v1.schema.json",
124 CAPABILITIES_SCHEMA_V1,
125 ),
126 ("json-schema/error-v1.schema.json", ERROR_SCHEMA_V1),
127 ("json-schema/health-v1.schema.json", HEALTH_SCHEMA_V1),
128 (
129 "json-schema/put-request-v1.schema.json",
130 PUT_REQUEST_SCHEMA_V1,
131 ),
132 (
133 "json-schema/delete-request-v1.schema.json",
134 DELETE_REQUEST_SCHEMA_V1,
135 ),
136 (
137 "json-schema/get-request-v1.schema.json",
138 GET_REQUEST_SCHEMA_V1,
139 ),
140 (
141 "json-schema/get-response-v1.schema.json",
142 GET_RESPONSE_SCHEMA_V1,
143 ),
144 (
145 "json-schema/commit-receipt-v1.schema.json",
146 COMMIT_RECEIPT_SCHEMA_V1,
147 ),
148 (
149 "json-schema/query-request-v1.schema.json",
150 QUERY_REQUEST_SCHEMA_V1,
151 ),
152 (
153 "json-schema/query-response-v1.schema.json",
154 QUERY_RESPONSE_SCHEMA_V1,
155 ),
156 ("json-schema/proof-v1.schema.json", PROOF_SCHEMA_V1),
157 ] {
158 assert_eq!(
159 fs::read_to_string(canonical_root.join(relative))?,
160 packaged,
161 "packaged contract asset differs from contracts/{relative}"
162 );
163 }
164 Ok(())
165 }
166
167 #[test]
168 fn json_schemas_use_draft_2020_12() -> Result<(), Box<dyn Error>> {
169 for schema in [
170 CAPABILITIES_SCHEMA_V1,
171 ERROR_SCHEMA_V1,
172 HEALTH_SCHEMA_V1,
173 PUT_REQUEST_SCHEMA_V1,
174 DELETE_REQUEST_SCHEMA_V1,
175 GET_REQUEST_SCHEMA_V1,
176 GET_RESPONSE_SCHEMA_V1,
177 COMMIT_RECEIPT_SCHEMA_V1,
178 QUERY_REQUEST_SCHEMA_V1,
179 QUERY_RESPONSE_SCHEMA_V1,
180 PROOF_SCHEMA_V1,
181 ] {
182 let document: JsonValue = serde_json::from_str(schema)?;
183 assert_eq!(
184 document.get("$schema").and_then(JsonValue::as_str),
185 Some("https://json-schema.org/draft/2020-12/schema")
186 );
187 }
188 Ok(())
189 }
190
191 #[test]
192 fn checked_in_json_schemas_match_rust_wire_models() -> Result<(), Box<dyn Error>> {
193 assert_schema::<CapabilitiesV1>(CAPABILITIES_SCHEMA_V1)?;
194 assert_schema::<ErrorV1>(ERROR_SCHEMA_V1)?;
195 assert_schema::<HealthV1>(HEALTH_SCHEMA_V1)?;
196 assert_schema::<PutRequestV1>(PUT_REQUEST_SCHEMA_V1)?;
197 assert_schema::<DeleteRequestV1>(DELETE_REQUEST_SCHEMA_V1)?;
198 assert_schema::<GetRequestV1>(GET_REQUEST_SCHEMA_V1)?;
199 assert_schema::<GetResponseV1>(GET_RESPONSE_SCHEMA_V1)?;
200 assert_schema::<CommitReceiptV1>(COMMIT_RECEIPT_SCHEMA_V1)?;
201 assert_schema::<QueryRequestV1>(QUERY_REQUEST_SCHEMA_V1)?;
202 assert_schema::<QueryResponseV1>(QUERY_RESPONSE_SCHEMA_V1)?;
203 assert_schema::<ProofV1>(PROOF_SCHEMA_V1)?;
204 Ok(())
205 }
206
207 fn assert_schema<T: JsonSchema>(checked_in: &str) -> Result<(), Box<dyn Error>> {
208 let generated = SchemaGenerator::default().into_root_schema_for::<T>();
209 let checked_in: JsonValue = serde_json::from_str(checked_in)?;
210 assert_eq!(serde_json::to_value(generated)?, checked_in);
211 Ok(())
212 }
213
214 fn validate_external_schema_refs(value: &YamlValue, base: &Path) -> Result<(), Box<dyn Error>> {
215 match value {
216 YamlValue::Mapping(mapping) => {
217 if let Some(reference) = mapping
218 .get(YamlValue::String("$ref".to_owned()))
219 .and_then(YamlValue::as_str)
220 .filter(|reference| reference.starts_with("../json-schema/"))
221 {
222 let encoded = fs::read_to_string(base.join(reference))?;
223 let _schema: JsonValue = serde_json::from_str(&encoded)?;
224 }
225 for (key, value) in mapping {
226 validate_external_schema_refs(key, base)?;
227 validate_external_schema_refs(value, base)?;
228 }
229 }
230 YamlValue::Sequence(values) => {
231 for value in values {
232 validate_external_schema_refs(value, base)?;
233 }
234 }
235 YamlValue::Tagged(value) => validate_external_schema_refs(&value.value, base)?,
236 YamlValue::Null | YamlValue::Bool(_) | YamlValue::Number(_) | YamlValue::String(_) => {}
237 }
238 Ok(())
239 }
240}