use std::{fs, path::Path};
use typify::{TypeSpace, TypeSpaceSettings};
const SCHEMA_URL: &'static str = "https://raw.githubusercontent.com/versa-protocol/schema";
const SCHEMA_PATH: &'static str = "data";
const SCHEMAS: [&'static str; 2] = ["receipt", "itinerary"];
const LATEST_SCHEMA_VERSION: &'static str = "2.1.1";
#[rustfmt::skip]
const LTS_SCHEMA_VERSIONS: [&'static str; 4] = [
"1.11.0",
"2.0.0",
"2.1.0",
LATEST_SCHEMA_VERSION
];
fn get_schema_definition_json(schema_name: &str, version: &str) -> Result<String, ()> {
let schema_url = format!(
"{}/{}/{}/{}.schema.json",
SCHEMA_URL, version, SCHEMA_PATH, schema_name
);
match ureq::get(&schema_url).call() {
Ok(mut res) => {
let response_status = res.status();
if response_status != ureq::http::StatusCode::OK {
println!(
"Error fetching schema version {}: {}",
version, response_status
);
return Err(());
}
return match res.body_mut().read_to_string() {
Ok(text) => Ok(text),
Err(e) => {
println!("Error fetching schema version {}: {:?}", version, e);
Err(())
}
};
}
Err(e) => {
println!("Error fetching schema version {}: {:?}", version, e);
return Err(());
}
}
}
fn generate_schema_bindings(schema_name: &str, version: &str, output_dir: &Path) {
let Ok(content) = get_schema_definition_json(schema_name, version) else {
return;
};
let schema = serde_json::from_str::<schemars::schema::RootSchema>(&content).unwrap();
let mut type_space = TypeSpace::new(TypeSpaceSettings::default().with_struct_builder(true));
type_space.add_root_schema(schema).unwrap();
let contents = prettyplease::unparse(&syn::parse2::<syn::File>(type_space.to_stream()).unwrap())
.replace("chrono::naive::NaiveDate", "String");
let mut out_file = output_dir.to_path_buf();
out_file.push(format!("{}.rs", schema_name));
fs::write(out_file, contents).unwrap();
}
fn strip_additional_properties(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(map) => {
map.remove("additionalProperties");
for v in map.values_mut() {
strip_additional_properties(v);
}
}
serde_json::Value::Array(arr) => {
for v in arr {
strip_additional_properties(v);
}
}
_ => {}
}
}
fn generate_tolerant_schema_bindings(schema_name: &str, version: &str, output_dir: &Path) {
let Ok(content) = get_schema_definition_json(schema_name, version) else {
return;
};
let mut val = serde_json::from_str::<serde_json::Value>(&content).unwrap();
strip_additional_properties(&mut val);
let stripped_schema = serde_json::to_string(&val).unwrap();
let schema = serde_json::from_str::<schemars::schema::RootSchema>(&stripped_schema).unwrap();
let mut type_space = TypeSpace::new(TypeSpaceSettings::default().with_struct_builder(true));
type_space.add_root_schema(schema).unwrap();
let contents = prettyplease::unparse(&syn::parse2::<syn::File>(type_space.to_stream()).unwrap())
.replace("chrono::naive::NaiveDate", "String");
let mut out_file = output_dir.to_path_buf();
out_file.push(format!("{}_unstrict.rs", schema_name));
fs::write(out_file, contents).unwrap();
}
fn bundle_schema_definition(schema: &str, version: &str) {
let Ok(content) = get_schema_definition_json(schema, version) else {
return;
};
let safe_version = format!("{}", version.replace('.', "_").replace('-', "_"));
let safe_version_name = format!("v{}", safe_version);
let mut out_file = Path::new("src/schema/json").to_path_buf();
out_file.push(format!("{}_{}.json", schema, safe_version_name));
fs::write(out_file, content).unwrap();
println!(
"Bundled schema definition for {} version {}",
schema, version
);
}
fn bundle_schema_definitions(schema: &str) {
for version in LTS_SCHEMA_VERSIONS {
bundle_schema_definition(schema, version);
}
}
fn generate_version_mod_file(version: &str) {
let safe_version = version.replace('.', "_").replace('-', "_");
let version_dir = Path::new("src/schema").join(format!("v{}", safe_version));
let mod_content = format!(
r#"//! Schema bindings for version {}
pub mod itinerary;
pub mod itinerary_unstrict;
pub mod receipt;
pub mod receipt_unstrict;
"#,
version
);
fs::write(version_dir.join("mod.rs"), mod_content).unwrap();
}
fn output_current_schema_version() {
let mut out_file = Path::new("src/schema").to_path_buf();
out_file.push("current.rs");
fs::write(
out_file,
format!(
r#"//! Current schema version constant
pub const SCHEMA_VERSION: &str = "{}";
"#,
LATEST_SCHEMA_VERSION
),
)
.unwrap();
}
fn update_main_mod_file() {
let escaped_latest_version = LATEST_SCHEMA_VERSION.replace('.', "_").replace('-', "_");
let mut mod_content = format!(
r#"//! Schema module containing type definitions for all LTS versions
// NOTE: This file is auto-generated by codegen.rs; DO NOT EDIT THIS FILE DIRECTLY
pub mod current;
pub mod downshift;
// Re-export latest version as the default
pub use v{escaped_latest_version}::*;
// Version-specific modules
"#,
);
for version in LTS_SCHEMA_VERSIONS {
let safe_version = version.replace('.', "_").replace('-', "_");
mod_content.push_str(&format!("pub mod v{};\n", safe_version));
}
mod_content.push_str(
r#"
#[cfg(feature = "validator")]
mod json;
#[cfg(feature = "validator")]
pub mod validator;
"#,
);
fs::write(Path::new("src/schema/mod.rs"), mod_content).unwrap();
}
fn main() {
fs::create_dir_all("src/schema").unwrap();
let json_dir = Path::new("src/schema/json");
if json_dir.exists() {
for entry in fs::read_dir(json_dir).unwrap() {
let entry = entry.unwrap();
if entry
.path()
.extension()
.map(|s| s == "json")
.unwrap_or(false)
{
fs::remove_file(entry.path()).unwrap();
}
}
} else {
fs::create_dir_all(json_dir).unwrap();
}
output_current_schema_version();
for version in LTS_SCHEMA_VERSIONS {
let safe_version = version.replace('.', "_").replace('-', "_");
let version_dir = Path::new("src/schema").join(format!("v{}", safe_version));
fs::create_dir_all(&version_dir).unwrap();
println!("Generating bindings for version {}", version);
for schema in SCHEMAS.iter() {
generate_schema_bindings(schema, version, &version_dir);
generate_tolerant_schema_bindings(schema, version, &version_dir);
}
generate_version_mod_file(version);
}
println!("Generating latest version bindings in root schema directory");
let schema_dir = Path::new("src/schema");
for schema in SCHEMAS.iter() {
generate_schema_bindings(schema, LATEST_SCHEMA_VERSION, schema_dir);
generate_tolerant_schema_bindings(schema, LATEST_SCHEMA_VERSION, schema_dir);
}
for schema in SCHEMAS.iter() {
bundle_schema_definitions(schema);
}
update_main_mod_file();
println!("Schema generation complete!");
}