graphql-engine/v3/crates/utils/opendds-derive
Rakesh Emmadi 8844fab8d3 opendd-derive: Support multiple JSON schema examples (#1229)
<!-- The PR description should answer 2 important questions: -->

### What

<!-- What is this PR trying to accomplish (and why, if it's not
obvious)? -->

<!-- Consider: do we need to add a changelog entry? -->

<!-- Does this PR introduce new validation that might break old builds?
-->

<!-- Consider: do we need to put new checks behind a flag? -->
The `example` JSON schema attribute can be specified multiple times
enabling inclusion of multiple examples. The OpenDd metadata is
auto-documented in hasura.io/docs using the json schema. Having multiple
examples is quite helpful for our users.

Inspired from the `schemars`'s `example` attribute behavior.

### How

<!-- How is it trying to accomplish it (what are the implementation
steps)? -->
- Use `#[darling(multiple)]` attr to allow the `example` attribute
multiple times.
- Add more examples for model predicates and permission.

V3_GIT_ORIGIN_REV_ID: 7e9c31891afed5b96ec8a5bb7538062382ef4d27
2024-10-16 17:22:09 +00:00
..
src opendd-derive: Support multiple JSON schema examples (#1229) 2024-10-16 17:22:09 +00:00
Cargo.toml Replace lazy_static with stdlib equivalents. (#758) 2024-06-26 12:45:41 +00:00
README.md Add polling interval config for subscription GraphQL OpenDD metadata (#1129) 2024-09-20 12:16:29 +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(default = <value>)]

    If the value is not present when deserializing, use the value provided.

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

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

  • [opendd(hidden = true)]

    Hide the field from the json schema, useful for keeping work in progress out of the public API.

  • #[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()).

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

    Set the generated JSON schema's title.

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::VariantNames 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"
    }
    
  • #[opendd(externally_tagged)]

    The JSON object is externally tagged, where the name of the variant is used as the property name and the variant contents is that property's value.

    Example:

    #[derive(OpenDd)]
    #[opendd(externally_tagged)]
    enum ExternallyTaggedEnum {
        VariantOne(VariantOneStruct),
        VariantTwo(VariantTwoStruct),
    }
    
    #[derive(Debug, PartialEq, OpenDd)]
    struct VariantOneStruct {
        prop_a: String,
        prop_b: i32,
    }
    
    #[derive(Debug, PartialEq, OpenDd)]
    struct VariantTwoStruct {
        prop_1: bool,
        prop_2: String,
    }
    

    The following JSON object is parsed into ExternallyTaggedEnum::VariantTwo(VariantTwoStruct { prop_1: true, prop_2: "testing" }):

    {
      "variantTwo": {
        "prop1": true,
        "prop2": "testing"
      }
    }
    

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.

  • [opendd(hidden = true)]

    Hide this variant from the json schema, useful for keeping work in progress out of the public API.

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

    Set the generated JSON schema's title. This applies exclusively to enums with the as_versioned_with_definition and externally_tagged attribute.

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

    Include the result of the given function in the generated JSON schema's examples. This applies exclusively to enums with the as_versioned_with_definition attribute.

Common JSON Schema attributes

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

  • #[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(id = "json schema id"))]

    Set the generated schema's $id.

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

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

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