Skip to main content

hyphae_contracts/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Canonical public contract documents embedded for validation and generation.
4
5pub mod v1;
6
7/// `OpenAPI` 3.1 document for HTTP API version 1.
8pub const OPENAPI_V1: &str = include_str!("../assets/openapi/hyphae-v1.yaml");
9
10/// JSON Schema for the version 1 capability response.
11pub const CAPABILITIES_SCHEMA_V1: &str =
12    include_str!("../assets/json-schema/capabilities-v1.schema.json");
13
14/// JSON Schema for the version 1 error envelope.
15pub const ERROR_SCHEMA_V1: &str = include_str!("../assets/json-schema/error-v1.schema.json");
16
17/// JSON Schema for version 1 liveness and readiness responses.
18pub const HEALTH_SCHEMA_V1: &str = include_str!("../assets/json-schema/health-v1.schema.json");
19
20/// JSON Schema for version 1 atomic put requests.
21pub const PUT_REQUEST_SCHEMA_V1: &str =
22    include_str!("../assets/json-schema/put-request-v1.schema.json");
23
24/// JSON Schema for version 1 atomic delete requests.
25pub const DELETE_REQUEST_SCHEMA_V1: &str =
26    include_str!("../assets/json-schema/delete-request-v1.schema.json");
27
28/// JSON Schema for version 1 exact-get requests.
29pub const GET_REQUEST_SCHEMA_V1: &str =
30    include_str!("../assets/json-schema/get-request-v1.schema.json");
31
32/// JSON Schema for version 1 proof-bearing get responses.
33pub const GET_RESPONSE_SCHEMA_V1: &str =
34    include_str!("../assets/json-schema/get-response-v1.schema.json");
35
36/// JSON Schema for version 1 durable commit receipts.
37pub const COMMIT_RECEIPT_SCHEMA_V1: &str =
38    include_str!("../assets/json-schema/commit-receipt-v1.schema.json");
39
40/// JSON Schema for version 1 structured query requests.
41pub const QUERY_REQUEST_SCHEMA_V1: &str =
42    include_str!("../assets/json-schema/query-request-v1.schema.json");
43
44/// JSON Schema for version 1 proof-bearing query responses.
45pub const QUERY_RESPONSE_SCHEMA_V1: &str =
46    include_str!("../assets/json-schema/query-response-v1.schema.json");
47
48/// JSON Schema for version 1 result-proof transport.
49pub 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}