magnet_schema 0.8.0

Magnet, a JSON/BSON schema generator
Documentation
//! "Runtime" support for `magnet_derive` -- quasi-private functions.

use bson::{ Bson, Document };

/// Describes a lower or upper bound.
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Bound {
    /// The range is not bounded.
    Unbounded,
    /// The range is bounded, the bound is in the range.
    Inclusive(f64),
    /// The range is bounded, the bound is not in the range.
    Exclusive(f64),
}

/// Describes both the lower and the upper bounds of a range.
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Bounds {
    /// The lower bound of the range.
    pub lower: Bound,
    /// The upper bound of the range.
    pub upper: Bound,
}

/// Based on bounds parsed from attributes, generates minimum and maximum
/// constraints and adds them to a JSON schema. Calls to this functions
/// are to be made from `magnet_derive`'d, generated code only.
#[doc(hidden)]
pub fn extend_schema_with_bounds(mut schema: Document, bounds: Bounds) -> Document {
    match bounds.lower {
        Bound::Unbounded => {},
        Bound::Inclusive(minimum) => {
            schema.insert("minimum", minimum);
            schema.insert("exclusiveMinimum", false);
        },
        Bound::Exclusive(minimum) => {
            schema.insert("minimum", minimum);
            schema.insert("exclusiveMinimum", true);
        },
    }

    match bounds.upper {
        Bound::Unbounded => {},
        Bound::Inclusive(maximum) => {
            schema.insert("maximum", maximum);
            schema.insert("exclusiveMaximum", false);
        },
        Bound::Exclusive(maximum) => {
            schema.insert("maximum", maximum);
            schema.insert("exclusiveMaximum", true);
        },
    }

    schema
}

/// This function should not be used directly; calls to it are only generated by
/// `magnet_derive` when emitting code for internally-tagged newtype variants.
///
/// If the newtype schema comes from a struct, just extend its "required"
/// and "properties" fields to include the tag and the variant name.
///
/// If the newtype schema comes from a map-like construct with dynamic keys,
/// just create the "required" and "properties" fields, with their single
/// element being the tag and the variant name.
///
/// If the newtype schema comes from an enum with supported structure,
/// e.g. all variants are structs or newtypes over structs, then recursively
/// flatten and check the `anyOf` / `oneOf` structure of the schema, and
/// add the tag and the variant name to each.
/// TODO(H2CO3): implement me --- this scenario is yet to be handled.
///
/// Every other case is considered an error.
#[doc(hidden)]
pub fn extend_schema_with_tag(schema: Document, tag: &str, variant: &str) -> Document {
    if schema_is_struct(&schema) {
        extend_struct_schema_with_tag(schema, tag, variant)
    } else if schema_is_map(&schema) {
        extend_map_schema_with_tag(schema, tag, variant)
    } else if schema_is_enum(&schema) {
        extend_enum_schema_with_tag(schema, tag, variant)
    } else {
        panic!("newtype variant doesn't describe a struct, a map, or an enum?!")
    }
}

/// Check if a schema describes a struct: an object with a fixed set of keys.
/// Note: we could check for `"type"` being an array containing `"object"`
/// as well, in case it's an `Option`, but internally-tagged newtype variants
/// around `Option` aren't supported by Serde anyway.
fn schema_is_struct(doc: &Document) -> bool {
    doc.get_str("type") == Ok("object")
    &&
    doc.get_document("properties").is_ok()
}

/// Check if a schema holds a dynamic set of keys.
/// Note: we could check for `"type"` being an array containing `"object"`
/// as well, in case it's an `Option`, but internally-tagged newtype variants
/// around `Option` aren't supported by Serde anyway.
fn schema_is_map(doc: &Document) -> bool {
    doc.get_str("type") == Ok("object")
    &&
    doc.get_document("additionalProperties").is_ok()
}

/// Check if a BSON schema describes an enum.
fn schema_is_enum(doc: &Document) -> bool {
    doc.get_array("anyOf").is_ok()
}

/// Extends a `struct`'s schema so that it describes an internally-tagged variant.
fn extend_struct_schema_with_tag(mut schema: Document, tag: &str, variant: &str) -> Document {
    let mut required = match schema.remove("required") {
        Some(Bson::Array(arr)) => arr,
        Some(_) => panic!("`required` is not an array in struct schema?!"),
        None => panic!("`required` key not found in struct schema?!"),
    };
    let mut properties = match schema.remove("properties") {
        Some(Bson::Document(doc)) => doc,
        Some(_) => panic!("`properties` is not a document in struct schema?!"),
        None => panic!("`properties` key not found in struct schema?!"),
    };

    // TODO(H2CO3): check for duplicate items and keys --
    // however, Serde should catch them too, shouldn't it?
    required.push(tag.into());
    properties.insert(tag, doc!{ "enum": [ variant ] });

    schema.insert("required", required);
    schema.insert("properties", properties);

    schema
}

/// Extends a map's schema so that it describes an internally-tagged variant.
fn extend_map_schema_with_tag(mut schema: Document, tag: &str, variant: &str) -> Document {
    // TODO(H2CO3): check for existence of the two following fields?
    schema.insert("required", vec![ tag.into() ]);
    schema.insert("properties", doc!{ tag: { "enum": [ variant ] } });

    schema
}

/// Extends an `enum`'s schema so that it describes an internally-tagged variant.
fn extend_enum_schema_with_tag(_schema: Document, _tag: &str, _variant: &str) -> Document {
    // TODO(H2CO3): recursively and transitively walk `anyOf` / `oneOf`
    // structure, until the leaves (struct or newtype-around-struct) are reached
    // or an error occurs (a non struct or newtype-around-struct type is found).
    unimplemented!("internally-tagged newtype variants around enums are not yet supported")
}