├── .craft.yml ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CODEOWNERS ├── Cargo.toml ├── README.md ├── scripts └── bump-version.sh ├── src ├── bin │ └── main.rs ├── diff_walker.rs ├── lib.rs ├── resolver.rs └── types.rs └── tests ├── fixtures ├── additional_properties │ ├── extend.json │ └── restrict.json ├── any_of │ ├── any_of_with_constraint_to_type_1.json │ ├── any_of_with_constraint_to_type_2.json │ ├── number_1.json │ ├── objects_1.json │ ├── objects_2.json │ ├── objects_3.json │ ├── order_change.json │ ├── to_equivalent_type.json │ ├── to_less_strict_type.json │ ├── to_more_strict_type.json │ ├── type_to_any_of_within_array.json │ ├── type_to_equivalent_any_of.json │ ├── type_to_less_strict_any_of.json │ └── type_to_more_strict_any_of.json ├── const │ ├── const_in_any_of_1.json │ ├── const_in_any_of_2.json │ ├── const_in_any_of_3.json │ ├── const_in_properties.json │ ├── const_string_to_const_number.json │ ├── const_string_to_other_const_string.json │ ├── integer_to_const_string.json │ ├── number_to_const_string.json │ ├── object_to_const_object.json │ └── string_to_const_number.json ├── properties │ ├── add.json │ ├── add_property_in_array.json │ ├── add_property_in_array_of_any_of.json │ ├── change.json │ ├── remove.json │ └── remove_property_while_allowing_additional_properties.json ├── range │ ├── add_maximum.json │ ├── add_minimum.json │ ├── add_minimum_in_array.json │ ├── all.json │ ├── change_minimum.json │ ├── change_minimum_and_maximum.json │ ├── minimum_to_exclusive_maximum.json │ ├── redundant_minimum_1.json │ ├── redundant_minimum_2.json │ ├── redundant_minimum_3.json │ ├── remove_maximum.json │ ├── remove_minimum.json │ ├── to_exclusive_maximum.json │ ├── to_exclusive_minimum.json │ └── unchanged_minimum.json ├── ref │ ├── factor_out_definitions.json │ └── factor_out_definitions_and_change.json ├── required │ ├── add.json │ └── drop.json └── type │ ├── basic.json │ ├── extend_from_const.json │ ├── extend_type.json │ ├── integer_to_number.json │ ├── nothing.json │ ├── number_to_integer.json │ └── restrict_to_const.json ├── snapshots ├── test__from_fixtures@additional_properties__extend.json.snap ├── test__from_fixtures@additional_properties__restrict.json.snap ├── test__from_fixtures@any_of__any_of_with_constraint_to_type_1.json.snap ├── test__from_fixtures@any_of__any_of_with_constraint_to_type_2.json.snap ├── test__from_fixtures@any_of__number_1.json.snap ├── test__from_fixtures@any_of__objects_1.json.snap ├── test__from_fixtures@any_of__objects_2.json.snap ├── test__from_fixtures@any_of__objects_3.json.snap ├── test__from_fixtures@any_of__order_change.json.snap ├── test__from_fixtures@any_of__to_equivalent_type.json.snap ├── test__from_fixtures@any_of__to_less_strict_type.json.snap ├── test__from_fixtures@any_of__to_more_strict_type.json.snap ├── test__from_fixtures@any_of__type_to_any_of_within_array.json.snap ├── test__from_fixtures@any_of__type_to_equivalent_any_of.json.snap ├── test__from_fixtures@any_of__type_to_less_strict_any_of.json.snap ├── test__from_fixtures@any_of__type_to_more_strict_any_of.json.snap ├── test__from_fixtures@const__const_in_any_of_1.json.snap ├── test__from_fixtures@const__const_in_any_of_2.json.snap ├── test__from_fixtures@const__const_in_any_of_3.json.snap ├── test__from_fixtures@const__const_in_properties.json.snap ├── test__from_fixtures@const__const_string_to_const_number.json.snap ├── test__from_fixtures@const__const_string_to_other_const_string.json.snap ├── test__from_fixtures@const__integer_to_const_string.json.snap ├── test__from_fixtures@const__number_to_const_string.json.snap ├── test__from_fixtures@const__object_to_const_object.json.snap ├── test__from_fixtures@const__string_to_const_number.json.snap ├── test__from_fixtures@properties__add.json.snap ├── test__from_fixtures@properties__add_property_in_array.json.snap ├── test__from_fixtures@properties__add_property_in_array_of_any_of.json.snap ├── test__from_fixtures@properties__change.json.snap ├── test__from_fixtures@properties__remove.json.snap ├── test__from_fixtures@properties__remove_property_while_allowing_additional_properties.json.snap ├── test__from_fixtures@range__add_maximum.json.snap ├── test__from_fixtures@range__add_minimum.json.snap ├── test__from_fixtures@range__add_minimum_in_array.json.snap ├── test__from_fixtures@range__all.json.snap ├── test__from_fixtures@range__change_minimum.json.snap ├── test__from_fixtures@range__change_minimum_and_maximum.json.snap ├── test__from_fixtures@range__minimum_to_exclusive_maximum.json.snap ├── test__from_fixtures@range__redundant_minimum_1.json.snap ├── test__from_fixtures@range__redundant_minimum_2.json.snap ├── test__from_fixtures@range__redundant_minimum_3.json.snap ├── test__from_fixtures@range__remove_maximum.json.snap ├── test__from_fixtures@range__remove_minimum.json.snap ├── test__from_fixtures@range__to_exclusive_maximum.json.snap ├── test__from_fixtures@range__to_exclusive_minimum.json.snap ├── test__from_fixtures@range__unchanged_minimum.json.snap ├── test__from_fixtures@ref__factor_out_definitions.json.snap ├── test__from_fixtures@ref__factor_out_definitions_and_change.json.snap ├── test__from_fixtures@required__add.json.snap ├── test__from_fixtures@required__drop.json.snap ├── test__from_fixtures@type__basic.json.snap ├── test__from_fixtures@type__extend_from_const.json.snap ├── test__from_fixtures@type__extend_type.json.snap ├── test__from_fixtures@type__integer_to_number.json.snap ├── test__from_fixtures@type__nothing.json.snap ├── test__from_fixtures@type__number_to_integer.json.snap └── test__from_fixtures@type__restrict_to_const.json.snap └── test.rs /.craft.yml: -------------------------------------------------------------------------------- 1 | minVersion: 0.34.1 2 | github: 3 | owner: getsentry 4 | repo: json-schema-diff 5 | changelogPolicy: auto 6 | artifactProvider: 7 | name: none 8 | targets: 9 | - name: crates 10 | - name: github 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - "release/**" 8 | pull_request: 9 | 10 | jobs: 11 | lints: 12 | name: Lints 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout sources 16 | uses: actions/checkout@v2 17 | 18 | - name: Install stable toolchain 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | profile: minimal 22 | toolchain: stable 23 | override: true 24 | components: rustfmt, clippy 25 | 26 | - name: Run cargo fmt 27 | uses: actions-rs/cargo@v1 28 | with: 29 | command: fmt 30 | args: --all -- --check 31 | 32 | - name: Run cargo clippy 33 | uses: actions-rs/cargo@v1 34 | with: 35 | command: clippy 36 | args: --all-features --workspace --tests --examples -- -D clippy::all 37 | 38 | test: 39 | name: Test 40 | runs-on: ubuntu-latest 41 | 42 | steps: 43 | - name: Checkout sources 44 | uses: actions/checkout@v2 45 | 46 | - name: Install rust toolchain 47 | uses: actions-rs/toolchain@v1 48 | with: 49 | profile: minimal 50 | toolchain: stable 51 | override: true 52 | 53 | - name: Run cargo test 54 | uses: actions-rs/cargo@v1 55 | with: 56 | command: test 57 | args: --workspace --all-features 58 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: Version to release 8 | required: true 9 | force: 10 | description: Force a release even when there are release-blockers (optional) 11 | required: false 12 | 13 | jobs: 14 | release: 15 | runs-on: ubuntu-latest 16 | name: "Release a new version" 17 | steps: 18 | - name: Get auth token 19 | id: token 20 | uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 21 | with: 22 | app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} 23 | private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} 24 | - uses: actions/checkout@v2 25 | with: 26 | token: ${{ steps.token.outputs.token }} 27 | fetch-depth: 0 28 | 29 | - name: Prepare release 30 | uses: getsentry/action-prepare-release@v1 31 | env: 32 | GITHUB_TOKEN: ${{ steps.token.outputs.token }} 33 | with: 34 | version: ${{ github.event.inputs.version }} 35 | force: ${{ github.event.inputs.force }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | 4 | # Added by cargo 5 | # 6 | # already existing elements were commented out 7 | 8 | #/target 9 | /Cargo.lock 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog and versioning 2 | ## 0.1.7 3 | 4 | ### Various fixes & improvements 5 | 6 | - fix: Add CLI install instructions (#37) by @untitaker 7 | - support `exclusive` keywords (#35) by @6293 8 | - do not clone Schema, clone only when the inner struct is Bool (#34) by @6293 9 | - rm unnecessary boxing (#33) by @6293 10 | - compare anyOf based on handmade diff score (#32) by @6293 11 | 12 | ## 0.1.6 13 | 14 | ### Various fixes & improvements 15 | 16 | - support const keyword (#27) by @6293 17 | - include input values in snapshot for a smooth review (#28) by @6293 18 | - chore: consistent return type among methods (#29) by @6293 19 | - split testsuite into files (#24) by @6293 20 | - split multiple types into anyOf subschema (#20) by @6293 21 | - feat: Add support for changes to "required" properties (#19) by @lynnagara 22 | - doc: Remove feature list from README (#18) by @untitaker 23 | 24 | ## 0.1.5 25 | 26 | ### Various fixes & improvements 27 | 28 | - support for minimum/maximum changes (#13) by @6293 29 | - doc: Make the purpose more clear in README (#12) by @untitaker 30 | - fix: Reenable help output for json-schema-diff (#11) by @untitaker 31 | 32 | ## 0.1.4 33 | 34 | ### Various fixes & improvements 35 | 36 | - ref: Rewrite the crate and add support for references (#10) by @untitaker 37 | 38 | ## 0.1.3 39 | 40 | ### Various fixes & improvements 41 | 42 | - fix: anyOf is not order-sensitive (#9) by @untitaker 43 | - fix: Fix bug where additionalProperties was not true in changeset (#7) by @untitaker 44 | - fix: Add another failing test and fix licensing metadata (#8) by @untitaker 45 | - fix: Implement rudimentary support for anyOf and array items (#6) by @untitaker 46 | 47 | ## 0.1.2 48 | 49 | ### Various fixes & improvements 50 | 51 | - fix: Add repository link (#4) by @untitaker 52 | - fix: property removal is a breaking change if additionalProperties is false (#3) by @untitaker 53 | - fix: Use a different replacement function in bump-version (#2) by @untitaker 54 | - feat: Add codeowners (#1) by @untitaker 55 | 56 | ## 0.1.1 57 | 58 | ### Various fixes & improvements 59 | 60 | - fix formatting (e2f76cf0) by @untitaker 61 | - serialize is_breaking into output on CLI (249fee74) by @untitaker 62 | 63 | ## 0.1.0 64 | 65 | ### Various fixes & improvements 66 | 67 | - fix license identifier (7e770213) by @untitaker 68 | - fix cargo metadata (05593500) by @untitaker 69 | - add bump-version script (9370fd8e) by @untitaker 70 | 71 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Search and storage is the default owner 2 | * @getsentry/owners-snuba 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "json-schema-diff" 3 | version = "0.1.7" 4 | edition = "2021" 5 | license = "Apache-2.0" 6 | description = "Diff changes between JSON schema files." 7 | repository = "https://github.com/getsentry/json-schema-diff" 8 | authors = ["Sentry "] 9 | 10 | [lib] 11 | name = "json_schema_diff" 12 | path = "src/lib.rs" 13 | 14 | [[bin]] 15 | required-features = ["build-binary"] 16 | name = "json-schema-diff" 17 | path = "src/bin/main.rs" 18 | 19 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 20 | 21 | [dependencies] 22 | anyhow = { version = "1.0.70", optional = true } 23 | clap = { version = "4.1.13", features = ["std", "derive", "usage", "help"], default-features = false, optional = true } 24 | schemars = { version = "0.8.12", default-features = false } 25 | serde = "1.0.158" 26 | serde_json = "1.0.94" 27 | thiserror = "1.0.40" 28 | pathfinding = "4.2.1" 29 | 30 | [features] 31 | build-binary = ["clap", "anyhow"] 32 | 33 | [dev-dependencies] 34 | insta = { version = "1.29.0", features = ["glob", "serde"] } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # json-schema-diff 2 | 3 | A work-in-progress tool to diff changes between JSON schemas. A lot of JSON 4 | schema features are not implemented and therefore ignored, see [the issue 5 | tracker](https://github.com/getsentry/json-schema-diff/issues). 6 | 7 | Use this tool as a best-effort to find obviously breaking changes in CI, but not for much more. 8 | 9 | This crate is used with draft-07 but even that is work in progress. 10 | 11 | ## Usage via CLI 12 | 13 | [Install Rust](https://rustup.rs/) and: 14 | 15 | ```bash 16 | cargo install json-schema-diff 17 | 18 | cat schema-old.json schema-new.json 19 | # {"type": "string"} 20 | # {"type": "boolean"} 21 | 22 | cargo run --features=build-binary -- \ 23 | schema-old.json \ 24 | schema-new.json 25 | # {"path":"","change":{"TypeRemove":{"removed":"string"}},"is_breaking":true} 26 | # {"path":"","change":{"TypeAdd":{"added":"boolean"}},"is_breaking":false} 27 | ``` 28 | 29 | Sentry uses this tool in 30 | [`sentry-kafka-schemas`](https://github.com/getsentry/sentry-kafka-schemas) to 31 | annotate pull requests with breaking changes made to schema definitions. It 32 | invokes the CLI tool on the schema from master vs the schema in the PR, and 33 | post-processes the output using a Python script for human consumption. 34 | 35 | `is_breaking` is just a suggestion. You may choose to ignore it entirely and 36 | instead define which kinds of changes are breaking to you in wrapper scripts. 37 | 38 | ## Usage as library 39 | 40 | ```rust 41 | use json_schema_diff::*; 42 | 43 | let lhs = serde_json::json! {{ 44 | "type": "string", 45 | }}; 46 | let rhs = serde_json::json! {{ 47 | "type": "boolean", 48 | }}; 49 | 50 | assert_eq!( 51 | json_schema_diff::diff(lhs, rhs).unwrap(), 52 | vec![ 53 | Change { 54 | path: "".to_owned(), 55 | change: ChangeKind::TypeRemove { removed: JsonSchemaType::String } 56 | }, 57 | Change { 58 | path: "".to_owned(), 59 | change: ChangeKind::TypeAdd { added: JsonSchemaType::Boolean } 60 | } 61 | ] 62 | ); 63 | ``` 64 | 65 | ## License 66 | 67 | Licensed under Apache 2.0 68 | -------------------------------------------------------------------------------- /scripts/bump-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | cd $SCRIPT_DIR/.. 6 | 7 | OLD_VERSION="${1}" 8 | NEW_VERSION="${2}" 9 | 10 | echo "Current version: ${OLD_VERSION}" 11 | echo "Bumping version: ${NEW_VERSION}" 12 | 13 | function replace() { 14 | ! grep "$2" $3 15 | perl -i -pe "s/$1/$2/g" $3 16 | grep "$2" $3 # verify that replacement was successful 17 | } 18 | 19 | replace "^version = \".*?\"" "version = \"$NEW_VERSION\"" Cargo.toml 20 | -------------------------------------------------------------------------------- /src/bin/main.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::path::PathBuf; 3 | 4 | use anyhow::Error; 5 | use clap::Parser; 6 | use serde::Serialize; 7 | 8 | /// Compare old and new schema, and print differences 9 | #[derive(Parser)] 10 | #[clap(about, version)] 11 | struct Args { 12 | /// The old schema 13 | lhs: PathBuf, 14 | /// The new schema 15 | rhs: PathBuf, 16 | } 17 | 18 | #[derive(Serialize)] 19 | struct Change { 20 | #[serde(flatten)] 21 | inner: json_schema_diff::Change, 22 | is_breaking: bool, 23 | } 24 | 25 | fn main() -> Result<(), Error> { 26 | let args = Args::parse(); 27 | 28 | let lhs: serde_json::Value = serde_json::from_reader(File::open(args.lhs)?)?; 29 | let rhs: serde_json::Value = serde_json::from_reader(File::open(args.rhs)?)?; 30 | 31 | let changes = json_schema_diff::diff(lhs, rhs)?; 32 | 33 | for change in changes { 34 | let is_breaking = change.change.is_breaking(); 35 | let change = Change { 36 | inner: change, 37 | is_breaking, 38 | }; 39 | println!("{}", serde_json::to_string(&change)?); 40 | } 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /src/diff_walker.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, BTreeSet}; 2 | use std::mem::discriminant; 3 | 4 | use schemars::schema::{ 5 | InstanceType, NumberValidation, ObjectValidation, RootSchema, Schema, SchemaObject, 6 | SingleOrVec, SubschemaValidation, 7 | }; 8 | use serde_json::Value; 9 | 10 | use crate::resolver::Resolver; 11 | use crate::{Change, ChangeKind, Error, JsonSchemaType, Range}; 12 | 13 | pub struct DiffWalker { 14 | pub cb: F, 15 | pub lhs_root: RootSchema, 16 | pub rhs_root: RootSchema, 17 | lhs_resolver: Resolver, 18 | rhs_resolver: Resolver, 19 | } 20 | 21 | impl DiffWalker { 22 | pub fn new(cb: F, lhs_root: RootSchema, rhs_root: RootSchema) -> Self { 23 | let lhs_resolver = Resolver::for_schema(&lhs_root); 24 | let rhs_resolver = Resolver::for_schema(&rhs_root); 25 | Self { 26 | cb, 27 | lhs_root, 28 | rhs_root, 29 | lhs_resolver, 30 | rhs_resolver, 31 | } 32 | } 33 | 34 | fn diff_any_of( 35 | &mut self, 36 | json_path: &str, 37 | is_rhs_split: bool, 38 | lhs: &mut SchemaObject, 39 | rhs: &mut SchemaObject, 40 | ) -> Result<(), Error> { 41 | // hack to get a stable order for anyOf. serde_json::Value does not impl Hash or Ord, so we 42 | // can't use a set. 43 | if let (Some(lhs_any_of), Some(rhs_any_of)) = 44 | (&mut lhs.subschemas().any_of, &mut rhs.subschemas().any_of) 45 | { 46 | let max_len = lhs_any_of.len().max(rhs_any_of.len()); 47 | lhs_any_of.resize(max_len, Schema::Bool(false)); 48 | rhs_any_of.resize(max_len, Schema::Bool(false)); 49 | 50 | let mut mat = pathfinding::matrix::Matrix::new(max_len, max_len, 0i32); 51 | for (i, l) in lhs_any_of.iter_mut().enumerate() { 52 | for (j, r) in rhs_any_of.iter_mut().enumerate() { 53 | let mut count = 0; 54 | let counter = |_change: Change| count += 1; 55 | DiffWalker::new( 56 | Box::new(counter) as Box, 57 | self.lhs_root.clone(), 58 | self.rhs_root.clone(), 59 | ) 60 | .diff("", l, r)?; 61 | mat[(i, j)] = count; 62 | } 63 | } 64 | let pairs = pathfinding::kuhn_munkres::kuhn_munkres_min(&mat).1; 65 | for i in 0..max_len { 66 | let new_path = match is_rhs_split { 67 | true => json_path.to_owned(), 68 | false => format!("{json_path}.", pairs[i]), 69 | }; 70 | self.do_diff( 71 | &new_path, 72 | true, 73 | &mut lhs_any_of[i].clone().into_object(), 74 | &mut rhs_any_of[pairs[i]].clone().into_object(), 75 | )?; 76 | } 77 | } 78 | 79 | Ok(()) 80 | } 81 | 82 | fn diff_instance_types( 83 | &mut self, 84 | json_path: &str, 85 | lhs: &mut SchemaObject, 86 | rhs: &mut SchemaObject, 87 | ) { 88 | let lhs_ty = lhs.effective_type().into_set(); 89 | let rhs_ty = rhs.effective_type().into_set(); 90 | 91 | for removed in lhs_ty.difference(&rhs_ty) { 92 | (self.cb)(Change { 93 | path: json_path.to_owned(), 94 | change: ChangeKind::TypeRemove { 95 | removed: removed.clone(), 96 | }, 97 | }); 98 | } 99 | 100 | for added in rhs_ty.difference(&lhs_ty) { 101 | (self.cb)(Change { 102 | path: json_path.to_owned(), 103 | change: ChangeKind::TypeAdd { 104 | added: added.clone(), 105 | }, 106 | }); 107 | } 108 | } 109 | 110 | fn diff_const(&mut self, json_path: &str, lhs: &mut SchemaObject, rhs: &mut SchemaObject) { 111 | Self::normalize_const(lhs); 112 | Self::normalize_const(rhs); 113 | match (&lhs.const_value, &rhs.const_value) { 114 | (Some(value), None) => (self.cb)(Change { 115 | path: json_path.to_owned(), 116 | change: ChangeKind::ConstRemove { 117 | removed: value.clone(), 118 | }, 119 | }), 120 | (None, Some(value)) => (self.cb)(Change { 121 | path: json_path.to_owned(), 122 | change: ChangeKind::ConstAdd { 123 | added: value.clone(), 124 | }, 125 | }), 126 | (Some(l), Some(r)) if l != r => { 127 | (self.cb)(Change { 128 | path: json_path.to_owned(), 129 | change: ChangeKind::ConstRemove { removed: l.clone() }, 130 | }); 131 | (self.cb)(Change { 132 | path: json_path.to_owned(), 133 | change: ChangeKind::ConstAdd { added: r.clone() }, 134 | }); 135 | } 136 | _ => (), 137 | } 138 | } 139 | 140 | fn diff_properties( 141 | &mut self, 142 | json_path: &str, 143 | lhs: &mut SchemaObject, 144 | rhs: &mut SchemaObject, 145 | ) -> Result<(), Error> { 146 | let lhs_props: BTreeSet<_> = lhs.object().properties.keys().cloned().collect(); 147 | let rhs_props: BTreeSet<_> = rhs.object().properties.keys().cloned().collect(); 148 | 149 | let lhs_additional_properties = lhs 150 | .object() 151 | .additional_properties 152 | .as_ref() 153 | .is_none_or(|x| x.clone().into_object().is_true()); 154 | 155 | for removed in lhs_props.difference(&rhs_props) { 156 | (self.cb)(Change { 157 | path: json_path.to_owned(), 158 | change: ChangeKind::PropertyRemove { 159 | lhs_additional_properties, 160 | removed: removed.clone(), 161 | }, 162 | }); 163 | } 164 | 165 | for added in rhs_props.difference(&lhs_props) { 166 | (self.cb)(Change { 167 | path: json_path.to_owned(), 168 | change: ChangeKind::PropertyAdd { 169 | lhs_additional_properties, 170 | added: added.clone(), 171 | }, 172 | }); 173 | } 174 | 175 | for common in rhs_props.intersection(&lhs_props) { 176 | let lhs_child = lhs.object().properties.get_mut(common.as_str()).unwrap(); 177 | let rhs_child = rhs.object().properties.get_mut(common.as_str()).unwrap(); 178 | 179 | let new_path = format!("{json_path}.{common}"); 180 | self.diff(&new_path, lhs_child, rhs_child)?; 181 | } 182 | 183 | Ok(()) 184 | } 185 | 186 | fn diff_additional_properties( 187 | &mut self, 188 | json_path: &str, 189 | lhs: &mut SchemaObject, 190 | rhs: &mut SchemaObject, 191 | ) -> Result<(), Error> { 192 | if let (Some(lhs_additional_properties), Some(rhs_additional_properties)) = ( 193 | &mut lhs.object().additional_properties, 194 | &mut rhs.object().additional_properties, 195 | ) { 196 | if rhs_additional_properties != lhs_additional_properties { 197 | let new_path = format!("{json_path}."); 198 | 199 | self.diff( 200 | &new_path, 201 | lhs_additional_properties, 202 | rhs_additional_properties, 203 | )?; 204 | } 205 | } 206 | 207 | Ok(()) 208 | } 209 | 210 | fn diff_range( 211 | &mut self, 212 | json_path: &str, 213 | lhs: &mut SchemaObject, 214 | rhs: &mut SchemaObject, 215 | ) -> Result<(), Error> { 216 | let mut diff = |lhs, rhs| match (lhs, rhs) { 217 | (None, Some(value)) => (self.cb)(Change { 218 | path: json_path.to_owned(), 219 | change: ChangeKind::RangeAdd { added: value }, 220 | }), 221 | (Some(value), None) => (self.cb)(Change { 222 | path: json_path.to_owned(), 223 | change: ChangeKind::RangeRemove { removed: value }, 224 | }), 225 | (Some(lhs), Some(rhs)) 226 | if (lhs != rhs && discriminant(&lhs) == discriminant(&rhs)) 227 | || discriminant(&lhs) != discriminant(&rhs) => 228 | { 229 | (self.cb)(Change { 230 | path: json_path.to_owned(), 231 | change: ChangeKind::RangeChange { 232 | old_value: lhs, 233 | new_value: rhs, 234 | }, 235 | }) 236 | } 237 | _ => (), 238 | }; 239 | let choose_min = |schema: &mut SchemaObject| match ( 240 | schema.number_validation().minimum, 241 | schema.number_validation().exclusive_minimum, 242 | ) { 243 | (Some(min), None) => Some(Range::Minimum(min)), 244 | (None, Some(exc)) => Some(Range::ExclusiveMinimum(exc)), 245 | (Some(min), Some(exc)) if min <= exc => Some(Range::ExclusiveMinimum(exc)), 246 | (Some(min), Some(exc)) if min > exc => Some(Range::Minimum(min)), 247 | _ => None, 248 | }; 249 | let choose_max = |schema: &mut SchemaObject| match ( 250 | schema.number_validation().maximum, 251 | schema.number_validation().exclusive_maximum, 252 | ) { 253 | (Some(max), None) => Some(Range::Maximum(max)), 254 | (None, Some(exc)) => Some(Range::ExclusiveMaximum(exc)), 255 | (Some(max), Some(exc)) if max >= exc => Some(Range::ExclusiveMaximum(exc)), 256 | (Some(max), Some(exc)) if max < exc => Some(Range::Maximum(max)), 257 | _ => None, 258 | }; 259 | diff(choose_min(lhs), choose_min(rhs)); 260 | diff(choose_max(lhs), choose_max(rhs)); 261 | Ok(()) 262 | } 263 | 264 | fn diff_array_items( 265 | &mut self, 266 | json_path: &str, 267 | lhs: &mut SchemaObject, 268 | rhs: &mut SchemaObject, 269 | ) -> Result<(), Error> { 270 | match (&mut lhs.array().items, &mut rhs.array().items) { 271 | (Some(SingleOrVec::Vec(lhs_items)), Some(SingleOrVec::Vec(rhs_items))) => { 272 | if lhs_items.len() != rhs_items.len() { 273 | (self.cb)(Change { 274 | path: json_path.to_owned(), 275 | change: ChangeKind::TupleChange { 276 | new_length: rhs_items.len(), 277 | }, 278 | }); 279 | } 280 | 281 | for (i, (lhs_inner, rhs_inner)) in 282 | lhs_items.iter_mut().zip(rhs_items.iter_mut()).enumerate() 283 | { 284 | let new_path = format!("{json_path}.{i}"); 285 | self.diff(&new_path, lhs_inner, rhs_inner)?; 286 | } 287 | } 288 | (Some(SingleOrVec::Single(lhs_inner)), Some(SingleOrVec::Single(rhs_inner))) => { 289 | let new_path = format!("{json_path}.?"); 290 | self.diff(&new_path, lhs_inner, rhs_inner)?; 291 | } 292 | (Some(SingleOrVec::Single(lhs_inner)), Some(SingleOrVec::Vec(rhs_items))) => { 293 | (self.cb)(Change { 294 | path: json_path.to_owned(), 295 | change: ChangeKind::ArrayToTuple { 296 | new_length: rhs_items.len(), 297 | }, 298 | }); 299 | 300 | for (i, rhs_inner) in rhs_items.iter_mut().enumerate() { 301 | let new_path = format!("{json_path}.{i}"); 302 | self.diff(&new_path, lhs_inner, rhs_inner)?; 303 | } 304 | } 305 | (Some(SingleOrVec::Vec(lhs_items)), Some(SingleOrVec::Single(rhs_inner))) => { 306 | (self.cb)(Change { 307 | path: json_path.to_owned(), 308 | change: ChangeKind::TupleToArray { 309 | old_length: lhs_items.len(), 310 | }, 311 | }); 312 | 313 | for (i, lhs_inner) in lhs_items.iter_mut().enumerate() { 314 | let new_path = format!("{json_path}.{i}"); 315 | self.diff(&new_path, lhs_inner, rhs_inner)?; 316 | } 317 | } 318 | (None, None) => (), 319 | 320 | #[cfg(not(test))] 321 | _ => (), 322 | #[cfg(test)] 323 | (x, y) => todo!("{:?} {:?}", x, y), 324 | } 325 | 326 | Ok(()) 327 | } 328 | 329 | fn diff_required( 330 | &mut self, 331 | json_path: &str, 332 | lhs: &mut SchemaObject, 333 | rhs: &mut SchemaObject, 334 | ) -> Result<(), Error> { 335 | let lhs_required = &lhs.object().required; 336 | let rhs_required = &rhs.object().required; 337 | 338 | for removed in lhs_required.difference(rhs_required) { 339 | (self.cb)(Change { 340 | path: json_path.to_owned(), 341 | change: ChangeKind::RequiredRemove { 342 | property: removed.clone(), 343 | }, 344 | }); 345 | } 346 | 347 | for added in rhs_required.difference(lhs_required) { 348 | (self.cb)(Change { 349 | path: json_path.to_owned(), 350 | change: ChangeKind::RequiredAdd { 351 | property: added.clone(), 352 | }, 353 | }); 354 | } 355 | 356 | Ok(()) 357 | } 358 | 359 | fn resolve_references( 360 | &self, 361 | lhs: &mut SchemaObject, 362 | rhs: &mut SchemaObject, 363 | ) -> Result<(), Error> { 364 | if let Some(ref reference) = lhs.reference { 365 | if let Some(lhs_inner) = self.lhs_resolver.resolve(&self.lhs_root, reference) { 366 | *lhs = lhs_inner.clone().into_object(); 367 | } 368 | } 369 | 370 | if let Some(ref reference) = rhs.reference { 371 | if let Some(rhs_inner) = self.rhs_resolver.resolve(&self.rhs_root, reference) { 372 | *rhs = rhs_inner.clone().into_object(); 373 | } 374 | } 375 | 376 | Ok(()) 377 | } 378 | 379 | fn restrictions_for_single_type(schema_object: &SchemaObject, ty: InstanceType) -> Schema { 380 | let mut ret = SchemaObject { 381 | instance_type: Some(SingleOrVec::Single(Box::new(ty))), 382 | ..Default::default() 383 | }; 384 | match ty { 385 | InstanceType::String => ret.string = schema_object.string.clone(), 386 | InstanceType::Number | InstanceType::Integer => { 387 | ret.number = schema_object.number.clone() 388 | } 389 | InstanceType::Object => ret.object = schema_object.object.clone(), 390 | InstanceType::Array => ret.array = schema_object.array.clone(), 391 | _ => (), 392 | } 393 | Schema::Object(ret) 394 | } 395 | 396 | /// Split a schema into multiple schemas, one for each type in the multiple type. 397 | /// Returns the new schema and whether the schema was changed. 398 | fn split_types(schema_object: &mut SchemaObject) -> bool { 399 | let is_split = match schema_object.effective_type() { 400 | InternalJsonSchemaType::Multiple(types) 401 | if schema_object.subschemas().any_of.is_none() => 402 | { 403 | *schema_object = SchemaObject { 404 | subschemas: Some(Box::new(SubschemaValidation { 405 | any_of: Some( 406 | types 407 | .into_iter() 408 | .map(|ty| { 409 | Self::restrictions_for_single_type(schema_object, ty.into()) 410 | }) 411 | .collect(), 412 | ), 413 | ..Default::default() 414 | })), 415 | ..Default::default() 416 | }; 417 | true 418 | } 419 | _ => false, 420 | }; 421 | is_split 422 | } 423 | 424 | fn normalize_const(schema_object: &mut SchemaObject) { 425 | fn do_normalize(value: Value) -> SchemaObject { 426 | match value { 427 | Value::Object(obj) => { 428 | let properties = obj 429 | .into_iter() 430 | .map(|(k, v)| (k, Schema::Object(do_normalize(v)))) 431 | .collect::>(); 432 | SchemaObject { 433 | object: Some(Box::new(ObjectValidation { 434 | properties, 435 | ..Default::default() 436 | })), 437 | ..Default::default() 438 | } 439 | } 440 | _ => SchemaObject { 441 | const_value: Some(value), 442 | ..Default::default() 443 | }, 444 | } 445 | } 446 | if let Some(value) = schema_object.const_value.take() { 447 | *schema_object = do_normalize(value) 448 | } 449 | } 450 | 451 | fn do_diff( 452 | &mut self, 453 | json_path: &str, 454 | // Whether we are comparing elements in any_of subschemas 455 | comparing_any_of: bool, 456 | lhs: &mut SchemaObject, 457 | rhs: &mut SchemaObject, 458 | ) -> Result<(), Error> { 459 | self.resolve_references(lhs, rhs)?; 460 | let is_lhs_split = Self::split_types(lhs); 461 | let is_rhs_split = Self::split_types(rhs); 462 | self.diff_any_of(json_path, is_rhs_split, lhs, rhs)?; 463 | if !comparing_any_of { 464 | self.diff_instance_types(json_path, lhs, rhs); 465 | } 466 | self.diff_const(json_path, lhs, rhs); 467 | // If we split the types, we don't want to compare type-specific properties 468 | // because they are already compared in the `Self::diff_any_of` 469 | if !is_lhs_split && !is_rhs_split { 470 | self.diff_properties(json_path, lhs, rhs)?; 471 | self.diff_range(json_path, lhs, rhs)?; 472 | self.diff_additional_properties(json_path, lhs, rhs)?; 473 | self.diff_array_items(json_path, lhs, rhs)?; 474 | self.diff_required(json_path, lhs, rhs)?; 475 | } 476 | Ok(()) 477 | } 478 | 479 | pub fn diff( 480 | &mut self, 481 | json_path: &str, 482 | lhs: &mut Schema, 483 | rhs: &mut Schema, 484 | ) -> Result<(), Error> { 485 | match (lhs, rhs) { 486 | (Schema::Object(lhs), Schema::Object(rhs)) => self.do_diff(json_path, false, lhs, rhs), 487 | (bool_lhs, Schema::Object(rhs)) => { 488 | self.do_diff(json_path, false, &mut bool_lhs.clone().into_object(), rhs) 489 | } 490 | (Schema::Object(lhs), bool_rhs) => { 491 | self.do_diff(json_path, false, lhs, &mut bool_rhs.clone().into_object()) 492 | } 493 | (bool_lhs, bool_rhs) => self.do_diff( 494 | json_path, 495 | false, 496 | &mut bool_lhs.clone().into_object(), 497 | &mut bool_rhs.clone().into_object(), 498 | ), 499 | } 500 | } 501 | } 502 | 503 | trait JsonSchemaExt { 504 | fn is_true(&self) -> bool; 505 | fn effective_type(&mut self) -> InternalJsonSchemaType; 506 | /// Look for NumberValidation from "number" property in the schema. 507 | /// Check if `anyOf` subschema has NumberValidation, if the subschema is a single type. 508 | fn number_validation(&mut self) -> NumberValidation; 509 | } 510 | 511 | impl JsonSchemaExt for SchemaObject { 512 | fn is_true(&self) -> bool { 513 | *self == SchemaObject::default() 514 | } 515 | 516 | fn effective_type(&mut self) -> InternalJsonSchemaType { 517 | if let Some(ref ty) = self.instance_type { 518 | match ty { 519 | SingleOrVec::Single(ty) => JsonSchemaType::from(**ty).into(), 520 | SingleOrVec::Vec(tys) => InternalJsonSchemaType::Multiple( 521 | tys.iter().copied().map(JsonSchemaType::from).collect(), 522 | ), 523 | } 524 | } else if let Some(ref constant) = self.const_value { 525 | serde_value_to_own(constant).into() 526 | } else if !self.object().properties.is_empty() { 527 | JsonSchemaType::Object.into() 528 | } else if let Some(ref any_of) = self.subschemas().any_of { 529 | InternalJsonSchemaType::Multiple( 530 | any_of 531 | .iter() 532 | .flat_map(|a| Self::effective_type(&mut a.clone().into_object()).explode()) 533 | .collect::>() 534 | .into_iter() 535 | .collect(), 536 | ) 537 | } else if self 538 | .subschemas() 539 | .not 540 | .as_ref() 541 | .is_some_and(|x| x.clone().into_object().is_true()) 542 | { 543 | InternalJsonSchemaType::Never 544 | } else { 545 | InternalJsonSchemaType::Any 546 | } 547 | } 548 | 549 | fn number_validation(&mut self) -> NumberValidation { 550 | let number_validation = self.number().clone(); 551 | if number_validation == NumberValidation::default() { 552 | self.subschemas() 553 | .any_of 554 | .as_ref() 555 | .filter(|schemas| schemas.len() == 1) 556 | .and_then(|a| a.first()) 557 | .map(|subschema| subschema.clone().into_object().number().clone()) 558 | .unwrap_or_default() 559 | } else { 560 | number_validation 561 | } 562 | } 563 | } 564 | 565 | #[derive(Clone, Ord, Eq, PartialEq, PartialOrd, Debug)] 566 | enum InternalJsonSchemaType { 567 | Simple(JsonSchemaType), 568 | Any, 569 | Never, 570 | Multiple(Vec), 571 | } 572 | 573 | impl From for InternalJsonSchemaType { 574 | fn from(other: JsonSchemaType) -> Self { 575 | InternalJsonSchemaType::Simple(other) 576 | } 577 | } 578 | 579 | impl InternalJsonSchemaType { 580 | fn into_set(self) -> BTreeSet { 581 | self.explode().into_iter().collect() 582 | } 583 | 584 | fn explode(self) -> Vec { 585 | match self { 586 | Self::Simple(JsonSchemaType::Number) => { 587 | vec![JsonSchemaType::Integer, JsonSchemaType::Number] 588 | } 589 | Self::Any => vec![ 590 | JsonSchemaType::String, 591 | JsonSchemaType::Number, 592 | JsonSchemaType::Integer, 593 | JsonSchemaType::Object, 594 | JsonSchemaType::Array, 595 | JsonSchemaType::Boolean, 596 | JsonSchemaType::Null, 597 | ], 598 | Self::Never => vec![], 599 | Self::Simple(x) => vec![x], 600 | Self::Multiple(xs) => xs 601 | .into_iter() 602 | .map(InternalJsonSchemaType::from) 603 | .flat_map(Self::explode) 604 | .collect(), 605 | } 606 | } 607 | } 608 | 609 | fn serde_value_to_own(val: &Value) -> JsonSchemaType { 610 | match val { 611 | Value::Number(_) => JsonSchemaType::Number, 612 | Value::Null => JsonSchemaType::Null, 613 | Value::String(_) => JsonSchemaType::String, 614 | Value::Bool(_) => JsonSchemaType::Boolean, 615 | Value::Array(_) => JsonSchemaType::Array, 616 | Value::Object(_) => JsonSchemaType::Object, 617 | } 618 | } 619 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![warn(missing_docs)] 3 | 4 | use schemars::schema::{RootSchema, Schema}; 5 | use serde_json::Value; 6 | use thiserror::Error; 7 | 8 | mod diff_walker; 9 | mod resolver; 10 | mod types; 11 | 12 | pub use types::*; 13 | 14 | /// Take two JSON schemas, and compare them. 15 | /// 16 | /// `lhs` (left-hand side) is the old schema, `rhs` (right-hand side) is the new schema. 17 | pub fn diff(lhs: Value, rhs: Value) -> Result, Error> { 18 | let lhs_root: RootSchema = serde_json::from_value(lhs)?; 19 | let rhs_root: RootSchema = serde_json::from_value(rhs)?; 20 | 21 | let mut changes = vec![]; 22 | let mut walker = diff_walker::DiffWalker::new( 23 | |change: Change| { 24 | changes.push(change); 25 | }, 26 | lhs_root, 27 | rhs_root, 28 | ); 29 | walker.diff( 30 | "", 31 | &mut Schema::Object(walker.lhs_root.schema.clone()), 32 | &mut Schema::Object(walker.rhs_root.schema.clone()), 33 | )?; 34 | Ok(changes) 35 | } 36 | -------------------------------------------------------------------------------- /src/resolver.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use schemars::schema::{RootSchema, Schema, SchemaObject}; 4 | 5 | pub struct Resolver { 6 | ref_lookup: BTreeMap, 7 | } 8 | 9 | impl Resolver { 10 | pub fn for_schema(root: &RootSchema) -> Self { 11 | let mut ref_lookup = BTreeMap::new(); 12 | 13 | for (key, schema) in &root.definitions { 14 | if let Some(id) = schema.get_schema_id() { 15 | ref_lookup.insert(id.to_owned(), key.clone()); 16 | } 17 | 18 | if let Some(root_id) = root.schema.get_schema_id() { 19 | ref_lookup.insert(format!("{root_id}#/definitions/{key}"), key.clone()); 20 | ref_lookup.insert(format!("{root_id}#/$defs/{key}"), key.clone()); 21 | } 22 | 23 | ref_lookup.insert(format!("#/definitions/{key}"), key.clone()); 24 | ref_lookup.insert(format!("#/$defs/{key}"), key.clone()); 25 | } 26 | 27 | Self { ref_lookup } 28 | } 29 | 30 | /// Resolves a reference. 31 | /// 32 | /// `root` must be the same schema that was used to construct the resolver. 33 | /// This is not checked. 34 | pub fn resolve<'a>(&self, root: &'a RootSchema, reference: &str) -> Option<&'a Schema> { 35 | let key = self.ref_lookup.get(reference)?; 36 | root.definitions.get(key) 37 | } 38 | } 39 | 40 | trait MayHaveSchemaId { 41 | fn get_schema_id(&self) -> Option<&str>; 42 | } 43 | 44 | impl MayHaveSchemaId for SchemaObject { 45 | fn get_schema_id(&self) -> Option<&str> { 46 | self.metadata 47 | .as_ref() 48 | .and_then(|m| m.id.as_ref()) 49 | .map(|id| id.as_str()) 50 | } 51 | } 52 | 53 | impl MayHaveSchemaId for Schema { 54 | fn get_schema_id(&self) -> Option<&str> { 55 | match self { 56 | Schema::Object(schema_obj) => schema_obj.get_schema_id(), 57 | Schema::Bool(_) => None, 58 | } 59 | } 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use super::*; 65 | 66 | #[test] 67 | fn draft7_definitions() { 68 | let root: RootSchema = serde_json::from_str( 69 | r#"{ 70 | "definitions": { 71 | "A": {} 72 | } 73 | }"#, 74 | ) 75 | .unwrap(); 76 | let resolver = Resolver::for_schema(&root); 77 | 78 | let resolved = resolver.resolve(&root, "#/definitions/A"); 79 | assert!(resolved.is_some()); 80 | 81 | let resolved = resolver.resolve(&root, "#/definitions/not-there"); 82 | assert!(resolved.is_none()); 83 | } 84 | 85 | #[test] 86 | fn draft7_root_has_id() { 87 | let root: RootSchema = serde_json::from_str( 88 | r#"{ 89 | "$id": "urn:uuid:e773a2e8-d746-4dc6-9480-0bba5ff33504", 90 | "definitions": { 91 | "A": {} 92 | } 93 | }"#, 94 | ) 95 | .unwrap(); 96 | let resolver = Resolver::for_schema(&root); 97 | 98 | let resolved = resolver.resolve(&root, "#/definitions/A"); 99 | assert!(resolved.is_some()); 100 | let resolved = resolver.resolve( 101 | &root, 102 | "urn:uuid:e773a2e8-d746-4dc6-9480-0bba5ff33504#/definitions/A", 103 | ); 104 | assert!(resolved.is_some()); 105 | } 106 | 107 | #[test] 108 | fn draft7_definition_has_id() { 109 | let root: RootSchema = serde_json::from_str( 110 | r#"{ 111 | "definitions": { 112 | "A": { 113 | "$id": "some-id" 114 | } 115 | } 116 | }"#, 117 | ) 118 | .unwrap(); 119 | let resolver = Resolver::for_schema(&root); 120 | 121 | let resolved = resolver.resolve(&root, "some-id"); 122 | assert!(resolved.is_some()); 123 | assert_eq!(resolved, resolver.resolve(&root, "#/definitions/A")) 124 | } 125 | 126 | #[test] 127 | fn draft2020_12_defs() { 128 | let root: RootSchema = serde_json::from_str( 129 | r#"{ 130 | "$defs": { 131 | "A": { 132 | "$id": "some-id" 133 | } 134 | } 135 | }"#, 136 | ) 137 | .unwrap(); 138 | let resolver = Resolver::for_schema(&root); 139 | 140 | let resolved = resolver.resolve(&root, "#/$defs/A"); 141 | assert!(resolved.is_some()); 142 | assert_eq!(resolved, resolver.resolve(&root, "some-id")); 143 | 144 | let resolved = resolver.resolve(&root, "#/$defs/not-there"); 145 | assert!(resolved.is_none()); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use schemars::schema::InstanceType; 2 | use serde::Serialize; 3 | use thiserror::Error; 4 | 5 | /// An "atomic" change made to the JSON schema in question, going from LHS to RHS. 6 | /// 7 | /// Just a wrapper container for `ChangeKind` 8 | #[derive(Debug, PartialEq, Serialize)] 9 | pub struct Change { 10 | /// JSON path for the given change. `""` for "root schema". `".foo"` for property foo. 11 | pub path: String, 12 | /// Data specific to the kind of change. 13 | pub change: ChangeKind, 14 | } 15 | 16 | /// The kind of change + data relevant to the change. 17 | #[derive(Debug, PartialEq, Serialize)] 18 | pub enum ChangeKind { 19 | /// A type has been added and is now additionally allowed. 20 | TypeAdd { 21 | /// The type in question. 22 | added: JsonSchemaType, 23 | }, 24 | /// A type has been removed and is no longer allowed. 25 | TypeRemove { 26 | /// The type in question. 27 | removed: JsonSchemaType, 28 | }, 29 | /// A const value has been added as an allowed value. 30 | ConstAdd { 31 | /// The value of the added const. 32 | added: serde_json::Value, 33 | }, 34 | /// A const has been removed as an allowed value. 35 | ConstRemove { 36 | /// The value of the removed const. 37 | removed: serde_json::Value, 38 | }, 39 | /// A property has been added and (depending on additionalProperties) is now additionally 40 | /// allowed. 41 | PropertyAdd { 42 | /// The value of additionalProperties within the current JSON object. 43 | lhs_additional_properties: bool, 44 | /// The name of the added property. 45 | added: String, 46 | }, 47 | /// A property has been removed and (depending on additionalProperties) might now no longer be 48 | /// allowed. 49 | PropertyRemove { 50 | /// The value of additionalProperties within the current JSON object. 51 | lhs_additional_properties: bool, 52 | /// The name of the added property. 53 | removed: String, 54 | }, 55 | /// A minimum/maximum constraint has been added. 56 | RangeAdd { 57 | /// The value of the added constraint. 58 | added: Range, 59 | }, 60 | /// A minimum/maximum constraint has been removed. 61 | RangeRemove { 62 | /// The value of the removed constraint. 63 | removed: Range, 64 | }, 65 | /// A minimum/maximum constraint has been updated. 66 | RangeChange { 67 | /// The old constraint value. 68 | old_value: Range, 69 | /// The new constraint value. 70 | new_value: Range, 71 | }, 72 | /// An array-type item has been changed from tuple validation to array validation. 73 | /// 74 | /// See https://json-schema.org/understanding-json-schema/reference/array.html 75 | /// 76 | /// Changes will still be emitted for inner items. 77 | TupleToArray { 78 | /// The length of the (old) tuple 79 | old_length: usize, 80 | }, 81 | /// An array-type item has been changed from array validation to tuple validation. 82 | /// 83 | /// See https://json-schema.org/understanding-json-schema/reference/array.html 84 | /// 85 | /// Changes will still be emitted for inner items. 86 | ArrayToTuple { 87 | /// The length of the (new) tuple 88 | new_length: usize, 89 | }, 90 | /// An array-type item with tuple validation has changed its length ("items" array got longer 91 | /// or shorter. 92 | /// 93 | /// See https://json-schema.org/understanding-json-schema/reference/array.html 94 | /// 95 | /// Changes will still be emitted for inner items. 96 | TupleChange { 97 | /// The new length of the tuple 98 | new_length: usize, 99 | }, 100 | /// A previously required property has been removed 101 | RequiredRemove { 102 | /// The property that is no longer required 103 | property: String, 104 | }, 105 | /// A previously optional property has been made required 106 | RequiredAdd { 107 | /// The property that is now required 108 | property: String, 109 | }, 110 | } 111 | 112 | impl ChangeKind { 113 | /// Whether the change is breaking. 114 | /// 115 | /// What is considered breaking is WIP. Changes are intentionally exposed as-is in public API 116 | /// so that the user can develop their own logic as to what they consider breaking. 117 | /// 118 | /// Currently the rule of thumb is, a change is breaking if it would cause messages that used 119 | /// to validate fine under RHS to no longer validate under LHS. 120 | pub fn is_breaking(&self) -> bool { 121 | match self { 122 | Self::TypeAdd { .. } => false, 123 | Self::TypeRemove { .. } => true, 124 | Self::ConstAdd { .. } => true, 125 | Self::ConstRemove { .. } => false, 126 | Self::PropertyAdd { 127 | lhs_additional_properties, 128 | .. 129 | } => *lhs_additional_properties, 130 | Self::PropertyRemove { 131 | lhs_additional_properties, 132 | .. 133 | } => !*lhs_additional_properties, 134 | Self::RangeAdd { .. } => true, 135 | Self::RangeRemove { .. } => false, 136 | Self::RangeChange { 137 | old_value, 138 | new_value, 139 | } => match (old_value, new_value) { 140 | (Range::ExclusiveMinimum(exc), Range::Minimum(min)) if exc >= min => false, 141 | (Range::ExclusiveMaximum(exc), Range::Maximum(max)) if exc <= max => false, 142 | (Range::Minimum(l), Range::Minimum(r)) if l >= r => false, 143 | (Range::ExclusiveMinimum(l), Range::ExclusiveMinimum(r)) if l >= r => false, 144 | (Range::Maximum(l), Range::Maximum(r)) if l <= r => false, 145 | (Range::ExclusiveMaximum(l), Range::ExclusiveMaximum(r)) if l <= r => false, 146 | _ => true, 147 | }, 148 | Self::TupleToArray { .. } => false, 149 | Self::ArrayToTuple { .. } => true, 150 | Self::TupleChange { .. } => true, 151 | Self::RequiredRemove { .. } => false, 152 | Self::RequiredAdd { .. } => true, 153 | } 154 | } 155 | } 156 | 157 | /// The errors that can happen in this crate. 158 | #[derive(Error, Debug)] 159 | pub enum Error { 160 | /// Failed to parse the JSON schema. 161 | /// 162 | /// Any deserialization errors from serde that happen while converting the value into our AST 163 | /// end up here. 164 | #[error("failed to parse schema")] 165 | Serde(#[from] serde_json::Error), 166 | } 167 | 168 | /// All primitive types defined in JSON schema. 169 | #[derive(Serialize, Clone, Ord, Eq, PartialEq, PartialOrd, Debug)] 170 | #[allow(missing_docs)] 171 | pub enum JsonSchemaType { 172 | #[serde(rename = "string")] 173 | String, 174 | #[serde(rename = "number")] 175 | Number, 176 | #[serde(rename = "integer")] 177 | Integer, 178 | #[serde(rename = "object")] 179 | Object, 180 | #[serde(rename = "array")] 181 | Array, 182 | #[serde(rename = "boolean")] 183 | Boolean, 184 | #[serde(rename = "null")] 185 | Null, 186 | } 187 | 188 | impl From for InstanceType { 189 | fn from(t: JsonSchemaType) -> Self { 190 | match t { 191 | JsonSchemaType::String => InstanceType::String, 192 | JsonSchemaType::Number => InstanceType::Number, 193 | JsonSchemaType::Integer => InstanceType::Integer, 194 | JsonSchemaType::Object => InstanceType::Object, 195 | JsonSchemaType::Array => InstanceType::Array, 196 | JsonSchemaType::Boolean => InstanceType::Boolean, 197 | JsonSchemaType::Null => InstanceType::Null, 198 | } 199 | } 200 | } 201 | 202 | impl From for JsonSchemaType { 203 | fn from(t: InstanceType) -> Self { 204 | match t { 205 | InstanceType::String => JsonSchemaType::String, 206 | InstanceType::Number => JsonSchemaType::Number, 207 | InstanceType::Integer => JsonSchemaType::Integer, 208 | InstanceType::Object => JsonSchemaType::Object, 209 | InstanceType::Array => JsonSchemaType::Array, 210 | InstanceType::Boolean => JsonSchemaType::Boolean, 211 | InstanceType::Null => JsonSchemaType::Null, 212 | } 213 | } 214 | } 215 | 216 | /// Range constraints in JSON schema. 217 | #[derive(Serialize, Clone, PartialEq, PartialOrd, Debug)] 218 | #[serde(rename_all = "camelCase")] 219 | #[allow(missing_docs)] 220 | pub enum Range { 221 | Minimum(f64), 222 | Maximum(f64), 223 | ExclusiveMinimum(f64), 224 | ExclusiveMaximum(f64), 225 | } 226 | 227 | #[cfg(test)] 228 | mod tests { 229 | use super::*; 230 | #[test] 231 | fn is_range_change_breaking() { 232 | assert!(!ChangeKind::RangeChange { 233 | old_value: Range::Minimum(1.0), 234 | new_value: Range::Minimum(1.0), 235 | } 236 | .is_breaking()); 237 | 238 | assert!(ChangeKind::RangeChange { 239 | old_value: Range::Minimum(1.0), 240 | new_value: Range::Minimum(2.0), 241 | } 242 | .is_breaking()); 243 | 244 | assert!(!ChangeKind::RangeChange { 245 | old_value: Range::Minimum(2.0), 246 | new_value: Range::Minimum(1.0), 247 | } 248 | .is_breaking()); 249 | 250 | assert!(ChangeKind::RangeChange { 251 | old_value: Range::Minimum(1.0), 252 | new_value: Range::ExclusiveMinimum(1.0), 253 | } 254 | .is_breaking()); 255 | 256 | assert!(ChangeKind::RangeChange { 257 | old_value: Range::Minimum(1.0), 258 | new_value: Range::ExclusiveMinimum(2.0), 259 | } 260 | .is_breaking()); 261 | 262 | assert!(ChangeKind::RangeChange { 263 | old_value: Range::Minimum(2.0), 264 | new_value: Range::ExclusiveMinimum(1.0), 265 | } 266 | .is_breaking()); 267 | 268 | assert!(!ChangeKind::RangeChange { 269 | old_value: Range::ExclusiveMinimum(1.0), 270 | new_value: Range::ExclusiveMinimum(1.0), 271 | } 272 | .is_breaking()); 273 | 274 | assert!(ChangeKind::RangeChange { 275 | old_value: Range::ExclusiveMinimum(1.0), 276 | new_value: Range::ExclusiveMinimum(2.0), 277 | } 278 | .is_breaking()); 279 | 280 | assert!(!ChangeKind::RangeChange { 281 | old_value: Range::ExclusiveMinimum(2.0), 282 | new_value: Range::ExclusiveMinimum(1.0), 283 | } 284 | .is_breaking()); 285 | 286 | assert!(!ChangeKind::RangeChange { 287 | old_value: Range::Maximum(1.0), 288 | new_value: Range::Maximum(1.0), 289 | } 290 | .is_breaking()); 291 | 292 | assert!(!ChangeKind::RangeChange { 293 | old_value: Range::Maximum(1.0), 294 | new_value: Range::Maximum(2.0), 295 | } 296 | .is_breaking()); 297 | 298 | assert!(ChangeKind::RangeChange { 299 | old_value: Range::Maximum(2.0), 300 | new_value: Range::Maximum(1.0), 301 | } 302 | .is_breaking()); 303 | 304 | assert!(ChangeKind::RangeChange { 305 | old_value: Range::Maximum(1.0), 306 | new_value: Range::ExclusiveMaximum(1.0), 307 | } 308 | .is_breaking()); 309 | 310 | assert!(ChangeKind::RangeChange { 311 | old_value: Range::Maximum(1.0), 312 | new_value: Range::ExclusiveMaximum(2.0), 313 | } 314 | .is_breaking()); 315 | 316 | assert!(ChangeKind::RangeChange { 317 | old_value: Range::Maximum(2.0), 318 | new_value: Range::ExclusiveMaximum(1.0), 319 | } 320 | .is_breaking()); 321 | 322 | assert!(!ChangeKind::RangeChange { 323 | old_value: Range::ExclusiveMaximum(1.0), 324 | new_value: Range::ExclusiveMaximum(1.0), 325 | } 326 | .is_breaking()); 327 | 328 | assert!(!ChangeKind::RangeChange { 329 | old_value: Range::ExclusiveMaximum(1.0), 330 | new_value: Range::ExclusiveMaximum(2.0), 331 | } 332 | .is_breaking()); 333 | 334 | assert!(ChangeKind::RangeChange { 335 | old_value: Range::ExclusiveMaximum(2.0), 336 | new_value: Range::ExclusiveMaximum(1.0), 337 | } 338 | .is_breaking()); 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /tests/fixtures/additional_properties/extend.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "additionalProperties": false }, 3 | "rhs": { "additionalProperties": true } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/additional_properties/restrict.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "additionalProperties": true }, 3 | "rhs": { "additionalProperties": false } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/any_of/any_of_with_constraint_to_type_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "anyOf": [{"type": "integer", "minimum": 1}] 4 | }, 5 | "rhs": { 6 | "type": ["integer", "string"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/any_of/any_of_with_constraint_to_type_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "anyOf": [{"type": "integer", "minimum": 1}, {"type": "integer", "minimum": 2}] 4 | }, 5 | "rhs": { 6 | "type": ["integer", "string"], 7 | "minimum": 1 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/fixtures/any_of/number_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "anyOf": [ 4 | {"type": "number", "maximum": 10}, 5 | {"type": "number", "minimum": 1}, 6 | {"type": "number", "minimum": 100, "maximum": 200} 7 | ] 8 | }, 9 | "rhs": { 10 | "anyOf": [ 11 | {"type": "number", "minimum": 7, "maximum": 14}, 12 | {"type": "number", "maximum": 3}, 13 | {"type": "number", "minimum": 2} 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/fixtures/any_of/objects_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "anyOf": [ 4 | {"properties": {"foo": {}}}, 5 | {"properties": {"type": {"const": "bar"}}} 6 | ] 7 | }, 8 | "rhs": { 9 | "anyOf": [ 10 | { 11 | "title": "replay_recording", 12 | "type": "object", 13 | "properties": {"foo": {}} 14 | }, 15 | {"properties": {"type": {"const": "bar"}}} 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/fixtures/any_of/objects_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "anyOf": [ 4 | {"properties": {"foo": {}}}, 5 | {"properties": {"type": {"const": "bar"}}} 6 | ] 7 | }, 8 | "rhs": { 9 | "anyOf": [ 10 | { "type": "boolean" }, 11 | { 12 | "title": "replay_recording", 13 | "type": "object", 14 | "properties": {"foo": {}} 15 | }, 16 | {"properties": {"type": {"const": "bar"}}} 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/fixtures/any_of/objects_3.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "anyOf": [ 4 | { "type": "boolean" }, 5 | {"properties": {"foo": {}}}, 6 | {"properties": {"type": {"const": "bar"}}} 7 | ] 8 | }, 9 | "rhs": { 10 | "anyOf": [ 11 | { 12 | "title": "replay_recording", 13 | "type": "object", 14 | "properties": {"foo": {}} 15 | }, 16 | {"properties": {"type": {"const": "bar"}}} 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/fixtures/any_of/order_change.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "anyOf": [ 4 | {"type": "array"}, 5 | {"type": "string"} 6 | ] 7 | }, 8 | "rhs": { 9 | "anyOf": [ 10 | {"type": "string"}, 11 | {"type": "array"} 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/fixtures/any_of/to_equivalent_type.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "anyOf": [{ "type": "string" }]}, 3 | "rhs": { "type": "string" } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/any_of/to_less_strict_type.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "anyOf": [{"type": "integer"}] 4 | }, 5 | "rhs": { 6 | "type": ["integer", "string"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/any_of/to_more_strict_type.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "anyOf": [{"type": "integer"}, {"type": "string"}] 4 | }, 5 | "rhs": { 6 | "type": "string" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/any_of/type_to_any_of_within_array.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "array", 4 | "items": [ 5 | {"const": "start_unmerge"}, 6 | {"$ref": "#/definitions/Hello"} 7 | ], 8 | "definitions": { 9 | "Hello": { 10 | "type": "number" 11 | } 12 | } 13 | }, 14 | "rhs": { 15 | "type": "array", 16 | "items": [ 17 | {"const": "start_unmerge"}, 18 | {"$ref": "#/definitions/Hello"} 19 | ], 20 | "definitions": { 21 | "Hello": { 22 | "anyOf": [{"type": "number", "minimum": 1.0}] 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/fixtures/any_of/type_to_equivalent_any_of.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "integer" 4 | }, 5 | "rhs": { 6 | "anyOf": [{"type": "integer"}] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/any_of/type_to_less_strict_any_of.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "integer", 4 | "minimum": 1.0 5 | }, 6 | "rhs": { 7 | "anyOf": [{"type": "integer"}] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/fixtures/any_of/type_to_more_strict_any_of.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "integer" 4 | }, 5 | "rhs": { 6 | "anyOf": [{"type": "integer"}, {"type": "string"}] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/const/const_in_any_of_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "anyOf": [{ "type": "number" }, { "type": "string" }] }, 3 | "rhs": { "anyOf": [{ "const": 1 }, { "const": "1" }] } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/const/const_in_any_of_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "anyOf": [{ "type": "number" }, { "type": "string" }, { "const": 1 }] }, 3 | "rhs": { "anyOf": [{ "const": 1 }, { "const": "1" }] } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/const/const_in_any_of_3.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "anyOf": [{ "type": "object" }, { "type": "string" }, { "const": { "key": "value" } }] }, 3 | "rhs": { "anyOf": [{ "properties": { "key": { "const": "value" } } }, { "const": "1" }] } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/const/const_in_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "const": { "key": "value" } }, 3 | "rhs": { "properties": { "key": { "const": "value" } } } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/const/const_string_to_const_number.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "const": "foo" }, 3 | "rhs": { "const": 1 } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/const/const_string_to_other_const_string.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "const": "foo" }, 3 | "rhs": { "const": "bar" } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/const/integer_to_const_string.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "type": "integer" }, 3 | "rhs": { "const": "foo" } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/const/number_to_const_string.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "type": "number" }, 3 | "rhs": { "const": "foo" } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/const/object_to_const_object.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "type": "object" }, 3 | "rhs": { "const": { "key": "value" } } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/const/string_to_const_number.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "type": "string" }, 3 | "rhs": { "const": 1 } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/properties/add.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "object", 4 | "properties": { 5 | "hello": {"type": "string"} 6 | } 7 | }, 8 | "rhs": { 9 | "type": "object", 10 | "properties": { 11 | "hello": {"type": "string"}, 12 | "world": {"type": "string"} 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/fixtures/properties/add_property_in_array.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "array", 4 | "items": [ 5 | {"const": "start_unmerge"}, 6 | {"$ref": "#/definitions/Hello"} 7 | ], 8 | "definitions": { 9 | "Hello": { 10 | "type": "object" 11 | } 12 | } 13 | }, 14 | "rhs": { 15 | "type": "array", 16 | "items": [ 17 | {"const": "start_unmerge"}, 18 | {"$ref": "#/definitions/Hello"} 19 | ], 20 | "definitions": { 21 | "Hello": { 22 | "type": "object", 23 | "properties": {"transaction_id": {"type": "string"}} 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/fixtures/properties/add_property_in_array_of_any_of.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "anyOf": [ 4 | { 5 | "type": "array", 6 | "items": [ 7 | {"const": "start_unmerge"}, 8 | {"type": "object"} 9 | ] 10 | } 11 | ] 12 | }, 13 | "rhs": { 14 | "anyOf": [ 15 | { 16 | "type": "array", 17 | "items": [ 18 | {"const": "start_unmerge"}, 19 | {"type": "object", "properties": {"transaction_id": {"type": "string"}}} 20 | ] 21 | } 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/fixtures/properties/change.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "object", 4 | "properties": { 5 | "hello": {"type": "string"} 6 | } 7 | }, 8 | "rhs": { 9 | "type": "object", 10 | "properties": { 11 | "hello": {"type": "number"} 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/fixtures/properties/remove.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "object", 4 | "properties": { 5 | "hello": {"type": "string"}, 6 | "world": {"type": "string"} 7 | } 8 | }, 9 | "rhs": { 10 | "type": "object", 11 | "properties": { 12 | "hello": {"type": "string"} 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/fixtures/properties/remove_property_while_allowing_additional_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "object", 4 | "properties": { 5 | "foobar": {"type": "string"} 6 | }, 7 | "additionalProperties": true 8 | }, 9 | "rhs": { 10 | "type": "object", 11 | "additionalProperties": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/fixtures/range/add_maximum.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "number" 4 | }, 5 | "rhs": { 6 | "type": "number", 7 | "maximum": 1.0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/fixtures/range/add_minimum.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "number" 4 | }, 5 | "rhs": { 6 | "type": "number", 7 | "minimum": 1.0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/fixtures/range/add_minimum_in_array.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "array", 4 | "items": [ 5 | {"const": "start_unmerge"}, 6 | {"$ref": "#/definitions/Hello"} 7 | ], 8 | "definitions": { 9 | "Hello": { 10 | "type": "number" 11 | } 12 | } 13 | }, 14 | "rhs": { 15 | "type": "array", 16 | "items": [ 17 | {"const": "start_unmerge"}, 18 | {"$ref": "#/definitions/Hello"} 19 | ], 20 | "definitions": { 21 | "Hello": { 22 | "type": "number", 23 | "minimum": 1.0 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/fixtures/range/all.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "minimum": 1, 4 | "exclusiveMaximum": 100.0 5 | }, 6 | "rhs": { 7 | "exclusiveMinimum": 3, 8 | "maximum": 30.0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/fixtures/range/change_minimum.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "number", 4 | "minimum": 1.0 5 | }, 6 | "rhs": { 7 | "type": "number", 8 | "minimum": 1.3 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/fixtures/range/change_minimum_and_maximum.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "number", 4 | "minimum": 1.0, 5 | "maximum": 2.0 6 | }, 7 | "rhs": { 8 | "type": "number", 9 | "minimum": 1.5, 10 | "maximum": 2.5 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/fixtures/range/minimum_to_exclusive_maximum.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "number", 4 | "minimum": 1.0 5 | }, 6 | "rhs": { 7 | "type": "number", 8 | "exclusiveMaximum": 1.0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/fixtures/range/redundant_minimum_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "number", 4 | "minimum": 1.0, 5 | "exclusiveMinimum": 1.0 6 | }, 7 | "rhs": { 8 | "type": "number", 9 | "exclusiveMinimum": 0.4 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/range/redundant_minimum_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "number", 4 | "minimum": 1.0, 5 | "exclusiveMinimum": 2.0 6 | }, 7 | "rhs": { 8 | "type": "number", 9 | "exclusiveMinimum": 0.4 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/range/redundant_minimum_3.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "number", 4 | "minimum": 1.0, 5 | "exclusiveMinimum": 0.5 6 | }, 7 | "rhs": { 8 | "type": "number", 9 | "exclusiveMinimum": 0.4 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/range/remove_maximum.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "number", 4 | "maximum": 1.0 5 | }, 6 | "rhs": { 7 | "type": "number" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/fixtures/range/remove_minimum.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "number", 4 | "minimum": 1.0 5 | }, 6 | "rhs": { 7 | "type": "number" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/fixtures/range/to_exclusive_maximum.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "number", 4 | "maximum": 1.0 5 | }, 6 | "rhs": { 7 | "type": "number", 8 | "exclusiveMaximum": 1.0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/fixtures/range/to_exclusive_minimum.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "number", 4 | "minimum": 1.0 5 | }, 6 | "rhs": { 7 | "type": "number", 8 | "exclusiveMinimum": 1.0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/fixtures/range/unchanged_minimum.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "number", 4 | "minimum": 1.3 5 | }, 6 | "rhs": { 7 | "type": "number", 8 | "minimum": 1.3 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/fixtures/ref/factor_out_definitions.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "object" 4 | }, 5 | "rhs": { 6 | "$ref": "#/definitions/Hello", 7 | "definitions": { 8 | "Hello": {"type": "object"} 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/ref/factor_out_definitions_and_change.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "type": "object" 4 | }, 5 | "rhs": { 6 | "$ref": "#/definitions/Hello", 7 | "definitions": { 8 | "Hello": {"type": "array"} 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/required/add.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "required": [] 4 | }, 5 | "rhs": { 6 | "required": ["value"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/required/drop.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { 3 | "required": ["value"] 4 | }, 5 | "rhs": { 6 | "required": [] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/type/basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "type": "string" }, 3 | "rhs": { "type": "number" } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/type/extend_from_const.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "const": "hello" }, 3 | "rhs": { "type": ["string"] } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/type/extend_type.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "type": "string" }, 3 | "rhs": { "type": ["string", "number"] } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/type/integer_to_number.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "type": "integer" }, 3 | "rhs": { "type": "number" } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/type/nothing.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "type": "string" }, 3 | "rhs": { "type": "string" } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/type/number_to_integer.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "type": "number" }, 3 | "rhs": { "type": "integer" } 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/type/restrict_to_const.json: -------------------------------------------------------------------------------- 1 | { 2 | "lhs": { "type": ["string"] }, 3 | "rhs": { "const": "hello" } 4 | } 5 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@additional_properties__extend.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | additionalProperties: false 7 | rhs: 8 | additionalProperties: true 9 | input_file: tests/fixtures/additional_properties/extend.json 10 | --- 11 | [ 12 | Change { 13 | path: ".", 14 | change: TypeAdd { 15 | added: String, 16 | }, 17 | }, 18 | Change { 19 | path: ".", 20 | change: TypeAdd { 21 | added: Number, 22 | }, 23 | }, 24 | Change { 25 | path: ".", 26 | change: TypeAdd { 27 | added: Integer, 28 | }, 29 | }, 30 | Change { 31 | path: ".", 32 | change: TypeAdd { 33 | added: Object, 34 | }, 35 | }, 36 | Change { 37 | path: ".", 38 | change: TypeAdd { 39 | added: Array, 40 | }, 41 | }, 42 | Change { 43 | path: ".", 44 | change: TypeAdd { 45 | added: Boolean, 46 | }, 47 | }, 48 | Change { 49 | path: ".", 50 | change: TypeAdd { 51 | added: Null, 52 | }, 53 | }, 54 | ] 55 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@additional_properties__restrict.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | additionalProperties: true 7 | rhs: 8 | additionalProperties: false 9 | input_file: tests/fixtures/additional_properties/restrict.json 10 | --- 11 | [ 12 | Change { 13 | path: ".", 14 | change: TypeRemove { 15 | removed: String, 16 | }, 17 | }, 18 | Change { 19 | path: ".", 20 | change: TypeRemove { 21 | removed: Number, 22 | }, 23 | }, 24 | Change { 25 | path: ".", 26 | change: TypeRemove { 27 | removed: Integer, 28 | }, 29 | }, 30 | Change { 31 | path: ".", 32 | change: TypeRemove { 33 | removed: Object, 34 | }, 35 | }, 36 | Change { 37 | path: ".", 38 | change: TypeRemove { 39 | removed: Array, 40 | }, 41 | }, 42 | Change { 43 | path: ".", 44 | change: TypeRemove { 45 | removed: Boolean, 46 | }, 47 | }, 48 | Change { 49 | path: ".", 50 | change: TypeRemove { 51 | removed: Null, 52 | }, 53 | }, 54 | ] 55 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@any_of__any_of_with_constraint_to_type_1.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | anyOf: 7 | - minimum: 1 8 | type: integer 9 | rhs: 10 | type: 11 | - integer 12 | - string 13 | input_file: tests/fixtures/any_of/any_of_with_constraint_to_type_1.json 14 | --- 15 | [ 16 | Change { 17 | path: "", 18 | change: RangeRemove { 19 | removed: Minimum( 20 | 1.0, 21 | ), 22 | }, 23 | }, 24 | Change { 25 | path: "", 26 | change: TypeAdd { 27 | added: String, 28 | }, 29 | }, 30 | ] 31 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@any_of__any_of_with_constraint_to_type_2.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | anyOf: 7 | - minimum: 1 8 | type: integer 9 | - minimum: 2 10 | type: integer 11 | rhs: 12 | minimum: 1 13 | type: 14 | - integer 15 | - string 16 | input_file: tests/fixtures/any_of/any_of_with_constraint_to_type_2.json 17 | --- 18 | [ 19 | Change { 20 | path: "", 21 | change: RangeRemove { 22 | removed: Minimum( 23 | 2.0, 24 | ), 25 | }, 26 | }, 27 | Change { 28 | path: "", 29 | change: TypeAdd { 30 | added: String, 31 | }, 32 | }, 33 | ] 34 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@any_of__number_1.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | anyOf: 7 | - maximum: 10 8 | type: number 9 | - minimum: 1 10 | type: number 11 | - maximum: 200 12 | minimum: 100 13 | type: number 14 | rhs: 15 | anyOf: 16 | - maximum: 14 17 | minimum: 7 18 | type: number 19 | - maximum: 3 20 | type: number 21 | - minimum: 2 22 | type: number 23 | input_file: tests/fixtures/any_of/number_1.json 24 | --- 25 | [ 26 | Change { 27 | path: ".", 28 | change: RangeChange { 29 | old_value: Maximum( 30 | 10.0, 31 | ), 32 | new_value: Maximum( 33 | 3.0, 34 | ), 35 | }, 36 | }, 37 | Change { 38 | path: ".", 39 | change: RangeChange { 40 | old_value: Minimum( 41 | 1.0, 42 | ), 43 | new_value: Minimum( 44 | 2.0, 45 | ), 46 | }, 47 | }, 48 | Change { 49 | path: ".", 50 | change: RangeChange { 51 | old_value: Minimum( 52 | 100.0, 53 | ), 54 | new_value: Minimum( 55 | 7.0, 56 | ), 57 | }, 58 | }, 59 | Change { 60 | path: ".", 61 | change: RangeChange { 62 | old_value: Maximum( 63 | 200.0, 64 | ), 65 | new_value: Maximum( 66 | 14.0, 67 | ), 68 | }, 69 | }, 70 | ] 71 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@any_of__objects_1.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | anyOf: 7 | - properties: 8 | foo: {} 9 | - properties: 10 | type: 11 | const: bar 12 | rhs: 13 | anyOf: 14 | - properties: 15 | foo: {} 16 | title: replay_recording 17 | type: object 18 | - properties: 19 | type: 20 | const: bar 21 | input_file: tests/fixtures/any_of/objects_1.json 22 | --- 23 | [] 24 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@any_of__objects_2.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | anyOf: 7 | - properties: 8 | foo: {} 9 | - properties: 10 | type: 11 | const: bar 12 | rhs: 13 | anyOf: 14 | - type: boolean 15 | - properties: 16 | foo: {} 17 | title: replay_recording 18 | type: object 19 | - properties: 20 | type: 21 | const: bar 22 | input_file: tests/fixtures/any_of/objects_2.json 23 | --- 24 | [ 25 | Change { 26 | path: "", 27 | change: TypeAdd { 28 | added: Boolean, 29 | }, 30 | }, 31 | ] 32 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@any_of__objects_3.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | anyOf: 7 | - type: boolean 8 | - properties: 9 | foo: {} 10 | - properties: 11 | type: 12 | const: bar 13 | rhs: 14 | anyOf: 15 | - properties: 16 | foo: {} 17 | title: replay_recording 18 | type: object 19 | - properties: 20 | type: 21 | const: bar 22 | input_file: tests/fixtures/any_of/objects_3.json 23 | --- 24 | [ 25 | Change { 26 | path: "", 27 | change: TypeRemove { 28 | removed: Boolean, 29 | }, 30 | }, 31 | ] 32 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@any_of__order_change.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | anyOf: 7 | - type: array 8 | - type: string 9 | rhs: 10 | anyOf: 11 | - type: string 12 | - type: array 13 | input_file: tests/fixtures/any_of/order_change.json 14 | --- 15 | [] 16 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@any_of__to_equivalent_type.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | anyOf: 7 | - type: string 8 | rhs: 9 | type: string 10 | input_file: tests/fixtures/any_of/to_equivalent_type.json 11 | --- 12 | [] 13 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@any_of__to_less_strict_type.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | anyOf: 7 | - type: integer 8 | rhs: 9 | type: 10 | - integer 11 | - string 12 | input_file: tests/fixtures/any_of/to_less_strict_type.json 13 | --- 14 | [ 15 | Change { 16 | path: "", 17 | change: TypeAdd { 18 | added: String, 19 | }, 20 | }, 21 | ] 22 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@any_of__to_more_strict_type.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | anyOf: 7 | - type: integer 8 | - type: string 9 | rhs: 10 | type: string 11 | input_file: tests/fixtures/any_of/to_more_strict_type.json 12 | --- 13 | [ 14 | Change { 15 | path: "", 16 | change: TypeRemove { 17 | removed: Integer, 18 | }, 19 | }, 20 | ] 21 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@any_of__type_to_any_of_within_array.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | definitions: 7 | Hello: 8 | type: number 9 | items: 10 | - const: start_unmerge 11 | - $ref: "#/definitions/Hello" 12 | type: array 13 | rhs: 14 | definitions: 15 | Hello: 16 | anyOf: 17 | - minimum: 1 18 | type: number 19 | items: 20 | - const: start_unmerge 21 | - $ref: "#/definitions/Hello" 22 | type: array 23 | input_file: tests/fixtures/any_of/type_to_any_of_within_array.json 24 | --- 25 | [ 26 | Change { 27 | path: ".1", 28 | change: RangeAdd { 29 | added: Minimum( 30 | 1.0, 31 | ), 32 | }, 33 | }, 34 | ] 35 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@any_of__type_to_equivalent_any_of.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | type: integer 7 | rhs: 8 | anyOf: 9 | - type: integer 10 | input_file: tests/fixtures/any_of/type_to_equivalent_any_of.json 11 | --- 12 | [] 13 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@any_of__type_to_less_strict_any_of.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | minimum: 1 7 | type: integer 8 | rhs: 9 | anyOf: 10 | - type: integer 11 | input_file: tests/fixtures/any_of/type_to_less_strict_any_of.json 12 | --- 13 | [ 14 | Change { 15 | path: "", 16 | change: RangeRemove { 17 | removed: Minimum( 18 | 1.0, 19 | ), 20 | }, 21 | }, 22 | ] 23 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@any_of__type_to_more_strict_any_of.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | type: integer 7 | rhs: 8 | anyOf: 9 | - type: integer 10 | - type: string 11 | input_file: tests/fixtures/any_of/type_to_more_strict_any_of.json 12 | --- 13 | [ 14 | Change { 15 | path: "", 16 | change: TypeAdd { 17 | added: String, 18 | }, 19 | }, 20 | ] 21 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@const__const_in_any_of_1.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | anyOf: 7 | - type: number 8 | - type: string 9 | rhs: 10 | anyOf: 11 | - const: 1 12 | - const: "1" 13 | input_file: tests/fixtures/const/const_in_any_of_1.json 14 | --- 15 | [ 16 | Change { 17 | path: ".", 18 | change: ConstAdd { 19 | added: Number(1), 20 | }, 21 | }, 22 | Change { 23 | path: ".", 24 | change: ConstAdd { 25 | added: String("1"), 26 | }, 27 | }, 28 | ] 29 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@const__const_in_any_of_2.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | anyOf: 7 | - type: number 8 | - type: string 9 | - const: 1 10 | rhs: 11 | anyOf: 12 | - const: 1 13 | - const: "1" 14 | input_file: tests/fixtures/const/const_in_any_of_2.json 15 | --- 16 | [ 17 | Change { 18 | path: ".", 19 | change: ConstAdd { 20 | added: String("1"), 21 | }, 22 | }, 23 | ] 24 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@const__const_in_any_of_3.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | anyOf: 7 | - type: object 8 | - type: string 9 | - const: 10 | key: value 11 | rhs: 12 | anyOf: 13 | - properties: 14 | key: 15 | const: value 16 | - const: "1" 17 | input_file: tests/fixtures/const/const_in_any_of_3.json 18 | --- 19 | [ 20 | Change { 21 | path: ".", 22 | change: ConstAdd { 23 | added: String("1"), 24 | }, 25 | }, 26 | ] 27 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@const__const_in_properties.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | const: 7 | key: value 8 | rhs: 9 | properties: 10 | key: 11 | const: value 12 | input_file: tests/fixtures/const/const_in_properties.json 13 | --- 14 | [] 15 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@const__const_string_to_const_number.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | const: foo 7 | rhs: 8 | const: 1 9 | input_file: tests/fixtures/const/const_string_to_const_number.json 10 | --- 11 | [ 12 | Change { 13 | path: "", 14 | change: TypeRemove { 15 | removed: String, 16 | }, 17 | }, 18 | Change { 19 | path: "", 20 | change: TypeAdd { 21 | added: Number, 22 | }, 23 | }, 24 | Change { 25 | path: "", 26 | change: TypeAdd { 27 | added: Integer, 28 | }, 29 | }, 30 | Change { 31 | path: "", 32 | change: ConstRemove { 33 | removed: String("foo"), 34 | }, 35 | }, 36 | Change { 37 | path: "", 38 | change: ConstAdd { 39 | added: Number(1), 40 | }, 41 | }, 42 | ] 43 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@const__const_string_to_other_const_string.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | const: foo 7 | rhs: 8 | const: bar 9 | input_file: tests/fixtures/const/const_string_to_other_const_string.json 10 | --- 11 | [ 12 | Change { 13 | path: "", 14 | change: ConstRemove { 15 | removed: String("foo"), 16 | }, 17 | }, 18 | Change { 19 | path: "", 20 | change: ConstAdd { 21 | added: String("bar"), 22 | }, 23 | }, 24 | ] 25 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@const__integer_to_const_string.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | type: integer 7 | rhs: 8 | const: foo 9 | input_file: tests/fixtures/const/integer_to_const_string.json 10 | --- 11 | [ 12 | Change { 13 | path: "", 14 | change: TypeRemove { 15 | removed: Integer, 16 | }, 17 | }, 18 | Change { 19 | path: "", 20 | change: TypeAdd { 21 | added: String, 22 | }, 23 | }, 24 | Change { 25 | path: "", 26 | change: ConstAdd { 27 | added: String("foo"), 28 | }, 29 | }, 30 | ] 31 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@const__number_to_const_string.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | type: number 7 | rhs: 8 | const: foo 9 | input_file: tests/fixtures/const/number_to_const_string.json 10 | --- 11 | [ 12 | Change { 13 | path: "", 14 | change: TypeRemove { 15 | removed: Number, 16 | }, 17 | }, 18 | Change { 19 | path: "", 20 | change: TypeRemove { 21 | removed: Integer, 22 | }, 23 | }, 24 | Change { 25 | path: "", 26 | change: TypeAdd { 27 | added: String, 28 | }, 29 | }, 30 | Change { 31 | path: "", 32 | change: ConstAdd { 33 | added: String("foo"), 34 | }, 35 | }, 36 | ] 37 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@const__object_to_const_object.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | type: object 7 | rhs: 8 | const: 9 | key: value 10 | input_file: tests/fixtures/const/object_to_const_object.json 11 | --- 12 | [ 13 | Change { 14 | path: "", 15 | change: PropertyAdd { 16 | lhs_additional_properties: true, 17 | added: "key", 18 | }, 19 | }, 20 | ] 21 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@const__string_to_const_number.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | type: string 7 | rhs: 8 | const: 1 9 | input_file: tests/fixtures/const/string_to_const_number.json 10 | --- 11 | [ 12 | Change { 13 | path: "", 14 | change: TypeRemove { 15 | removed: String, 16 | }, 17 | }, 18 | Change { 19 | path: "", 20 | change: TypeAdd { 21 | added: Number, 22 | }, 23 | }, 24 | Change { 25 | path: "", 26 | change: TypeAdd { 27 | added: Integer, 28 | }, 29 | }, 30 | Change { 31 | path: "", 32 | change: ConstAdd { 33 | added: Number(1), 34 | }, 35 | }, 36 | ] 37 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@properties__add.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | properties: 7 | hello: 8 | type: string 9 | type: object 10 | rhs: 11 | properties: 12 | hello: 13 | type: string 14 | world: 15 | type: string 16 | type: object 17 | input_file: tests/fixtures/properties/add.json 18 | --- 19 | [ 20 | Change { 21 | path: "", 22 | change: PropertyAdd { 23 | lhs_additional_properties: true, 24 | added: "world", 25 | }, 26 | }, 27 | ] 28 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@properties__add_property_in_array.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | definitions: 7 | Hello: 8 | type: object 9 | items: 10 | - const: start_unmerge 11 | - $ref: "#/definitions/Hello" 12 | type: array 13 | rhs: 14 | definitions: 15 | Hello: 16 | properties: 17 | transaction_id: 18 | type: string 19 | type: object 20 | items: 21 | - const: start_unmerge 22 | - $ref: "#/definitions/Hello" 23 | type: array 24 | input_file: tests/fixtures/properties/add_property_in_array.json 25 | --- 26 | [ 27 | Change { 28 | path: ".1", 29 | change: PropertyAdd { 30 | lhs_additional_properties: true, 31 | added: "transaction_id", 32 | }, 33 | }, 34 | ] 35 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@properties__add_property_in_array_of_any_of.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | anyOf: 7 | - items: 8 | - const: start_unmerge 9 | - type: object 10 | type: array 11 | rhs: 12 | anyOf: 13 | - items: 14 | - const: start_unmerge 15 | - properties: 16 | transaction_id: 17 | type: string 18 | type: object 19 | type: array 20 | input_file: tests/fixtures/properties/add_property_in_array_of_any_of.json 21 | --- 22 | [ 23 | Change { 24 | path: "..1", 25 | change: PropertyAdd { 26 | lhs_additional_properties: true, 27 | added: "transaction_id", 28 | }, 29 | }, 30 | ] 31 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@properties__change.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | properties: 7 | hello: 8 | type: string 9 | type: object 10 | rhs: 11 | properties: 12 | hello: 13 | type: number 14 | type: object 15 | input_file: tests/fixtures/properties/change.json 16 | --- 17 | [ 18 | Change { 19 | path: ".hello", 20 | change: TypeRemove { 21 | removed: String, 22 | }, 23 | }, 24 | Change { 25 | path: ".hello", 26 | change: TypeAdd { 27 | added: Number, 28 | }, 29 | }, 30 | Change { 31 | path: ".hello", 32 | change: TypeAdd { 33 | added: Integer, 34 | }, 35 | }, 36 | ] 37 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@properties__remove.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | properties: 7 | hello: 8 | type: string 9 | world: 10 | type: string 11 | type: object 12 | rhs: 13 | properties: 14 | hello: 15 | type: string 16 | type: object 17 | input_file: tests/fixtures/properties/remove.json 18 | --- 19 | [ 20 | Change { 21 | path: "", 22 | change: PropertyRemove { 23 | lhs_additional_properties: true, 24 | removed: "world", 25 | }, 26 | }, 27 | ] 28 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@properties__remove_property_while_allowing_additional_properties.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | additionalProperties: true 7 | properties: 8 | foobar: 9 | type: string 10 | type: object 11 | rhs: 12 | additionalProperties: true 13 | type: object 14 | input_file: tests/fixtures/properties/remove_property_while_allowing_additional_properties.json 15 | --- 16 | [ 17 | Change { 18 | path: "", 19 | change: PropertyRemove { 20 | lhs_additional_properties: true, 21 | removed: "foobar", 22 | }, 23 | }, 24 | ] 25 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@range__add_maximum.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | type: number 7 | rhs: 8 | maximum: 1 9 | type: number 10 | input_file: tests/fixtures/range/add_maximum.json 11 | --- 12 | [ 13 | Change { 14 | path: "", 15 | change: RangeAdd { 16 | added: Maximum( 17 | 1.0, 18 | ), 19 | }, 20 | }, 21 | ] 22 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@range__add_minimum.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | type: number 7 | rhs: 8 | minimum: 1 9 | type: number 10 | input_file: tests/fixtures/range/add_minimum.json 11 | --- 12 | [ 13 | Change { 14 | path: "", 15 | change: RangeAdd { 16 | added: Minimum( 17 | 1.0, 18 | ), 19 | }, 20 | }, 21 | ] 22 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@range__add_minimum_in_array.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | definitions: 7 | Hello: 8 | type: number 9 | items: 10 | - const: start_unmerge 11 | - $ref: "#/definitions/Hello" 12 | type: array 13 | rhs: 14 | definitions: 15 | Hello: 16 | minimum: 1 17 | type: number 18 | items: 19 | - const: start_unmerge 20 | - $ref: "#/definitions/Hello" 21 | type: array 22 | input_file: tests/fixtures/range/add_minimum_in_array.json 23 | --- 24 | [ 25 | Change { 26 | path: ".1", 27 | change: RangeAdd { 28 | added: Minimum( 29 | 1.0, 30 | ), 31 | }, 32 | }, 33 | ] 34 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@range__all.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | exclusiveMaximum: 100 7 | minimum: 1 8 | rhs: 9 | exclusiveMinimum: 3 10 | maximum: 30 11 | input_file: tests/fixtures/range/all.json 12 | --- 13 | [ 14 | Change { 15 | path: "", 16 | change: RangeChange { 17 | old_value: Minimum( 18 | 1.0, 19 | ), 20 | new_value: ExclusiveMinimum( 21 | 3.0, 22 | ), 23 | }, 24 | }, 25 | Change { 26 | path: "", 27 | change: RangeChange { 28 | old_value: ExclusiveMaximum( 29 | 100.0, 30 | ), 31 | new_value: Maximum( 32 | 30.0, 33 | ), 34 | }, 35 | }, 36 | ] 37 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@range__change_minimum.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | minimum: 1 7 | type: number 8 | rhs: 9 | minimum: 1.3 10 | type: number 11 | input_file: tests/fixtures/range/change_minimum.json 12 | --- 13 | [ 14 | Change { 15 | path: "", 16 | change: RangeChange { 17 | old_value: Minimum( 18 | 1.0, 19 | ), 20 | new_value: Minimum( 21 | 1.3, 22 | ), 23 | }, 24 | }, 25 | ] 26 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@range__change_minimum_and_maximum.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | maximum: 2 7 | minimum: 1 8 | type: number 9 | rhs: 10 | maximum: 2.5 11 | minimum: 1.5 12 | type: number 13 | input_file: tests/fixtures/range/change_minimum_and_maximum.json 14 | --- 15 | [ 16 | Change { 17 | path: "", 18 | change: RangeChange { 19 | old_value: Minimum( 20 | 1.0, 21 | ), 22 | new_value: Minimum( 23 | 1.5, 24 | ), 25 | }, 26 | }, 27 | Change { 28 | path: "", 29 | change: RangeChange { 30 | old_value: Maximum( 31 | 2.0, 32 | ), 33 | new_value: Maximum( 34 | 2.5, 35 | ), 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@range__minimum_to_exclusive_maximum.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | minimum: 1 7 | type: number 8 | rhs: 9 | exclusiveMaximum: 1 10 | type: number 11 | input_file: tests/fixtures/range/minimum_to_exclusive_maximum.json 12 | --- 13 | [ 14 | Change { 15 | path: "", 16 | change: RangeRemove { 17 | removed: Minimum( 18 | 1.0, 19 | ), 20 | }, 21 | }, 22 | Change { 23 | path: "", 24 | change: RangeAdd { 25 | added: ExclusiveMaximum( 26 | 1.0, 27 | ), 28 | }, 29 | }, 30 | ] 31 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@range__redundant_minimum_1.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | exclusiveMinimum: 1 7 | minimum: 1 8 | type: number 9 | rhs: 10 | exclusiveMinimum: 0.4 11 | type: number 12 | input_file: tests/fixtures/range/redundant_minimum_1.json 13 | --- 14 | [ 15 | Change { 16 | path: "", 17 | change: RangeChange { 18 | old_value: ExclusiveMinimum( 19 | 1.0, 20 | ), 21 | new_value: ExclusiveMinimum( 22 | 0.4, 23 | ), 24 | }, 25 | }, 26 | ] 27 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@range__redundant_minimum_2.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | exclusiveMinimum: 2 7 | minimum: 1 8 | type: number 9 | rhs: 10 | exclusiveMinimum: 0.4 11 | type: number 12 | input_file: tests/fixtures/range/redundant_minimum_2.json 13 | --- 14 | [ 15 | Change { 16 | path: "", 17 | change: RangeChange { 18 | old_value: ExclusiveMinimum( 19 | 2.0, 20 | ), 21 | new_value: ExclusiveMinimum( 22 | 0.4, 23 | ), 24 | }, 25 | }, 26 | ] 27 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@range__redundant_minimum_3.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | exclusiveMinimum: 0.5 7 | minimum: 1 8 | type: number 9 | rhs: 10 | exclusiveMinimum: 0.4 11 | type: number 12 | input_file: tests/fixtures/range/redundant_minimum_3.json 13 | --- 14 | [ 15 | Change { 16 | path: "", 17 | change: RangeChange { 18 | old_value: Minimum( 19 | 1.0, 20 | ), 21 | new_value: ExclusiveMinimum( 22 | 0.4, 23 | ), 24 | }, 25 | }, 26 | ] 27 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@range__remove_maximum.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | maximum: 1 7 | type: number 8 | rhs: 9 | type: number 10 | input_file: tests/fixtures/range/remove_maximum.json 11 | --- 12 | [ 13 | Change { 14 | path: "", 15 | change: RangeRemove { 16 | removed: Maximum( 17 | 1.0, 18 | ), 19 | }, 20 | }, 21 | ] 22 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@range__remove_minimum.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | minimum: 1 7 | type: number 8 | rhs: 9 | type: number 10 | input_file: tests/fixtures/range/remove_minimum.json 11 | --- 12 | [ 13 | Change { 14 | path: "", 15 | change: RangeRemove { 16 | removed: Minimum( 17 | 1.0, 18 | ), 19 | }, 20 | }, 21 | ] 22 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@range__to_exclusive_maximum.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | maximum: 1 7 | type: number 8 | rhs: 9 | exclusiveMaximum: 1 10 | type: number 11 | input_file: tests/fixtures/range/to_exclusive_maximum.json 12 | --- 13 | [ 14 | Change { 15 | path: "", 16 | change: RangeChange { 17 | old_value: Maximum( 18 | 1.0, 19 | ), 20 | new_value: ExclusiveMaximum( 21 | 1.0, 22 | ), 23 | }, 24 | }, 25 | ] 26 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@range__to_exclusive_minimum.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | minimum: 1 7 | type: number 8 | rhs: 9 | exclusiveMinimum: 1 10 | type: number 11 | input_file: tests/fixtures/range/to_exclusive_minimum.json 12 | --- 13 | [ 14 | Change { 15 | path: "", 16 | change: RangeChange { 17 | old_value: Minimum( 18 | 1.0, 19 | ), 20 | new_value: ExclusiveMinimum( 21 | 1.0, 22 | ), 23 | }, 24 | }, 25 | ] 26 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@range__unchanged_minimum.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | minimum: 1.3 7 | type: number 8 | rhs: 9 | minimum: 1.3 10 | type: number 11 | input_file: tests/fixtures/range/unchanged_minimum.json 12 | --- 13 | [] 14 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@ref__factor_out_definitions.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | type: object 7 | rhs: 8 | $ref: "#/definitions/Hello" 9 | definitions: 10 | Hello: 11 | type: object 12 | input_file: tests/fixtures/ref/factor_out_definitions.json 13 | --- 14 | [] 15 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@ref__factor_out_definitions_and_change.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | type: object 7 | rhs: 8 | $ref: "#/definitions/Hello" 9 | definitions: 10 | Hello: 11 | type: array 12 | input_file: tests/fixtures/ref/factor_out_definitions_and_change.json 13 | --- 14 | [ 15 | Change { 16 | path: "", 17 | change: TypeRemove { 18 | removed: Object, 19 | }, 20 | }, 21 | Change { 22 | path: "", 23 | change: TypeAdd { 24 | added: Array, 25 | }, 26 | }, 27 | ] 28 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@required__add.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | required: [] 7 | rhs: 8 | required: 9 | - value 10 | input_file: tests/fixtures/required/add.json 11 | --- 12 | [ 13 | Change { 14 | path: "", 15 | change: RequiredAdd { 16 | property: "value", 17 | }, 18 | }, 19 | ] 20 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@required__drop.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | required: 7 | - value 8 | rhs: 9 | required: [] 10 | input_file: tests/fixtures/required/drop.json 11 | --- 12 | [ 13 | Change { 14 | path: "", 15 | change: RequiredRemove { 16 | property: "value", 17 | }, 18 | }, 19 | ] 20 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@type__basic.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | type: string 7 | rhs: 8 | type: number 9 | input_file: tests/fixtures/type/basic.json 10 | --- 11 | [ 12 | Change { 13 | path: "", 14 | change: TypeRemove { 15 | removed: String, 16 | }, 17 | }, 18 | Change { 19 | path: "", 20 | change: TypeAdd { 21 | added: Number, 22 | }, 23 | }, 24 | Change { 25 | path: "", 26 | change: TypeAdd { 27 | added: Integer, 28 | }, 29 | }, 30 | ] 31 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@type__extend_from_const.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | const: hello 7 | rhs: 8 | type: 9 | - string 10 | input_file: tests/fixtures/type/extend_from_const.json 11 | --- 12 | [ 13 | Change { 14 | path: "", 15 | change: ConstRemove { 16 | removed: String("hello"), 17 | }, 18 | }, 19 | ] 20 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@type__extend_type.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | type: string 7 | rhs: 8 | type: 9 | - string 10 | - number 11 | input_file: tests/fixtures/type/extend_type.json 12 | --- 13 | [ 14 | Change { 15 | path: "", 16 | change: TypeAdd { 17 | added: Number, 18 | }, 19 | }, 20 | Change { 21 | path: "", 22 | change: TypeAdd { 23 | added: Integer, 24 | }, 25 | }, 26 | ] 27 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@type__integer_to_number.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | type: integer 7 | rhs: 8 | type: number 9 | input_file: tests/fixtures/type/integer_to_number.json 10 | --- 11 | [ 12 | Change { 13 | path: "", 14 | change: TypeAdd { 15 | added: Number, 16 | }, 17 | }, 18 | ] 19 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@type__nothing.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | type: string 7 | rhs: 8 | type: string 9 | input_file: tests/fixtures/type/nothing.json 10 | --- 11 | [] 12 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@type__number_to_integer.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | type: number 7 | rhs: 8 | type: integer 9 | input_file: tests/fixtures/type/number_to_integer.json 10 | --- 11 | [ 12 | Change { 13 | path: "", 14 | change: TypeRemove { 15 | removed: Number, 16 | }, 17 | }, 18 | ] 19 | -------------------------------------------------------------------------------- /tests/snapshots/test__from_fixtures@type__restrict_to_const.json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/test.rs 3 | expression: diff 4 | info: 5 | lhs: 6 | type: 7 | - string 8 | rhs: 9 | const: hello 10 | input_file: tests/fixtures/type/restrict_to_const.json 11 | --- 12 | [ 13 | Change { 14 | path: "", 15 | change: ConstAdd { 16 | added: String("hello"), 17 | }, 18 | }, 19 | ] 20 | -------------------------------------------------------------------------------- /tests/test.rs: -------------------------------------------------------------------------------- 1 | use insta::{assert_debug_snapshot, glob, with_settings}; 2 | use json_schema_diff::diff; 3 | use serde_json::Value; 4 | 5 | #[test] 6 | fn test_from_fixtures() { 7 | let test = |path: &std::path::Path| { 8 | let contents = std::fs::read_to_string(path).unwrap(); 9 | let values: Value = serde_json::from_str(&contents).unwrap(); 10 | let diff = diff(values["lhs"].clone(), values["rhs"].clone()).unwrap(); 11 | with_settings!({ info => &values }, { 12 | assert_debug_snapshot!(diff); 13 | }); 14 | }; 15 | glob!("../tests/fixtures", "**/*.json", test); 16 | } 17 | --------------------------------------------------------------------------------