schema_oxidation 0.1.0

Produce Rust data structures from JSON schemas.
Documentation
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use std::collections::HashSet;

fn generate_object(
    id: &str,
    pre: &mut TokenStream2,
    definition: &serde_json::Value,
) -> TokenStream2 {
    let ident = quote::format_ident!("{}", id);

    let mut current_struct = TokenStream2::new();

    let defn = definition.as_object().unwrap();

    if defn.contains_key("$ref") {
        todo!("References NYI!");
        // need to return ref as struct name
    }

    if defn.contains_key("patternProperties") {
        let patterns = defn["patternProperties"].as_object().unwrap();
        for pattern in patterns {
            if pattern.0 != ".*" {
                panic!("Only support \".*\" patterns, not {}!", pattern.0);
            }
            let ty = single_type(id, pre, pattern.1);
            return quote! {
                std::collections::HashMap<String, #ty>
            };
        }
    }

    let extra_are_errors = false;

    let mut required: HashSet<String> = HashSet::new();

    if defn.contains_key("requiredProperties") {
        for prop in defn["requiredProperties"].as_array().unwrap() {
            required.insert(prop.as_str().unwrap().to_string());
        }
    }

    if defn.contains_key("properties") {
        for prop in defn["properties"].as_object().unwrap() {
            let prop_name = quote::format_ident!("{}", prop.0);
            let prop_type = single_type(format!("{}_{}", id, prop.0).as_str(), pre, prop.1);

            if required.contains(prop.0) {
                current_struct.extend(quote! {
                    pub #prop_name : #prop_type,
                })
            } else {
                current_struct.extend(quote! {
                    pub #prop_name : Option<#prop_type>,
                })
            }
        }
    }

    let extra = if extra_are_errors {
        quote! {}
    } else {
        quote! {
            #[serde(flatten)]
            additional: std::collections::HashMap<String, serde_json::Value>,
        }
    };

    pre.extend(quote! {
        #[derive(Deserialize,Serialize,Debug)]
        pub struct #ident {
            #current_struct
            #extra
        }
    });

    quote! {#ident}
}

fn single_def(id: &str, pre: &mut TokenStream2, definition: &serde_json::Value) -> TokenStream2 {
    let defn = definition.as_object().unwrap();
    let ident = quote::format_ident!("{}", id);

    let element_type = defn["type"].as_str().unwrap();

    match element_type {
        "string" => {
            quote! { pub type #ident = String; }
        }
        "int" => {
            quote! { pub type #ident = i64; }
        }
        "object" => {
            // this is a definition, we don't need the resulting name
            generate_object(id, pre, definition);
            TokenStream2::new()
        }
        _ => {
            panic!("Unknown object type {}!", element_type);
        }
    }
}

fn single_type(id: &str, pre: &mut TokenStream2, definition: &serde_json::Value) -> TokenStream2 {
    let defn = definition.as_object().unwrap();

    let element_type = defn["type"].as_str().unwrap();

    match element_type {
        "string" => {
            quote! { String }
        }
        "int" => {
            quote! { i64 }
        }
        "object" => {
            let objname = generate_object(id, pre, definition);
            quote! { #objname }
        }
        _ => {
            panic!("Unknown object type {}!", element_type);
        }
    }
}

pub fn generate_schema(all_data: &serde_json::Value) -> TokenStream2 {
    let mut pre = TokenStream2::new();
    let mut schema_defs = crate::schema::single_def("SchemaRoot", &mut pre, all_data);

    let all_obj = all_data.as_object().unwrap();

    if all_obj.contains_key("$defs") {
        for defn in all_obj["$defs"].as_object().unwrap() {
            schema_defs.extend(crate::schema::single_def(defn.0, &mut pre, defn.1));
        }
    };

    quote! { #pre #schema_defs }
}