graphql-engine/v3/crates/opendds-derive
Samir Talwar c7d9baaf66 Use references where possible instead of cloning. (#478)
When trying to reduce the number of dependencies we use in the engine, I
was blocked by a few `.clone()` calls that, on inspection, turned out to
be completely unnecessary.

I have replaced those with passing by reference, and then gone on a
pedant spree. I enabled the `needless_pass_by_value` Clippy warning and
fixed it everywhere that it highlighted. In most places, this meant
adding `&`, but I also marked some types as `Copy`, which makes
pass-by-value the right move.

In one place, I replaced calls to `async_map` with `if` and `else`, to
avoid constructing closures that capture across async boundaries. This
means I could just delete `async_map`.

V3_GIT_ORIGIN_REV_ID: 6ff71f0c553b707889d89552eff3e8c001e898cc
2024-04-18 17:35:48 +00:00
..
src Use references where possible instead of cloning. (#478) 2024-04-18 17:35:48 +00:00
Cargo.toml use clippy settings in Cargo workspace (#441) 2024-04-08 10:14:11 +00:00
README.md Move all crates into a folder (#355) 2024-03-19 18:07:14 +00:00

Table Of Contents

Derive Macros for OpenDd Trait

This crate provides a derive macro for implementing the OpenDd trait from the open-dds crate. To utilize it, simply add opendds-derive as a dependency in your Cargo.toml file, alongside the open-ddscrate. Then, include opendds_derive::OpenDd in the #[derive(..)] attributes list. Please note that at present, only struct and enum types are supported for derivation. Use #[opendd()] to specify type level and field or variant level attributes

The Trait

The OpenDd trait is defined as follows,

pub trait OpenDd: Sized {
    fn deserialize(json: serde_json::Value) -> Result<Self, OpenDdDeserializeError>;

    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema;

    fn _schema_name() -> String;

    fn _schema_is_referenceable() -> bool {
        false
    }
}

All types used to express the open data domain specification need to have the OpenDd trait implemented. At present, the OpenDd trait exposes methods for deserializing the type from a JSON value and generating a json schema for the type.

Struct

Limitations:

  • Only structs with named fields and single unnamed field (newtype) are allowed to use the derive macro
#[derive(opendds_derive::OpenDd)]
struct NamedFieldStruct {
  named_field_1: Type1,
  named_field_2: Type2,
}

#[derive(opendds_derive::OpenDd)]
struct NewTypeStruct(Field);
  • Structs with multiple unnamed fields or no fields are not supported.
struct UnitStruct;
struct UnnamedFieldStruct(Type1, Type2);

Common Behavior:

  • All fields of struct are deserialized with their camel-cased fields in the JSON object.
  • Having unknown fields in the object results in parse error.

Attributes

Type Level

Only json schema related attributes applicable here.

Field Level

  • #[opendd(default)]

    If the value is not present when deserializing, use the Default::default().

  • #[opendd(rename = "name")]

    Deserialize this field with the given name instead of camel-cased Rust field name.

Enum

Limitations:

  • All variants should carry exactly one unnamed field.
#[derive(opendds_derive::OpenDd)]
enum MyEnum {
    VariantOne(TypeOne),
    VariantTwo(TypeTwo),
}
  • Variants with no fields, multiple unnamed fields or named fields are not supported
enum Unsupported {
    NoFields,
    MultipleUnnamed(TypeOne, TypeTwo),
    Named{field_1: TypeOne, field_1: TypeTwo},
}

Attributes

Type Level

Enum types are deserialized from JSON objects using a specific key-value pair. The key, also referred to as the tag, must be a String value. It determines the variant of the enum to deserialize from the rest of the JSON object's content. The following type-level attributes provide various ways for deserializing the JSON object along with json schema related attributes.

  • #[opendd(as_versioned_internally_tagged)]

    Using version key as tag. The tag value is matched with camel-cased variant name. Rest of the object's content is deserialized as the variant value.

    Example:

    #[derive(opendds_derive::OpenDd)]
    #[opendd(as_versioned_internally_tagged)]
    enum VersionedEnum {
        V1(VersionOne),
        V2(VersionTwo),
    }
    
    #[derive(opendds_derive::OpenDd)]
    struct VersionOne {
        #[opendd(use_serde_json)]
        field_one: String
    }
    

    The following object is parsed into V1(VersionOne)

    {
        "version": "v1",
        "fieldOne": "some_value"
    }
    
  • #[opendd(as_versioned_with_definition)]

    Using version key as tag. The tag value is matched with camel-cased variant name. The variant value is deserialized with the definition key value from the object.

    Example:

    #[derive(opendds_derive::OpenDd)]
    #[opendd(as_versioned_with_definition)]
    enum VersionedEnum {
        V1(VersionOne),
        V2(VersionTwo),
    }
    
    #[derive(opendds_derive::OpenDd)]
    struct VersionTwo {
        #[opendd(use_serde_json)]
        field_two: String
    }
    

    The following object is parsed into V2(VersionTwo)

    {
        "version": "v2",
        "definition": {
            "fieldTwo": "some_value"
        }
    }
    
  • #[opendd(as_kind)]

    Using kind key as tag. The tag value is matched with the exact variant name. Rest of the object's content is deserialized as the variant value.

    Example:

    #[derive(opendds_derive::OpenDd)]
    #[opendd(as_kind)]
    enum KindEnum {
        KindOne(KindOneStruct),
        KindTwo(KindTwoStruct),
    }
    
    #[derive(opendds_derive::OpenDd)]
    #[opendd(use_serde_json)] // All field values are deserialized using serde_path_to_error::deserialize()
    struct KindOneStruct{
        field_one: i32,
        field_two: bool,
        field_three: String,
    }
    

    The following object is parsed into KindOne(KindOneStruct)

    {
        "kind": "KindOne",
        "fieldOne": 111,
        "fieldTwo": false,
        "fieldThree": "three"
    }
    
  • #[opendd(untagged_with_kind)]

    The JSON object is not tagged with any variant. Each variant should hold a enum type with #[opendd(as_kind)] (see above) implementation. Using kind as tag and its value is matched with internal enum variants. The internal enum variants need to have strum_macros::EnumVariantNames implementation.

    Example:

    #[derive(opendds_derive::OpenDd)]
    #[opendd(as_kind)]
    enum KindEnumOne {
        VariantOne(OneStruct),
        VaraintTwo(TwoStruct),
    }
    
    #[derive(opendds_derive::OpenDd)]
    #[opendd(as_kind)]
    enum KindEnumTwo {
        VariantThree(ThreeStruct),
        VaraintFour(FourStruct)
    }
    
    #[derive(opendds_derive::OpenDd)]
    #[opendd(untagged_with_kind)]
    enum UntaggedEnum {
        KindOne(KindEnumOne),
        KindTwo(KindEnumTwo),
    }
    
    #[derive(opendds_derive::OpenDd)]
    struct FourStruct {
        #[opendd(use_serde_json)]
        field_four: String,
    }
    

    The following object is parsed into UntaggedEnum::KindTwo(KindEnumTwo::VariantFour(FourStruct{field_four: "four"}))

    {
        "kind": "VariantFour",
        "fieldFour": "four"
    }
    

Variant Level

  • #[opendd(rename = "name")]

    Deserialize this variant with the given name and use it to generate enum value for the tag in the json schema.

  • #[opendd(alias = "name")]

    Deserialize this variant from the given name or from derived Rust name.

Common JSON Schema attributes

The following json schema related attributes are applicable for both structs and enums.

Type Level

  • #[opendd(json_schema(rename = "rename string value"))]

    Use the given name in the generated schema instead of the Rust name.

  • #[opendd(json_schema(title = "title string value"))]

    Set the generated schema's title.

  • #[opendd(json_schema(example = "some::function"))]

    Include the result of the given function in the generated schema's examples.

Field Level

  • #[opendd(json_schema(default_exp = "some::function()"))]

    To be used in conjuction with #[opendd(default)]. The given function should return a json value which is included in the generated schema's default. Not needed when the field type has serde::Serialize trait implemented. The default JSON value will be inferred using serde_json::json!(Default::default()).

Notes

  • Please make sure the following crates/modules are accessible in the module where the derive macro is used.
    • opendds - as module or crate with derive in the path. For eg. the macro refers the trait with opendds::derive::OpenDd
    • strum - to access strum::VariantNames