jsonschema-transpiler 2.0.1

A tool to transpile JSON Schema into schemas for data processing
#![recursion_limit = "128"]
#[macro_use]
extern crate log;
#[macro_use]
extern crate serde;
#[macro_use]
extern crate maplit;

mod ast;
mod avro;
mod bigquery;
pub mod casing;
mod jsonschema;
mod traits;

use regex::Regex;
use serde_json::{json, Value};
use traits::TranslateFrom;

/// The error resolution method in the [`TranslateFrom`] and [`TranslateInto`]
/// interfaces when converting between schema formats.
///
/// The `Cast` method will represent under-specified (e.g. empty objects) and
/// incompatible (e.g. variant-types or conflicting oneOf definitions) as
/// strings. This behavior is useful for compacting complex types into a single
/// column. In Spark and BigQuery, a casted column can be processed via a user
/// defined function that works on JSON. However, this method may cause issues
/// with schema evolution, for example when adding properties to empty objects.
///
/// The `Drop` method will drop fields if they do not fall neatly into one of
/// the supported types. This method ensures forward compatibility with schemas,
/// but it can lose large portions of nested data. Support from the data
/// processing side can recover dropped data from the structured section of the
/// schema.
///
/// The `Panic` method will panic if the JSON Schema is inconsistent or uses
/// unsupported features. This method is a useful way to test for incompatible
/// schemas.
#[derive(Copy, Clone, Default, Deserialize)]
pub enum ResolveMethod {
    #[default]
    Cast,
    Drop,
    Panic,
}

/// Options for modifying the behavior of translating between two schema
/// formats.
///
/// This structure passes context from the command-line interface into the
/// translation logic between the various schema types in the project. In
/// particular, the context is useful for resolving edge-cases in ambiguous
/// situations. This can includes situations like casting or dropping an empty
/// object.
#[derive(Clone, Default, Deserialize)]
#[serde(default)]
pub struct Context {
    pub resolve_method: ResolveMethod,
    pub normalize_case: bool,
    pub force_nullable: bool,
    pub tuple_struct: bool,
    pub allow_maps_without_value: bool,
    pub json_object_path_regex: Option<String>,
}

impl Context {
    /// Determine whether the given fully qualified name matches the configured json object path.
    fn is_json_object_path(&self, fqn: &str) -> bool {
        // would need to be passed in _somehow_.
        self.json_object_path_regex
            .as_ref()
            .map(|object_regex| {
                // Ensure we match from the beginning of the string
                let re = format!(r"\A{}", object_regex);
                let json_object_path_re = Regex::new(&re).unwrap();
                json_object_path_re.is_match(fqn)
            })
            .unwrap_or(false)
    }
}

fn into_ast(input: &Value, context: &mut Context) -> ast::Tag {
    let jsonschema: jsonschema::Tag = match serde_json::from_value(json!(input)) {
        Ok(tag) => tag,
        Err(e) => panic!("{:#?}", e),
    };

    // The only special thing this crates knows about the schema:
    // Every sub-tree id matching the regex in `mozPipelineMetadata.json_object_path_regex` is dumped as a JSON
    // column without peeking further into that subtree.
    let metadata = jsonschema
        .extra
        .get("mozPipelineMetadata")
        .and_then(|obj| obj["json_object_path_regex"].as_str());
    if let Some(json_object_path_regex) = metadata {
        context.json_object_path_regex = Some(json_object_path_regex.to_string());
    }

    ast::Tag::translate_from(jsonschema, context).unwrap()
}

/// Convert JSON Schema into an Avro compatible schema
pub fn convert_avro(input: &Value, mut context: Context) -> Value {
    let ast = into_ast(input, &mut context);
    let avro = avro::Type::translate_from(ast, &context).unwrap();
    json!(avro)
}

/// Convert JSON Schema into a BigQuery compatible schema
pub fn convert_bigquery(input: &Value, mut context: Context) -> Value {
    let ast = into_ast(input, &mut context);
    let bq = bigquery::Schema::translate_from(ast, &context).unwrap();
    json!(bq)
}