use std::{collections::HashMap, fs, path::Path};
use serde_json::{Value, json};
use typify::{CrateVers, TypeSpace, TypeSpaceImpl, TypeSpaceSettings};
use crate::utils::{annotate_decimal_types, merge_definitions, normalize_schema, write_if_changed};
struct FieldDecimalPatch {
model_name: &'static str,
fields: &'static [&'static str],
}
pub fn generate() {
println!("cargo:warning=Generating unified models from all specs");
let output_path = "src/models/models.rs";
let models_to_skip: [&str; 0] = [];
let models_to_rename = [("ERC20Details", "Erc20CompleteDetails")];
let models_to_patch_to_decimal: Vec<FieldDecimalPatch> = vec![FieldDecimalPatch {
model_name: "Instrument",
fields: &["tick_size", "minimum_amount", "maximum_amount"],
}];
let mut all_definitions: HashMap<String, Value> = HashMap::new();
extract_openapi_definitions(&mut all_definitions, &models_to_skip);
patch_openapi_definitions(&mut all_definitions);
extract_asyncapi_definitions(
&mut all_definitions,
"schemas/ws_asyncapi_rpc.json",
"AsyncAPI RPC",
&models_to_skip,
);
extract_asyncapi_definitions(
&mut all_definitions,
"schemas/ws_asyncapi_subscriptions.json",
"AsyncAPI Subscriptions",
&models_to_skip,
);
println!(
"cargo:warning=Total unique models: {}",
all_definitions.len()
);
let mut definitions = Value::Object(all_definitions.into_iter().collect());
rename_model_definitions(&mut definitions, &models_to_rename);
rewrite_refs(&mut definitions, &models_to_rename);
patch_specific_models(&mut definitions, &models_to_patch_to_decimal);
annotate_decimal_types(&mut definitions);
let root_schema = serde_json::from_value(json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": definitions,
}))
.expect("failed to convert definitions into a root schema");
let mut settings = TypeSpaceSettings::default();
settings
.with_struct_builder(true)
.with_crate(
"bigdecimal",
CrateVers::parse("0.4.10").expect("invalid BigDecimal crate version"),
None,
)
.with_replacement(
"TickerSlimSnapshot",
"crate::models::ticker_slim_schema::TickerSlimSchema",
std::iter::empty::<TypeSpaceImpl>(),
);
let mut type_space = TypeSpace::new(&settings);
type_space
.add_root_schema(root_schema)
.expect("Typify failed to process the root schema");
let syntax = syn::parse2::<syn::File>(type_space.to_stream())
.expect("Typify produced invalid Rust syntax");
let contents = prettyplease::unparse(&syntax);
let headers = [
"// This file is generated from multiple API specs.",
"// Do not edit manually.",
"",
"#![allow(clippy::derivable_impls)]",
"#![allow(clippy::type_complexity)]",
"#![allow(clippy::should_implement_trait)]",
];
let header = headers.join("\n");
let output = format!("{header}\n\n{contents}");
write_if_changed(Path::new(output_path), &output);
println!("cargo:warning=Successfully generated unified models at {output_path}");
}
fn extract_openapi_definitions(
all_definitions: &mut HashMap<String, Value>,
models_to_skip: &[&str],
) {
let path = "schemas/openapi.json";
println!("cargo:rerun-if-changed={path}");
let content =
fs::read_to_string(path).unwrap_or_else(|error| panic!("failed to read {path}: {error}"));
let mut openapi: Value = serde_json::from_str(&content)
.unwrap_or_else(|error| panic!("failed to parse {path}: {error}"));
let schemas = openapi
.pointer_mut("/components/schemas")
.unwrap_or_else(|| panic!("{path} does not contain components.schemas"))
.take();
let mut schemas_obj = schemas
.as_object()
.expect("schemas must be an object")
.clone();
for schema in schemas_obj.values_mut() {
normalize_schema(schema);
}
merge_definitions(
all_definitions,
Value::Object(schemas_obj),
"OpenAPI",
models_to_skip,
);
println!(
"cargo:warning=Extracted {} definitions from OpenAPI",
all_definitions.len()
);
}
fn extract_asyncapi_definitions(
all_definitions: &mut HashMap<String, Value>,
source_path: &str,
source_name: &str,
models_to_skip: &[&str],
) {
println!("cargo:rerun-if-changed={source_path}");
let source = fs::read_to_string(source_path)
.unwrap_or_else(|error| panic!("Unable to read {source_path}: {error}"));
let document: Value = serde_json::from_str(&source)
.unwrap_or_else(|error| panic!("Unable to parse {source_path}: {error}"));
let schemas = document
.pointer("/components/schemas")
.and_then(Value::as_object)
.expect("Unable to extract schemas from AsyncAPI document");
let messages = document
.pointer("/components/messages")
.and_then(Value::as_object)
.expect("Unable to extract messages from AsyncAPI document");
let mut schemas_obj = schemas.clone();
for schema in schemas_obj.values_mut() {
normalize_schema(schema);
}
let schemas_value = Value::Object(schemas_obj);
let count_before = all_definitions.len();
merge_definitions(all_definitions, schemas_value, source_name, models_to_skip);
for (message_name, message) in messages {
if models_to_skip.contains(&message_name.as_str()) {
println!("Skipping message: {message_name}");
continue;
}
let mut payload = message
.get("payload")
.cloned()
.unwrap_or_else(|| panic!("message `{message_name}` in {source_name} has no payload"));
normalize_schema(&mut payload);
if let Some(existing) = all_definitions.get(message_name) {
if existing != &payload {
panic!(
"conflicting schema definition for message {message_name:?} in {source_name}:\n\
existing definition:\n{}\n\
conflicting definition:\n{}",
serde_json::to_string_pretty(existing).unwrap(),
serde_json::to_string_pretty(&payload).unwrap(),
);
}
} else {
all_definitions.insert(message_name.clone(), payload);
}
}
let count_after = all_definitions.len();
println!(
"cargo:warning=Extracted {} new definitions from {source_name} ({} schemas, {} messages)",
count_after - count_before,
schemas.len(),
messages.len()
);
}
fn rename_model_definitions(schemas: &mut Value, models_to_rename: &[(&str, &str)]) {
let definitions = schemas
.as_object_mut()
.expect("definitions must be an object");
for &(old_name, new_name) in models_to_rename {
if old_name == new_name {
continue;
}
let Some(schema) = definitions.remove(old_name) else {
continue;
};
match definitions.entry(new_name.to_owned()) {
serde_json::map::Entry::Vacant(entry) => {
println!("cargo:warning=Renaming model: {old_name} -> {new_name}");
entry.insert(schema);
}
serde_json::map::Entry::Occupied(entry) if entry.get() == &schema => {
}
serde_json::map::Entry::Occupied(entry) => {
panic!(
"cannot rename schema {old_name:?} to {new_name:?}: \
target name already contains a different definition\n\
existing definition:\n{}\n\
incoming definition:\n{}",
serde_json::to_string_pretty(entry.get()).unwrap(),
serde_json::to_string_pretty(&schema).unwrap(),
);
}
}
}
}
fn rewrite_refs(value: &mut Value, models_to_rename: &[(&str, &str)]) {
match value {
Value::Object(object) => {
if let Some(Value::String(reference)) = object.get_mut("$ref") {
if let Some(name) = reference.strip_prefix("#/components/schemas/") {
let rewritten_name = models_to_rename
.iter()
.find_map(|(old_name, new_name)| (*old_name == name).then_some(*new_name))
.unwrap_or(name);
*reference = format!("#/definitions/{rewritten_name}");
}
else if let Some(name) = reference.strip_prefix("#/definitions/") {
let rewritten_name = models_to_rename
.iter()
.find_map(|(old_name, new_name)| (*old_name == name).then_some(*new_name))
.unwrap_or(name);
*reference = format!("#/definitions/{rewritten_name}");
}
}
for child in object.values_mut() {
rewrite_refs(child, models_to_rename);
}
}
Value::Array(values) => {
for child in values {
rewrite_refs(child, models_to_rename);
}
}
_ => {}
}
}
fn patch_openapi_definitions(all_definitions: &mut HashMap<String, Value>) {
if let Some(subaccount_schema) = all_definitions.get_mut("Subaccount")
&& let Some(required) = subaccount_schema.get_mut("required")
&& let Some(required_array) = required.as_array_mut()
{
required_array.retain(|item| item != "mm_credits");
}
}
fn patch_specific_models(
all_definitions: &mut serde_json::Value,
models_to_patch: &[FieldDecimalPatch],
) {
for patch in models_to_patch.iter() {
if let Some(model_schema) = all_definitions.get_mut(patch.model_name)
&& let Some(properties) = model_schema.get_mut("properties")
&& let Some(properties_obj) = properties.as_object_mut()
{
for field in patch.fields {
if let Some(field_schema) = properties_obj.get_mut(*field)
&& let Some(field_obj) = field_schema.as_object_mut()
{
field_obj.insert("format".to_string(), Value::String("decimal".to_string()));
}
}
}
}
}