oxidecomputer / typify

compiler from JSON Schema into idiomatic Rust types
Apache License 2.0
444 stars 59 forks source link

Issue with schema props #203

Open jrcarl624 opened 1 year ago

jrcarl624 commented 1 year ago

.1.0 (C:\Users\miner\OneDrive\Documents\GitHub\Androecia\FriendConnect-rs\minecraft-bedrock-schemas-rs)`

Caused by: process didn't exit successfully: C:\Users\miner\OneDrive\Documents\GitHub\Androecia\FriendConnect-rs\minecraft-bedrock-schemas-rs\target\debug\build\minecraft-bedrock-schemas-89cec1978fa2cbc3\build-script-build (exit code: 101) --- stdout Writing to: .\src\behavior\animations\animations.rs Writing to: .\src\behavior\animation_controllers\animation_controller.rs

--- stderr thread 'main' panicked at 'not yet implemented: invalid (or unexpected) schema: SchemaObject { metadata: Some( Metadata { id: None, title: Some( "Transition", ), description: Some( "The transition definition for.", ), default: None, deprecated: false, read_only: false, write_only: false, examples: [], }, ), instance_type: Some( Single( Array, ), ), format: None, enum_values: None, const_value: None, subschemas: None, number: None, string: None, array: Some( ArrayValidation { items: Some( Single( Object( SchemaObject { metadata: Some( Metadata { id: None, title: Some( "Transition", ), description: Some( "A transition to another state.", ), default: None, deprecated: false, read_only: false, write_only: false, examples: [ Object { "default": String("query.is_chested"), }, ], }, ), instance_type: Some( Single( Object, ), ), format: None, enum_values: None, const_value: None, subschemas: None, number: None, string: None, array: None, object: Some( ObjectValidation { max_properties: Some( 1, ), min_properties: Some( 1, ), required: {}, properties: {}, pattern_properties: {}, additional_properties: Some( Object( SchemaObject { metadata: None, instance_type: None, format: None, enum_values: None, const_value: None, subschemas: None, number: None, string: None, array: None, object: None, reference: Some( "#/definitions/A", ), extensions: {}, }, ), ), property_names: None, }, ), reference: None, extensions: {}, }, ), ), ), additional_items: None, max_items: None, min_items: None, unique_items: None, contains: None, }, ), object: Some( ObjectValidation { max_properties: None, min_properties: Some( 1, ), required: {}, properties: {}, pattern_properties: {}, additional_properties: None, property_names: None, }, ), reference: None, extensions: {}, }', typify\typify-impl\src\convert.rs:551:36

{
  "$schema": "http://json-schema.org/draft-07/schema",
  "$id": "blockception.minecraft.behavior.animation_controller",
  "examples": [
    {
      "format_version": "1.19.0",
      "animation_controllers": {
        "controller.animation.example": {
          "initial_state": "default",
          "states": {
            "default": {
              "transitions": [
                {
                  "state_1": "query.is_baby"
                }
              ]
            },
            "state_1": {}
          }
        }
      }
    }
  ],
  "definitions": {
    "animationspec": {
      "anyOf": [
        {
          "title": "Animation Specification",
          "description": "A single string that specifies which animation there are.",
          "type": "string"
        },
        {
          "type": "object",
          "title": "Animation Specification",
          "description": "A object specification on when to animate.",
          "maxProperties": 1,
          "minProperties": 1,
          "additionalProperties": {
            "$ref": "#/definitions/A"
          }
        }
      ]
    },
    "particle_effect_spec": {
      "additionalProperties": false,
      "type": "object",
      "required": [
        "effect"
      ],
      "properties": {
        "bind_to_actor": {
          "type": "boolean",
          "title": "Bind To Actor",
          "description": "Set to false to have the effect spawned in the world without being bound to an actor (by default an effect is bound to the actor).",
          "const": false
        },
        "effect": {
          "type": "string",
          "title": "Effect",
          "description": "The name of a particle effect that should be played."
        },
        "locator": {
          "type": "string",
          "title": "Locator",
          "description": "The name of a locator on the actor where the effect should be located."
        },
        "pre_effect_script": {
          "type": "string",
          "title": "Pre Effect Script",
          "description": "A molang script that will be run when the particle emitter is initialized."
        }
      }
    },
    "commands": {
      "type": "string",
      "description": "The event or commands to execute.",
      "title": "Commands",
      "oneOf": [
        {
          "pattern": "^@s .+$",
          "title": "Event"
        },
        {
          "pattern": "^/.+$",
          "title": "Command"
        },
        {
          "pattern": "^.+;$",
          "title": "Molang"
        }
      ]
    },
    "A": {
      "type": "string",
      "title": "Molang",
      "description": "Molang definition.",
      "format": "molang",
      "examples": [
        "query.variant",
        "(1.0)",
        "query.",
        "variable.=;"
      ],
      "defaultSnippets": [
        {
          "label": "New Molang",
          "body": "$1"
        }
      ]
    },
    "B": {
      "title": "Format Version",
      "description": "A version that tells minecraft what type of data format can be expected when reading this file.",
      "pattern": "^([1-9]+)\\.([0-9]+)\\.([0-9]+)$",
      "type": "string",
      "default": "1.19.40",
      "examples": [
        "1.19.40",
        "1.18.0",
        "1.17.0",
        "1.16.0",
        "1.15.0",
        "1.14.0",
        "1.13.0",
        "1.12.0",
        "1.10.0",
        "1.8.0"
      ],
      "defaultSnippets": [
        {
          "label": "New Format version",
          "body": "1.${1|8,10,12,17,18,19|}.${3|2|0|}"
        }
      ]
    }
  },
  "type": "object",
  "title": "Animation Controller",
  "description": "Animation controller for behaviors.",
  "required": [
    "format_version",
    "animation_controllers"
  ],
  "additionalProperties": false,
  "properties": {
    "format_version": {
      "$ref": "#/definitions/B"
    },
    "animation_controllers": {
      "type": "object",
      "title": "Animation Controllers",
      "description": "The animation controllers schema for.",
      "propertyNames": {
        "pattern": "^controller\\.animation\\.[a-z\\.]+",
        "examples": [
          "controller.animation.example",
          "controller.animation.example.foo"
        ]
      },
      "additionalProperties": {
        "additionalProperties": false,
        "type": "object",
        "title": "Animation Controller",
        "description": "A single animation controller.",
        "required": [
          "states"
        ],
        "minProperties": 1,
        "properties": {
          "states": {
            "title": "States",
            "description": "The states of this animation controller.",
            "propertyNames": {
              "pattern": "[a-z\\.]+"
            },
            "minProperties": 1,
            "type": "object",
            "additionalProperties": {
              "additionalProperties": false,
              "title": "Animation State",
              "description": "Animation state.",
              "type": "object",
              "examples": [
                {
                  "animations": [
                    "anim.idle"
                  ],
                  "transitions": [
                    {
                      "example": "query.is_sheared"
                    }
                  ]
                }
              ],
              "properties": {
                "animations": {
                  "title": "Animations",
                  "description": "The animations definition for.",
                  "type": "array",
                  "items": {
                    "$ref": "#/definitions/animationspec",
                    "description": "The key definition of an animation to play, defined in the entity.",
                    "title": "Animations"
                  }
                },
                "on_entry": {
                  "type": "array",
                  "description": "Events, commands or transitions to preform on entry of this state.",
                  "title": "On Entry",
                  "items": {
                    "$ref": "#/definitions/commands"
                  }
                },
                "on_exit": {
                  "type": "array",
                  "description": "Events, commands or transitions to preform on exit of this state.",
                  "title": "On Exit",
                  "items": {
                    "$ref": "#/definitions/commands"
                  }
                },
                "transitions": {
                  "title": "Transition",
                  "description": "The transition definition for.",
                  "minProperties": 1,
                  "type": "array",
                  "items": {
                    "title": "Transition",
                    "description": "A transition to another state.",
                    "type": "object",
                    "maxProperties": 1,
                    "minProperties": 1,
                    "examples": [
                      {
                        "default": "query.is_chested"
                      }
                    ],
                    "additionalProperties": {
                      "$ref": "#/definitions/A"
                    }
                  }
                }
              }
            }
          },
          "initial_state": {
            "title": "Initial State",
            "description": "The state to start with, if not specified state at position 0 in the array is used.",
            "type": "string",
            "examples": [
              "default"
            ]
          }
        }
      }
    }
  }
}
jrcarl624 commented 1 year ago

Also wouldn't it be a better approach to use an enum for anyOf

ahl commented 1 year ago

The "Transitions" schema seems to contain an error:

                "transitions": {
                  "title": "Transition",
                  "description": "The transition definition for.",
                  "minProperties": 1,
                  "type": "array",
                  ...

This is an array with minProperties which is only valid for object types. I think it should be minItems: https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02#section-6.4.2

As it stands I believe that this schema is unsatisfiable in the most pedantic sense. That is to say, a type can't both be an array and have at least one object property.

It's hard to know what to do with these kinds of mis-formed schemas. We could try to infer what the intent was, but that's a slippery slope that can lead to bad assumptions. I recognize that this is a JSON schema whose content you don't control. Typify could do more to explain the problem (e.g. "object validation on array type"); then I'd suggest you could use something like JSON patch to clean it up, or a more general "JSON schema cleaner" to look for mistakes such as this.

ahl commented 1 year ago

The only path forward I can see here that we could handle is to ignore fields that are inconsistent with the type field.

ahl commented 1 year ago

While looking into #230 it does look like it's reasonable--maybe even correct--to ignore constraints unrelated to the type. I'll implement that when I have the chance.

jrcarl624 commented 1 year ago

While looking into #230 it does look like it's reasonable--maybe even correct--to ignore constraints unrelated to the type. I'll implement that when I have the chance.

Looking into this later I have validated the schema and it did not error out so the schema is correct according to draft 7

jrcarl624 commented 1 year ago
{
    "$id": "blockception.minecraft.language_names",
    "$schema": "http://json-schema.org/draft-07/schema",
    "additionalProperties": false,
    "definitions": {},
    "description": "A language names definitions file.",
    "examples": [
        [
            [
                "en_US",
                "English (US)"
            ],
            [
                "en_GB",
                "English (UK)"
            ],
            [
                "de_DE",
                "Deutsch (Deutschland)"
            ],
            [
                "es_ES",
                "Español (España)"
            ],
            [
                "es_MX",
                "Español (México)"
            ],
            [
                "fr_FR",
                "Français (France)"
            ],
            [
                "fr_CA",
                "Français (Canada)"
            ],
            [
                "it_IT",
                "Italiano (Italia)"
            ],
            [
                "ja_JP",
                "日本語 (日本)"
            ],
            [
                "ko_KR",
                "한국어 (대한민국)"
            ],
            [
                "pt_BR",
                "Português (Brasil)"
            ],
            [
                "pt_PT",
                "Português (Portugal)"
            ],
            [
                "ru_RU",
                "Русский (Россия)"
            ],
            [
                "zh_CN",
                "简体中文"
            ],
            [
                "zh_TW",
                "繁體中文"
            ],
            [
                "nl_NL",
                "Nederlands (Nederland)"
            ],
            [
                "bg_BG",
                "Български (BG)"
            ],
            [
                "cs_CZ",
                "Čeština (Česká republika)"
            ],
            [
                "da_DK",
                "Dansk (DA)"
            ],
            [
                "el_GR",
                "Ελληνικά (Ελλάδα)"
            ],
            [
                "fi_FI",
                "Suomi (Suomi)"
            ],
            [
                "hu_HU",
                "Magyar (HU)"
            ],
            [
                "id_ID",
                "Bahasa Indonesia (Indonesia)"
            ],
            [
                "nb_NO",
                "Norsk bokmål (Norge)"
            ],
            [
                "pl_PL",
                "Polski (PL)"
            ],
            [
                "sk_SK",
                "Slovensky (SK)"
            ],
            [
                "sv_SE",
                "Svenska (Sverige)"
            ],
            [
                "tr_TR",
                "Türkçe (Türkiye)"
            ],
            [
                "uk_UA",
                "Українська (Україна)"
            ]
        ]
    ],
    "items": {
        "description": "A language name identifier.",
        "items": [
            {
                "description": "A language identifier.",
                "pattern": "^[a-z]{2}_[A-Z]{2}$",
                "type": "string"
            },
            {
                "description": "The name of the language.",
                "type": "string"
            }
        ],
        "type": "array"
    },
    "title": "Language Names",
    "type": "array"
}

I dont seem to understand why this errors out, this is something when I have run into.

ahl commented 1 year ago

The first schema you mentioned should be addressed by #255 which fixes #253. The second schema you mentioned is more complex. If it had

    "minItems": 2,
    "maxItems": 2,

... it would be pretty simple to model as Vec<(StringWithRegex, String)> but because it doesn't, we need a type more like this:

struct ArrayItem(Option<StringWithRegex>, Option<String>, Vec<serde_json::Value>);

The inner array schema validates with an array of any number of items as long as the first two (if present) conform the string-with-regex, and string respectively. I've opened #254 to track that work more specifically.

ahl commented 1 year ago

Or maybe something like this--assuming we would prefer that only valid states are representable in Rust:

enum ArrayItem {
    pub Items0,
    pub Items1(StringWithRegex),
    pub Items2(StringWithRegex, String),
    pub ItemsN(StringWithRegex, String, Vec<serde_json::Value>),
}

impl ArrayItem {
    pub fn get0(&self) -> Option<&StringWithRegex> {
        match self {
            Self::Items1(zero) | Self::Items2(zero, ..) | Self::ItemsN(zero, ..) => Some(zero),
            _ => None,
        }
    }
    pub fn get1(&self) -> Option<&StringWithRegex> {
        match self {
            Self::Items2(_, one) | Self::ItemsN(_, one, ..) => Some(one),
            _ => None,
        }
    }
    // ...
}

We could generate something to make it easier to construct:

mod builder {
    struct ArrayItem(Option<StringWithRegex>, Option<String>, Vec<serde_json::Value>);

    pub fn with_0(self, item_0: StringWithRegex) -> Self {
        let (_, item_1, rest) = self;
        Self(Some(item_0), item_1, rest)
    }

    // ...

    impl std::convert::TryFrom<ArrayItem> for super::ArrayItem {
        fn try_from(value: ArrayItem) -> Result<super::ArrayItem, String> {
            match self {
                (None, None, rest) if rest.is_empty() => Ok(Self::Items0),
                (Some(zero), None, rest) if rest.is_empty() => Ok(Self::Items1(zero)),
                (Some(zero), Some(one), rest) if rest.is_empty() => Ok(Self::Items2(zero, one)),
                (Some(zero), Some(one), rest) => Ok(Self::ItemsN(zero, one, rest)),
                _ => Err("nope".to_string()),
            }
        }
    }
}