|
| 1 | +// This Source Code Form is subject to the terms of the Mozilla Public |
| 2 | +// License, v. 2.0. If a copy of the MPL was not distributed with this |
| 3 | +// file, You can obtain one at https://mozilla.org/MPL/2.0/. |
| 4 | + |
| 5 | +// Reads `rfd-api-spec.json` and emits `expected_response_types.rs` |
| 6 | +// containing a function that returns the schemars schema name for every JSON |
| 7 | +// response type the API can produce. The generated source references real |
| 8 | +// types from `rfd_sdk::types`, so a renamed/removed type breaks the build |
| 9 | +// instead of silently slipping past the dispatch coverage test. |
| 10 | + |
| 11 | +use serde_json::Value; |
| 12 | +use std::collections::BTreeSet; |
| 13 | +use std::env; |
| 14 | +use std::fs; |
| 15 | +use std::path::Path; |
| 16 | + |
| 17 | +fn main() { |
| 18 | + let spec_path = "../rfd-api-spec.json"; |
| 19 | + println!("cargo:rerun-if-changed=build.rs"); |
| 20 | + println!("cargo:rerun-if-changed={spec_path}"); |
| 21 | + |
| 22 | + let spec_text = |
| 23 | + fs::read_to_string(spec_path).unwrap_or_else(|e| panic!("read {spec_path}: {e}")); |
| 24 | + let spec: Value = serde_json::from_str(&spec_text).expect("parse spec JSON"); |
| 25 | + |
| 26 | + let entries = collect_entries(&spec); |
| 27 | + |
| 28 | + let mut src = String::from( |
| 29 | + "// AUTO-GENERATED by build.rs — do not edit.\n\ |
| 30 | + pub fn expected_response_schema_names() -> Vec<String> {\n vec![\n", |
| 31 | + ); |
| 32 | + for entry in &entries { |
| 33 | + src.push_str(" "); |
| 34 | + src.push_str(entry); |
| 35 | + src.push_str(",\n"); |
| 36 | + } |
| 37 | + src.push_str(" ]\n}\n"); |
| 38 | + |
| 39 | + let out_dir = env::var_os("OUT_DIR").expect("OUT_DIR set by cargo"); |
| 40 | + let dest = Path::new(&out_dir).join("expected_response_types.rs"); |
| 41 | + fs::write(&dest, src).expect("write expected_response_types.rs"); |
| 42 | +} |
| 43 | + |
| 44 | +fn collect_entries(spec: &Value) -> Vec<String> { |
| 45 | + let mut entries: BTreeSet<String> = BTreeSet::new(); |
| 46 | + let Some(paths) = spec.get("paths").and_then(Value::as_object) else { |
| 47 | + return Vec::new(); |
| 48 | + }; |
| 49 | + for ops in paths.values() { |
| 50 | + let Some(ops) = ops.as_object() else { continue }; |
| 51 | + for op in ops.values() { |
| 52 | + let Some(responses) = op.get("responses").and_then(Value::as_object) else { |
| 53 | + continue; |
| 54 | + }; |
| 55 | + for (status, resp) in responses { |
| 56 | + if !status.starts_with('2') { |
| 57 | + continue; |
| 58 | + } |
| 59 | + let Some(schema) = resp.pointer("/content/application~1json/schema") else { |
| 60 | + continue; |
| 61 | + }; |
| 62 | + if let Some(entry) = schema_to_entry(schema) { |
| 63 | + entries.insert(entry); |
| 64 | + } |
| 65 | + } |
| 66 | + } |
| 67 | + } |
| 68 | + entries.into_iter().collect() |
| 69 | +} |
| 70 | + |
| 71 | +fn schema_to_entry(schema: &Value) -> Option<String> { |
| 72 | + if let Some(r) = schema.get("$ref").and_then(Value::as_str) { |
| 73 | + let rust = strip_underscores(&ref_tail(r)); |
| 74 | + return Some(format!( |
| 75 | + "<rfd_sdk::types::{rust} as schemars::JsonSchema>::schema_name()" |
| 76 | + )); |
| 77 | + } |
| 78 | + if schema.get("type").and_then(Value::as_str) == Some("array") { |
| 79 | + if let Some(r) = schema.pointer("/items/$ref").and_then(Value::as_str) { |
| 80 | + let rust = strip_underscores(&ref_tail(r)); |
| 81 | + return Some(format!( |
| 82 | + "<Vec<rfd_sdk::types::{rust}> as schemars::JsonSchema>::schema_name()" |
| 83 | + )); |
| 84 | + } |
| 85 | + // Inline arrays without a `$ref` items target (e.g. `Vec<u8>`) have no |
| 86 | + // matching `rfd_sdk::types::*` to reference. The dispatch table is not |
| 87 | + // expected to cover them, so we skip rather than emit. |
| 88 | + } |
| 89 | + None |
| 90 | +} |
| 91 | + |
| 92 | +fn ref_tail(r: &str) -> String { |
| 93 | + r.rsplit('/').next().unwrap_or_default().to_string() |
| 94 | +} |
| 95 | + |
| 96 | +/// Translate an OpenAPI component name (`Foo_for_Bar_and_Baz`) to the Rust |
| 97 | +/// identifier progenitor emits (`FooForBarAndBaz`). |
| 98 | +fn strip_underscores(s: &str) -> String { |
| 99 | + let mut out = String::with_capacity(s.len()); |
| 100 | + let mut upper_next = false; |
| 101 | + for ch in s.chars() { |
| 102 | + if ch == '_' { |
| 103 | + upper_next = true; |
| 104 | + } else if upper_next { |
| 105 | + out.extend(ch.to_uppercase()); |
| 106 | + upper_next = false; |
| 107 | + } else { |
| 108 | + out.push(ch); |
| 109 | + } |
| 110 | + } |
| 111 | + out |
| 112 | +} |
0 commit comments