diff --git a/CHANGELOG.md b/CHANGELOG.md index 378da0f..c641353 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ ## Unreleased - ReleaseDate +### Added + +- Drift now returns every endpoint (and corresponding path) from which a particular changed type is accessible, not just the first. + +### Changed + +- For every endpoint or component that had any number of changes in it, a single `Change` instance is now returned. `Change` now consists of a `Vec` and a `Vec`. + ## [0.1.4] - 2026-05-06 - evaluate oneOf <-> enum equivalency diff --git a/src/change.rs b/src/change.rs index d8b0974..e48b21a 100644 --- a/src/change.rs +++ b/src/change.rs @@ -1,20 +1,49 @@ // Copyright 2026 Oxide Computer Company -use std::fmt; - use crate::JsonPathStack; -// Describes any change detected between two OpenAPI documents. +/// A paired path through old and new documents that led to a change. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ChangePath { + /// Path in the old document. + pub old: JsonPathStack, + /// Path in the new document. + pub new: JsonPathStack, + /// Context in which this path was traversed (Input or Output). + pub comparison: ChangeComparison, +} + +/// Describes changes detected within a single component or endpoint. +#[derive(Debug)] pub struct Change { + /// All paths through which this component/endpoint was reached. + /// + /// For schema changes, this may contain multiple paths if the same schema + /// is referenced from multiple locations. For endpoint changes, this will + /// typically contain a single path. + pub paths: Vec, + + /// Individual changes detected within this component/endpoint. + pub changes: Vec, +} + +/// A single change detected at a specific location within a component/endpoint. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ChangeInfo { + /// The relative path in the old document where the change occurred. + /// + /// For example, `properties/name` for a change at + /// `#/components/schemas/User/properties/name`. + /// + /// This is an empty string if the change is at the component/endpoint root + /// itself. + pub old_subpath: String, + + /// The relative path in the new document where the change occurred. + pub new_subpath: String, + /// Human-readable message describing the nature of the change. pub message: String, - /// The path in the old document where the change was detected. - pub old_path: JsonPathStack, - /// The path in the new document where the change was detected. - pub new_path: JsonPathStack, - - /// The way in which the relevant structures during the comparison. - pub comparison: ChangeComparison, /// Classification of the change compatibility. pub class: ChangeClass, @@ -23,88 +52,7 @@ pub struct Change { pub details: ChangeDetails, } -// Format `Change` in the nested `paths`/`changes` layout that will be -// introduced when the type is restructured into `Change`, `ChangePath`, and -// `ChangeInfo`. Doing this ahead of time keeps the restructuring commit free -// of test-output noise. -impl fmt::Debug for Change { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - /// Wrapper that formats as `ChangePath { old, new, comparison }`. - struct PathFmt<'a> { - old: &'a JsonPathStack, - new: &'a JsonPathStack, - comparison: &'a ChangeComparison, - } - - impl fmt::Debug for PathFmt<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ChangePath") - .field("old", self.old) - .field("new", self.new) - .field("comparison", self.comparison) - .finish() - } - } - - /// Wrapper that formats as `ChangeInfo { old_subpath, new_subpath, - /// message, class, details }`. - struct InfoFmt<'a> { - message: &'a str, - class: &'a ChangeClass, - details: &'a ChangeDetails, - } - - impl fmt::Debug for InfoFmt<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ChangeInfo") - .field("old_subpath", &"") - .field("new_subpath", &"") - .field("message", &self.message) - .field("class", self.class) - .field("details", self.details) - .finish() - } - } - - let path = PathFmt { - old: &self.old_path, - new: &self.new_path, - comparison: &self.comparison, - }; - let info = InfoFmt { - message: &self.message, - class: &self.class, - details: &self.details, - }; - - f.debug_struct("Change") - .field("paths", &[path]) - .field("changes", &[info]) - .finish() - } -} - -impl Change { - pub fn new( - message: impl ToString, - old_path: JsonPathStack, - new_path: JsonPathStack, - comparison: ChangeComparison, - class: ChangeClass, - details: ChangeDetails, - ) -> Self { - Self { - message: message.to_string(), - old_path, - new_path, - comparison, - class, - details, - } - } -} - -#[derive(Debug)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ChangeComparison { // Inputs such as operation parameters and request bodies. Input, @@ -115,7 +63,14 @@ pub enum ChangeComparison { Structural, } -#[derive(Debug)] +/// Classification of how a change affects compatibility between two OpenAPI +/// documents. +/// +/// The `Ord` implementation is purely so that a `ChangeClass` can live in +/// ordered collections. It carries no semantic meaning otherwise. In +/// particular, `Ord` doesn't sort by severity. Use pattern matching or `==` +/// when considering severity. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ChangeClass { BackwardIncompatible, ForwardIncompatible, @@ -124,7 +79,7 @@ pub enum ChangeClass { Unhandled, } -#[derive(Debug)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ChangeDetails { Metadata, Added, diff --git a/src/compare.rs b/src/compare.rs index c674f2c..97fb122 100644 --- a/src/compare.rs +++ b/src/compare.rs @@ -3,14 +3,14 @@ use std::collections::BTreeMap; use anyhow::Context as _; -use indexmap::IndexMap; +use indexmap::{IndexMap, IndexSet}; use openapiv3::{ MediaType, Operation, Parameter, ParameterSchemaOrContent, ReferenceOr, RequestBody, }; use serde_json::Value; use crate::{ - Change, ChangeClass, ChangeComparison, ChangeDetails, JsonPathStack, + Change, ChangeClass, ChangeComparison, ChangeDetails, ChangeInfo, ChangePath, JsonPathStack, context::{Context, Contextual, ToContext}, operations::{all_params, operations}, resolve::ReferenceOrResolver, @@ -22,14 +22,54 @@ pub fn compare(old: &Value, new: &Value) -> anyhow::Result> { let mut comp = Compare::default(); comp.compare(old, new)?; - Ok(comp.changes) + let mut changes = Vec::new(); + + for record in comp.records.into_values() { + if !record.changes.is_empty() { + assert!( + !record.paths.is_empty(), + "ChangeRecord has changes so it should have paths" + ); + changes.push(Change { + paths: record.paths.into_iter().collect(), + changes: record.changes.into_iter().collect(), + }); + } + } + + Ok(changes) +} + +/// The base path of a location in a document, excluding appended segments. +/// +/// For a `JsonPathStack` at `#/components/schemas/User/properties/name`, this +/// captures `#/components/schemas/User`. Wrapped in a newtype to prevent +/// confusion with `CurrentPointer` (which includes appended segments). +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) struct BasePath(String); + +impl BasePath { + pub(crate) fn new(stack: &JsonPathStack) -> Self { + let (base, _) = stack.base_and_subpath(); + Self(base.to_string()) + } +} + +/// Key identifying a change location in both documents. +/// +/// This key identifies a pair of corresponding base paths (one from each +/// document), such as endpoints or named schemas in `#/components/schemas/`. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub(crate) struct ChangeKey { + old: BasePath, + new: BasePath, } /// The full current location in a document, including appended segments. /// /// This is the path returned by `JsonPathStack::current_pointer()`, wrapped -/// in a newtype to prevent confusion with other path-like strings (e.g. a -/// future base path type that excludes appended segments). +/// in a newtype to prevent confusion with `BasePath` (which excludes appended +/// segments). #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub(crate) struct CurrentPointer(String); @@ -80,11 +120,57 @@ pub(crate) enum VisitState { }, } +/// Tracks all paths reaching a change location and detected changes within it. +#[derive(Debug)] +pub(crate) struct ChangeRecord { + paths: IndexSet, + // Use an IndexSet here to dedup change info. For types shared between + // requests and responses, we run through them twice: once with Input and + // once with Output. Those could potentially produce the same change, which + // is technically correct but confusing to the user. + // + // In the future, we can also do a post-processing step: if the same change + // is reported as both ForwardIncompatible and BackwardIncompatible, we mark + // it as (bidirectionally) Incompatible. + changes: IndexSet, +} + +/// Tracks state across the comparison of two OpenAPI documents. +/// +/// This struct uses two maps with deliberately different keying strategies: +/// +/// - `visit_state` is keyed by **full path** (`current_pointer`), e.g., +/// `(SubType/properties/value, SubType/properties/value)`. This memoizes +/// individual node comparisons: "have we already compared these exact +/// schema nodes?" Full paths are required because comparing a schema at +/// its root is a different operation from comparing one of its children. +/// If `visit_state` used base paths, recursing into +/// `SubType/properties/value` would find the entry for `SubType` and +/// return early, silently skipping the entire subtree. +/// +/// - `records` is keyed by **base path** only, e.g., `{SubType, SubType}`. +/// This groups changes by the component or endpoint that owns them. A +/// type change at `SubType/properties/value` belongs to the `SubType` +/// change record alongside root-level metadata changes. Multiple +/// `visit_state` entries are collated into a single `records` entry. +/// +/// `visit_state` also includes the comparison direction (Input vs Output) +/// because that changes compatibility: adding an optional field is +/// backward-compatible for output but forward-incompatible for input. +/// `records` omits direction; a single Change groups all changes to a schema +/// regardless of direction, with the direction preserved in each +/// ChangePath's `comparison` field. +/// +/// When a schema moves between `$ref` (out-of-line) and inline across +/// versions, the full paths diverge, producing distinct `visit_state` entries +/// and potentially distinct `records` entries. See the comment in +/// `compare_schema` and the `ref-vs-inline-type-change` test for details. #[derive(Default)] pub(crate) struct Compare { - pub changes: Vec, /// State of every schema comparison we've started. - pub visit_state: BTreeMap, + pub(crate) visit_state: BTreeMap, + /// Change records grouped by base path pair (component or endpoint). + records: BTreeMap, } impl Compare { @@ -105,7 +191,7 @@ impl Compare { None => "", }; let new_paths_root = Context::for_paths_root(new); - self.push_change( + self.record_change( format!("The operation {op_name} was removed"), &op_info.operation, &new_paths_root, @@ -122,7 +208,7 @@ impl Compare { None => "", }; let old_paths_root = Context::for_paths_root(old); - self.push_change( + self.record_change( format!("The operation {op_name} was added"), &old_paths_root, &op_info.operation, @@ -177,7 +263,7 @@ impl Compare { // incompatible. for old_param in a_unique.values() { let param_name = &old_param.parameter_data_ref().name; - self.push_change( + self.record_change( format!("The parameter '{param_name}' was removed"), old_param, &new_operation.operation, @@ -193,7 +279,7 @@ impl Compare { for new_param in b_unique.values() { let param_name = &new_param.parameter_data_ref().name; if new_param.parameter_data_ref().required { - self.push_change( + self.record_change( format!("A new, required parameter '{param_name}' was added"), &old_operation.operation, new_param, @@ -202,7 +288,7 @@ impl Compare { ChangeDetails::AddedRequired, ); } else { - self.push_change( + self.record_change( format!("A new, optional parameter '{param_name}' was added"), &old_operation.operation, new_param, @@ -220,7 +306,7 @@ impl Compare { // A parameter that is "more" required is backward incompatible. if !old_param_data.required && new_param_data.required { - self.push_change( + self.record_change( format!("The parameter '{param_name}' was optional and is now required"), old_param, new_param, @@ -232,7 +318,7 @@ impl Compare { // A parameter that is "less" required is forward incompatible. if old_param_data.required && !new_param_data.required { - self.push_change( + self.record_change( format!("The parameter '{param_name}' was required and is now optional"), old_param, new_param, @@ -257,7 +343,7 @@ impl Compare { (old, new) if old == new => {} _ => { - self.push_change( + self.record_change( "Unhandled change to parameter schema or content", old_param, new_param, @@ -294,7 +380,7 @@ impl Compare { if old_body.required { // Old clients will send a required body that new servers // don't expect. - self.push_change( + self.record_change( "a required body parameter was removed", old_operation, new_operation, @@ -305,7 +391,7 @@ impl Compare { } else { // Old clients may send an optional body that new servers // don't expect. - self.push_change( + self.record_change( "an optional body parameter was removed", old_operation, new_operation, @@ -325,7 +411,7 @@ impl Compare { // This is very much like changing the type of a parameter: // previously the body was "nothing" and now it must be // "something". - self.push_change( + self.record_change( "no body parameter was specified and now one is required", old_operation, new_operation, @@ -338,7 +424,7 @@ impl Compare { // clients will not send the body; it is // forward-incompatible because new clients might try to // send a body to an old server. - self.push_change( + self.record_change( "no body parameter was specified and now one is accepted", old_operation, new_operation, @@ -370,7 +456,7 @@ impl Compare { // Description and extension changes are trivial metadata changes. if old_description != new_description || old_extensions != new_extensions { - self.push_change( + self.record_change( "the body metadata (description or extensions) changed", old_operation, new_operation, @@ -381,7 +467,7 @@ impl Compare { } if !*old_required && *new_required { - self.push_change( + self.record_change( "the body parameter was optional and is now required", old_operation, new_operation, @@ -390,7 +476,7 @@ impl Compare { ChangeDetails::MoreStrict, ); } else if *old_required && !*new_required { - self.push_change( + self.record_change( "the body parameter was required and is now optional", old_operation, new_operation, @@ -429,7 +515,7 @@ impl Compare { // Considering the impact of a default response is complex and // requires us to consider other responses... and to perhaps // apply heuristic knowledge. - self.push_change( + self.record_change( "operation added a default response", old_operation, new_operation, @@ -441,7 +527,7 @@ impl Compare { (Some(_), None) => { // Fewer responses is always backward-compatible, but // considering the full impact is, again, complex. - self.push_change( + self.record_change( "operation removed a default response", old_operation, new_operation, @@ -483,7 +569,7 @@ impl Compare { // It's forward-incompatible for an operation to have responses it no // longer sends for old_status in a_unique.keys() { - self.push_change( + self.record_change( format!("operation no longer responds with status {old_status}"), old_operation, new_operation, @@ -496,7 +582,7 @@ impl Compare { // Adding a new response would break old clients that don't expect it // and is therefore backward-incompatible. for new_status in b_unique.keys() { - self.push_change( + self.record_change( format!("operation added a new response code {new_status}"), old_operation, new_operation, @@ -587,7 +673,58 @@ impl Compare { Ok(()) } - pub(crate) fn push_change( + /// Get or create a `ChangeRecord` for the given path stacks, inserting + /// the base path into the record's path set. + fn ensure_record( + &mut self, + old_path: &JsonPathStack, + new_path: &JsonPathStack, + comparison: ChangeComparison, + ) -> &mut ChangeRecord { + let key = ChangeKey { + old: BasePath::new(old_path), + new: BasePath::new(new_path), + }; + + let record = self.records.entry(key).or_insert_with(|| ChangeRecord { + paths: IndexSet::new(), + changes: IndexSet::new(), + }); + + // Store base-only paths (subpath stripped). IndexSet handles + // deduplication. + record.paths.insert(ChangePath { + old: old_path.without_subpath(), + new: new_path.without_subpath(), + comparison, + }); + + record + } + + /// Record a path reaching a change location without recording a new change. + /// + /// This is used when entering a named type to ensure all paths to the type + /// are recorded, even if we hit the memoization cache and don't record + /// the change again. + pub(crate) fn record_path( + &mut self, + old: &dyn ToContext<'_>, + new: &dyn ToContext<'_>, + comparison: ChangeComparison, + ) { + let old_path = old.to_context().stack(); + let new_path = new.to_context().stack(); + self.ensure_record(old_path, new_path, comparison); + } + + /// Record a change at the given paths. + /// + /// This is the unified change recording method. It extracts the base path + /// from each path stack and uses it as a key to group related changes. + /// The path (truncated to base) is added to the record's path set, and the + /// change info is added to the record's changes list. + pub(crate) fn record_change( &mut self, message: impl ToString, old: &dyn ToContext<'_>, @@ -596,15 +733,19 @@ impl Compare { class: ChangeClass, details: ChangeDetails, ) { - let change = Change::new( - message, - old.to_context().stack.clone(), - new.to_context().stack.clone(), - comparison, + let old_path = old.to_context().stack(); + let new_path = new.to_context().stack(); + let (_, old_subpath) = old_path.base_and_subpath(); + let (_, new_subpath) = new_path.base_and_subpath(); + + let record = self.ensure_record(old_path, new_path, comparison); + + record.changes.insert(ChangeInfo { + old_subpath: old_subpath.to_string(), + new_subpath: new_subpath.to_string(), + message: message.to_string(), class, details, - ); - - self.changes.push(change); + }); } } diff --git a/src/operations.rs b/src/operations.rs index 3874cb6..2f779d5 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -48,6 +48,9 @@ pub fn operations(raw_openapi: &Value) -> anyhow::Result for resolving path item refs. + // This uses `for_path` (not `for_operation`), so the base is the + // path item, not a specific operation. Shared parameters resolved + // from this context will group changes at the path-item level. let path_endpoint = EndpointPath::for_path(path); let path_context = Context::for_endpoint(raw_openapi, path_endpoint); let (path_item, path_context) = ref_or_operation.resolve(&path_context)?; @@ -59,7 +62,7 @@ pub fn operations(raw_openapi: &Value) -> anyhow::Result/ - let endpoint = EndpointPath::for_path(path).append(method); + let endpoint = EndpointPath::for_operation(path, method); let context = Context::for_endpoint(raw_openapi, endpoint); let op_key = OperationKey::new(path, method); diff --git a/src/path.rs b/src/path.rs index 99631f2..4bbd381 100644 --- a/src/path.rs +++ b/src/path.rs @@ -53,27 +53,86 @@ impl std::error::Error for InvalidComponentRef {} /// An endpoint path, guaranteed to start with `#/paths/`. /// /// This represents a location within the paths section of an OpenAPI document. -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct EndpointPath(String); +/// +/// The `base_len` field stores the length of the path when first constructed +/// (via `for_path` or `for_operation`), before any `append` calls. This allows +/// retrieval of the endpoint base path after segments have been appended. +/// +/// Note that `for_path` and `for_operation` produce different base lengths: +/// `for_path("/users")` has base `#/paths/~1users`, while +/// `for_operation("/users", "get")` has base `#/paths/~1users/get`. This +/// distinction matters for change grouping: shared parameters (which live at +/// the path-item level) group under the path-item base, while operation-level +/// constructs group under the operation base. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct EndpointPath { + path: String, + /// Length of the path at construction time, before any appends. + base_len: usize, +} impl EndpointPath { - /// Create an endpoint path from a raw API path (like `/users/{id}`). + /// Create an endpoint path for a path item (without a specific operation). /// - /// This escapes the path according to JSON pointer rules (RFC 6901). + /// This is used for path-level constructs like shared parameters. (Oxide's + /// OpenAPI documents do not have path-level constructs.) pub(crate) fn for_path(api_path: &str) -> Self { let escaped = escape_json_pointer_segment(api_path); - Self(format!("#/paths/{}", escaped)) + let path = format!("#/paths/{}", escaped); + Self { + base_len: path.len(), + path, + } + } + + /// Create an endpoint path for an operation. + /// + /// The `api_path` is the raw API path (like `/users/{id}`) and `method` is + /// the HTTP method (like `get`). The `api_path` is escaped per JSON pointer + /// rules; `method` is expected to not need escaping, and is used as-is. + pub(crate) fn for_operation(api_path: &str, method: &str) -> Self { + let escaped_path = escape_json_pointer_segment(api_path); + let path = format!("#/paths/{}/{}", escaped_path, method); + Self { + base_len: path.len(), + path, + } } /// Append a path segment, escaping special characters per RFC 6901. pub(crate) fn append(&self, segment: &str) -> Self { let escaped = escape_json_pointer_segment(segment); - Self(format!("{}/{}", self.0, escaped)) + Self { + path: format!("{}/{}", self.path, escaped), + base_len: self.base_len, + } } /// Get the JSON pointer string. fn as_str(&self) -> &str { - &self.0 + &self.path + } + + /// Get the base path (the path at construction time, before any appends). + fn base_path(&self) -> &str { + &self.path[..self.base_len] + } + + /// Get the subpath (the portion appended after construction). + /// + /// Returns an empty string if no segments have been appended. + fn subpath(&self) -> &str { + let sub = &self.path[self.base_len..]; + // Strip the leading '/' if present. + sub.strip_prefix('/').unwrap_or(sub) + } + + /// Return a new EndpointPath with the subpath stripped. + fn without_subpath(&self) -> Self { + Self { + path: self.base_path().to_owned(), + base_len: self.base_len, + } } } @@ -81,24 +140,64 @@ impl EndpointPath { /// /// This represents a location after following at least one `$ref`. It can point /// to any location in the document, not only components. +/// +/// The `base_len` field stores the length of the path when first constructed +/// (via `parse`), before any `append` calls. This allows retrieval of the +/// original ref target (e.g., the named type) after path segments have been +/// appended. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -struct RefTargetPath(String); +struct RefTargetPath { + path: String, + /// Length of the path at construction time, before any appends. + base_len: usize, +} impl RefTargetPath { /// Parse from a JSON pointer. Returns `None` if not a valid local ref. fn parse(pointer: &str) -> Option { - pointer.starts_with("#/").then(|| Self(pointer.to_string())) + pointer.starts_with("#/").then(|| Self { + base_len: pointer.len(), + path: pointer.to_string(), + }) } /// Append a path segment, escaping special characters per RFC 6901. fn append(&self, segment: &str) -> Self { let escaped = escape_json_pointer_segment(segment); - Self(format!("{}/{}", self.0, escaped)) + Self { + path: format!("{}/{}", self.path, escaped), + base_len: self.base_len, + } } /// Get the JSON pointer string. fn as_str(&self) -> &str { - &self.0 + &self.path + } + + /// Get the base path (the path at construction time, before any appends). + /// + /// For a path created from `#/components/schemas/User` and then appended + /// with `properties`, this returns `#/components/schemas/User`. + fn base_path(&self) -> &str { + &self.path[..self.base_len] + } + + /// Get the subpath (the portion appended after construction). + /// + /// Returns an empty string if no segments have been appended. + fn subpath(&self) -> &str { + let sub = &self.path[self.base_len..]; + // Strip the leading '/' if present. + sub.strip_prefix('/').unwrap_or(sub) + } + + /// Return a new RefTargetPath with the subpath stripped. + fn without_subpath(&self) -> Self { + Self { + path: self.base_path().to_owned(), + base_len: self.base_len, + } } } @@ -107,7 +206,7 @@ impl RefTargetPath { /// - If refs have been followed, the first was always from an endpoint. /// - All subsequent refs form a chain of intermediate locations. /// - Transitions: endpoint → ref target, or ref target → ref target. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] enum PathState { /// At the paths root (`#/paths`), used only for operation add/remove reporting. /// Cannot follow refs from this state. @@ -132,7 +231,7 @@ enum PathState { /// /// The stack tracks the location while traversing an OpenAPI document, /// particularly when following `$ref` references. -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct JsonPathStack { state: PathState, } @@ -186,6 +285,76 @@ impl JsonPathStack { } } + /// Get the base component path (the ref target before any appends). + /// + /// Returns `Some` when at a component (after following a `$ref`), containing + /// the original ref target. For example, if we followed a ref to + /// `#/components/schemas/User` and then appended `properties/name`, this + /// returns `Some("#/components/schemas/User")`. + /// + /// Returns `None` when at an endpoint or paths root (no ref followed). + pub fn component_base(&self) -> Option<&str> { + match &self.state { + PathState::PathsRoot | PathState::AtEndpoint(_) => None, + PathState::AtComponent { current, .. } => Some(current.base_path()), + } + } + + /// Get the base path and subpath for this location. + /// + /// The base is the primary entity being tracked: + /// + /// - For `PathsRoot`: `#/paths` with empty subpath. + /// - For endpoints: the endpoint path (e.g., `#/paths/~1users/get`). + /// - For components: the component path (e.g., `#/components/schemas/User`). + /// + /// The subpath is the relative path from the base to the current location + /// (e.g., `parameters/0` or `properties/name`). Empty if at the base itself. + pub fn base_and_subpath(&self) -> (&str, &str) { + match &self.state { + PathState::PathsRoot => ("#/paths", ""), + PathState::AtEndpoint(path) => (path.base_path(), path.subpath()), + PathState::AtComponent { current, .. } => (current.base_path(), current.subpath()), + } + } + + /// Return a new JsonPathStack with the subpath stripped from the current + /// location. + /// + /// This only affects the top of the stack (the current endpoint or + /// component); intermediate refs are preserved unchanged. + pub fn without_subpath(&self) -> Self { + let state = match &self.state { + PathState::PathsRoot => PathState::PathsRoot, + PathState::AtEndpoint(path) => PathState::AtEndpoint(path.without_subpath()), + PathState::AtComponent { + current, + origin_ref, + intermediate_refs, + } => PathState::AtComponent { + current: current.without_subpath(), + origin_ref: origin_ref.clone(), + intermediate_refs: intermediate_refs.clone(), + }, + }; + Self { state } + } + + /// Return the endpoint path this stack is at or originated from as a JSON + /// pointer, including the leading `#/paths`, but not including subpaths + /// within the endpoint. + /// + /// This can be used to enumerate all endpoints that have changes in them. + /// + /// Returns `None` if an endpoint was added or removed on the other side. + pub fn endpoint_base(&self) -> Option<&str> { + match &self.state { + PathState::PathsRoot => None, + PathState::AtEndpoint(path) => Some(path.base_path()), + PathState::AtComponent { origin_ref, .. } => Some(origin_ref.base_path()), + } + } + /// Append a path segment to the current location. /// /// This does not push a new reference; it extends the current path. @@ -318,6 +487,8 @@ mod tests { fn endpoint_path_for_path() { let path = EndpointPath::for_path("/users"); assert_eq!(path.as_str(), "#/paths/~1users"); + assert_eq!(path.base_path(), "#/paths/~1users"); + assert_eq!(path.subpath(), ""); } #[test] @@ -327,20 +498,28 @@ mod tests { assert_eq!(path.as_str(), "#/paths/~1users~1{id}~1posts"); } + #[test] + fn endpoint_path_for_operation() { + let path = EndpointPath::for_operation("/users", "get"); + assert_eq!(path.as_str(), "#/paths/~1users/get"); + assert_eq!(path.base_path(), "#/paths/~1users/get"); + assert_eq!(path.subpath(), ""); + } + #[test] fn endpoint_path_append() { - let path = EndpointPath::for_path("/users") - .append("get") - .append("responses"); + let path = EndpointPath::for_operation("/users", "get").append("responses"); assert_eq!(path.as_str(), "#/paths/~1users/get/responses"); + assert_eq!(path.base_path(), "#/paths/~1users/get"); + assert_eq!(path.subpath(), "responses"); } #[test] fn endpoint_path_append_escapes() { - let path = EndpointPath::for_path("/users") + let path = EndpointPath::for_operation("/users", "get") .append("foo/bar") .append("a~b"); - assert_eq!(path.as_str(), "#/paths/~1users/foo~1bar/a~0b"); + assert_eq!(path.as_str(), "#/paths/~1users/get/foo~1bar/a~0b"); } #[test] @@ -376,9 +555,46 @@ mod tests { assert_eq!(path.as_str(), "#/components/schemas/User/properties/name"); } + #[test] + fn ref_target_path_base_and_subpath() { + let path = RefTargetPath::parse("#/components/schemas/User").unwrap(); + assert_eq!(path.base_path(), "#/components/schemas/User"); + assert_eq!(path.subpath(), ""); + + let appended = path.append("properties").append("name"); + assert_eq!(appended.base_path(), "#/components/schemas/User"); + assert_eq!(appended.subpath(), "properties/name"); + } + + #[test] + fn ref_target_path_without_subpath() { + let path = RefTargetPath::parse("#/components/schemas/User") + .unwrap() + .append("properties") + .append("name"); + + let base_only = path.without_subpath(); + assert_eq!(base_only.as_str(), "#/components/schemas/User"); + assert_eq!(base_only.base_path(), "#/components/schemas/User"); + assert_eq!(base_only.subpath(), ""); + } + + #[test] + fn endpoint_path_without_subpath() { + let path = EndpointPath::for_operation("/users", "get") + .append("responses") + .append("200"); + assert_eq!(path.subpath(), "responses/200"); + + let base_only = path.without_subpath(); + assert_eq!(base_only.as_str(), "#/paths/~1users/get"); + assert_eq!(base_only.base_path(), "#/paths/~1users/get"); + assert_eq!(base_only.subpath(), ""); + } + #[test] fn json_path_stack_for_endpoint() { - let endpoint = EndpointPath::for_path("/users").append("get"); + let endpoint = EndpointPath::for_operation("/users", "get"); let stack = JsonPathStack::for_endpoint(endpoint); assert_eq!(stack.current_pointer(), "#/paths/~1users/get"); @@ -396,7 +612,7 @@ mod tests { #[test] fn json_path_stack_append() { - let endpoint = EndpointPath::for_path("/users").append("get"); + let endpoint = EndpointPath::for_operation("/users", "get"); let stack = JsonPathStack::for_endpoint(endpoint) .append("responses") .append("200") @@ -411,7 +627,7 @@ mod tests { #[test] fn json_path_stack_push() { - let endpoint = EndpointPath::for_path("/users").append("get"); + let endpoint = EndpointPath::for_operation("/users", "get"); let stack = JsonPathStack::for_endpoint(endpoint) .append("responses") .append("200") @@ -430,7 +646,7 @@ mod tests { #[test] fn json_path_stack_iter_order() { - let endpoint = EndpointPath::for_path("/users").append("get"); + let endpoint = EndpointPath::for_operation("/users", "get"); let stack = JsonPathStack::for_endpoint(endpoint) .push("#/components/schemas/A") .unwrap() @@ -449,7 +665,7 @@ mod tests { #[test] fn json_path_stack_display() { - let endpoint = EndpointPath::for_path("/users").append("get"); + let endpoint = EndpointPath::for_operation("/users", "get"); let stack = JsonPathStack::for_endpoint(endpoint) .push("#/components/schemas/User") .unwrap(); @@ -463,7 +679,7 @@ mod tests { #[test] fn json_path_stack_push_invalid_ref_returns_error() { - let endpoint = EndpointPath::for_path("/users").append("get"); + let endpoint = EndpointPath::for_operation("/users", "get"); let stack = JsonPathStack::for_endpoint(endpoint); let err = stack @@ -477,4 +693,118 @@ mod tests { expected JSON pointer starting with #/" ); } + + #[test] + fn json_path_stack_base_and_subpath() { + // PathsRoot. + let stack = JsonPathStack::paths_root(); + assert_eq!(stack.base_and_subpath(), ("#/paths", "")); + + // Endpoint at base. + let endpoint = EndpointPath::for_operation("/users", "get"); + let stack = JsonPathStack::for_endpoint(endpoint); + assert_eq!(stack.base_and_subpath(), ("#/paths/~1users/get", "")); + + // Endpoint with appended segments. + let stack = stack.append("responses").append("200"); + assert_eq!( + stack.base_and_subpath(), + ("#/paths/~1users/get", "responses/200") + ); + + // Component at base. + let stack = stack + .append("schema") + .push("#/components/schemas/User") + .unwrap(); + assert_eq!(stack.base_and_subpath(), ("#/components/schemas/User", "")); + + // Component with appended segments. + let stack = stack.append("properties").append("name"); + assert_eq!( + stack.base_and_subpath(), + ("#/components/schemas/User", "properties/name") + ); + } + + #[test] + fn json_path_stack_without_subpath() { + // PathsRoot: no-op. + let stack = JsonPathStack::paths_root(); + assert_eq!(stack.without_subpath().current_pointer(), "#/paths"); + + // Endpoint with appended segments: strips back to base. + let endpoint = EndpointPath::for_operation("/users", "get"); + let stack = JsonPathStack::for_endpoint(endpoint) + .append("responses") + .append("200"); + let stripped = stack.without_subpath(); + assert_eq!(stripped.current_pointer(), "#/paths/~1users/get"); + assert_eq!(stripped.base_and_subpath(), ("#/paths/~1users/get", "")); + + // Component with appended segments: strips back to component base, + // preserving the reference chain. + let endpoint = EndpointPath::for_operation("/users", "get"); + let stack = JsonPathStack::for_endpoint(endpoint) + .push("#/components/schemas/User") + .unwrap() + .append("properties") + .append("name"); + let stripped = stack.without_subpath(); + assert_eq!(stripped.current_pointer(), "#/components/schemas/User"); + assert_eq!( + stripped.base_and_subpath(), + ("#/components/schemas/User", "") + ); + + // Reference chain is preserved after stripping. + let entries: Vec<_> = stripped.iter().collect(); + assert_eq!(entries.len(), 2); + assert_eq!(entries[0], "#/components/schemas/User"); + assert_eq!(entries[1], "#/paths/~1users/get/$ref"); + } + + #[test] + fn json_path_stack_component_base() { + // PathsRoot: no component base. + assert_eq!(JsonPathStack::paths_root().component_base(), None); + + // Endpoint: no component base. + let endpoint = EndpointPath::for_operation("/users", "get"); + let stack = JsonPathStack::for_endpoint(endpoint); + assert_eq!(stack.component_base(), None); + + // Component at base: returns the ref target. + let stack = stack.push("#/components/schemas/User").unwrap(); + assert_eq!(stack.component_base(), Some("#/components/schemas/User")); + + // Component with appended segments: still returns the ref target base. + let stack = stack.append("properties").append("name"); + assert_eq!(stack.component_base(), Some("#/components/schemas/User")); + } + + #[test] + fn json_path_stack_endpoint() { + let stack = JsonPathStack::paths_root(); + assert_eq!(stack.endpoint_base(), None); + + let endpoint = EndpointPath::for_operation("/users", "get"); + let stack = JsonPathStack::for_endpoint(endpoint); + assert_eq!(stack.endpoint_base(), Some("#/paths/~1users/get")); + + let stack = stack.append("responses").append("200"); + assert_eq!(stack.endpoint_base(), Some("#/paths/~1users/get")); + + let stack = stack + .append("schema") + .push("#/components/schemas/User") + .unwrap(); + assert_eq!(stack.endpoint_base(), Some("#/paths/~1users/get")); + + let stack = stack + .append("properties") + .push("#/components/schemas/Address") + .unwrap(); + assert_eq!(stack.endpoint_base(), Some("#/paths/~1users/get")); + } } diff --git a/src/schema.rs b/src/schema.rs index cdb2ebf..e011643 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -97,7 +97,7 @@ impl Compare { ) => { // Both old and new are single-element wrappers. if old_meta != new_meta && !dry_run { - self.push_change( + self.record_change( "schema metadata changed", old_schema, new_schema, @@ -125,7 +125,7 @@ impl Compare { // A bare ref or inline type does not have metadata, so if the // old metadata is non-default, report a trivial change. if has_meaningful_metadata(old_meta) && !dry_run { - self.push_change( + self.record_change( "schema metadata removed", old_schema, new_schema, @@ -155,7 +155,7 @@ impl Compare { // A bare ref or inline type does not have metadata, so if the // new metadata is non-default, report a trivial change. if has_meaningful_metadata(new_meta) && !dry_run { - self.push_change( + self.record_change( "schema metadata added", old_schema, new_schema, @@ -189,6 +189,57 @@ impl Compare { old_schema: Contextual<'_, &Schema>, new_schema: Contextual<'_, &Schema>, ) -> anyhow::Result { + // Record the access path when either side is at the root before .append + // has been called (subpath is empty). + // + // This must happen before the `visit_state` check below. When a named + // schema is referenced from multiple endpoints, the first visit runs + // the full comparison and populates `visit_state`. Subsequent visits + // find a `Completed` entry and return early, never reaching + // `record_change`. This `record_path` call is the only opportunity + // to capture those later access paths. + // + // Why `||` and not `&&`? A schema can move between out-of-line + // (`$ref` to a named type) and inline across versions. When that + // happens, one side is at a component root (empty subpath) and the + // other is at a subpath within an endpoint or parent schema. With + // `&&` this path would not be recorded; with `||` it is. + // + // For a concrete example, see the `ref-vs-inline-type-change` test: + // SubType.value changes from string to integer, and via_ref is inlined. + // + // When comparing via_ref, the old side resolves the `$ref` to SubType + // (empty subpath), while the new side is inline within GreetingResponse + // (subpath = `properties/via_ref`). Meanwhile, via_allof, via_anyof, + // etc. still reference SubType via $ref on both sides. + // + // `visit_state` is keyed on full paths (`current_pointer`), so the + // ref-vs-inline pair has a different VisitedKey from the ref-vs-ref + // paths. The ref-vs-inline pair is not found in `visit_state`, and the + // full comparison runs independently, producing a separate Change + // with ChangeKey {SubType, GreetingResponse}. This is correct: the + // two Changes describe the same semantic change from different + // structural perspectives (one scoped to SubType, the other to + // GreetingResponse). + // + // Because ref-vs-inline always produces a unique VisitedKey (the + // full paths differ), `||` vs `&&` is moot today: + // + // * When changes are detected, `record_change` records the path via + // `ensure_record`. + // * When no changes are detected, `compare()` discards empty + // ChangeRecords. + // + // We use `||` as the correct, logical semantic: if `visit_state` were + // keyed on base paths instead, the `||` would become load-bearing. + if !dry_run { + let (_, old_subpath) = old_schema.context().stack().base_and_subpath(); + let (_, new_subpath) = new_schema.context().stack().base_and_subpath(); + if old_subpath.is_empty() || new_subpath.is_empty() { + self.record_path(&old_schema, &new_schema, comparison.into()); + } + } + let key = VisitedKey::new( comparison, old_schema.context().stack(), @@ -203,14 +254,14 @@ impl Compare { // in-flight comparison wherever the cycle originates. // // Returning `true` on `Visiting` is sound only because the compare - // methods call `schema_push_change` eagerly on detecting a change + // methods call `schema_record_change` eagerly on detecting a change // rather than making decisions based on the value returned from this // function. If that weren't the case -- for example, if there were // code which did something like: // // let inner_eq = self.compare_schema(...)?; // if !inner_eq { - // self.schema_push_change(...); + // self.schema_record_change(...); // } // // Then, relying on `true` here would result in a false negative. @@ -279,7 +330,7 @@ impl Compare { && old_extensions == new_extensions; if !metadata_equal { - let _ = self.schema_push_change( + let _ = self.schema_record_change( dry_run, "schema metadata changed".to_string(), &old_schema, @@ -351,7 +402,7 @@ impl Compare { SchemaKind::AnyOf { any_of: new_any_of }, ) => { if old_any_of != new_any_of { - self.schema_push_change( + self.schema_record_change( dry_run, "unhandled, 'anyOf' schema", &old_schema_kind, @@ -373,7 +424,7 @@ impl Compare { if old_any == new_any { Ok(true) } else { - self.schema_push_change( + self.schema_record_change( dry_run, "schema kind 'any' changed", &old_schema_kind, @@ -401,7 +452,7 @@ impl Compare { { let old_tag = SchemaKindTag::new(&old_schema_kind); let new_tag = SchemaKindTag::new(&new_schema_kind); - return self.schema_push_change( + return self.schema_record_change( dry_run, format!( "schema kind changed from {} to {} with equivalent enum values", @@ -418,7 +469,7 @@ impl Compare { let old_tag = SchemaKindTag::new(&old_schema_kind); let new_tag = SchemaKindTag::new(&new_schema_kind); - self.schema_push_change( + self.schema_record_change( dry_run, format!("schema kind changed from {} to {}", old_tag, new_tag), &old_schema_kind, @@ -441,7 +492,7 @@ impl Compare { match (old_schema_type.as_ref(), new_schema_type.as_ref()) { (openapiv3::Type::String(old_string), openapiv3::Type::String(new_string)) => { if old_string != new_string { - self.schema_push_change( + self.schema_record_change( dry_run, "string schema changed", &old_schema_type, @@ -456,7 +507,7 @@ impl Compare { } (openapiv3::Type::Number(old_number), openapiv3::Type::Number(new_number)) => { if old_number != new_number { - self.schema_push_change( + self.schema_record_change( dry_run, "number schema changed", &old_schema_type, @@ -471,7 +522,7 @@ impl Compare { } (openapiv3::Type::Integer(old_integer), openapiv3::Type::Integer(new_integer)) => { if old_integer != new_integer { - self.schema_push_change( + self.schema_record_change( dry_run, "integer schema changed", &old_schema_type, @@ -486,7 +537,7 @@ impl Compare { } (openapiv3::Type::Boolean(old_boolean), openapiv3::Type::Boolean(new_boolean)) => { if old_boolean != new_boolean { - self.schema_push_change( + self.schema_record_change( dry_run, "boolean schema changed", &old_schema_type, @@ -513,7 +564,7 @@ impl Compare { old_schema_type.subcomponent(old_object), new_schema_type.subcomponent(new_object), ), - _ => self.schema_push_change( + _ => self.schema_record_change( dry_run, "schema types changed", &old_schema_type, @@ -549,7 +600,7 @@ impl Compare { if old_min_items != new_min_items { ret = false; - let _ = self.schema_push_change( + let _ = self.schema_record_change( dry_run, "array minItems changed", &old_array, @@ -562,7 +613,7 @@ impl Compare { if old_max_items != new_max_items { ret = false; - let _ = self.schema_push_change( + let _ = self.schema_record_change( dry_run, "array maxItems changed", &old_array, @@ -575,7 +626,7 @@ impl Compare { if old_unique_items != new_unique_items { ret = false; - let _ = self.schema_push_change( + let _ = self.schema_record_change( dry_run, "array uniqueItems changed", &old_array, @@ -598,7 +649,7 @@ impl Compare { (None, None) => {} _ => { ret = false; - let _ = self.schema_push_change( + let _ = self.schema_record_change( dry_run, "array items changed", &old_array, @@ -639,7 +690,7 @@ impl Compare { if old_required != new_required { ret = false; - let _ = self.schema_push_change( + let _ = self.schema_record_change( dry_run, "object required properties changed", &old_object, @@ -652,7 +703,7 @@ impl Compare { if old_min_properties != new_min_properties { ret = false; - let _ = self.schema_push_change( + let _ = self.schema_record_change( dry_run, "object minProperties changed", &old_object, @@ -665,7 +716,7 @@ impl Compare { if old_max_properties != new_max_properties { ret = false; - let _ = self.schema_push_change( + let _ = self.schema_record_change( dry_run, "object maxProperties changed", &old_object, @@ -705,7 +756,7 @@ impl Compare { _ => { ret = false; - let _ = self.schema_push_change( + let _ = self.schema_record_change( dry_run, "object additionalProperties changed", &old_object, @@ -725,7 +776,7 @@ impl Compare { if !a_unique.is_empty() || !b_unique.is_empty() { ret = false; - let _ = self.schema_push_change( + let _ = self.schema_record_change( dry_run, "object properties changed", &old_object, @@ -760,7 +811,7 @@ impl Compare { let old_schemas = old_one_of.as_ref(); let new_schemas = new_one_of.as_ref(); if old_schemas.len() != new_schemas.len() { - return self.schema_push_change( + return self.schema_record_change( dry_run, "oneOf schema count changed", &old_one_of, @@ -796,7 +847,7 @@ impl Compare { let old_schemas = old_all_of.as_ref(); let new_schemas = new_all_of.as_ref(); if old_schemas.len() != new_schemas.len() { - return self.schema_push_change( + return self.schema_record_change( dry_run, "allOf schema count changed", &old_all_of, @@ -820,7 +871,7 @@ impl Compare { } else if old_schemas == new_schemas { Ok(true) } else { - self.schema_push_change( + self.schema_record_change( dry_run, "allOf with multiple schemas is unhandled", &old_all_of, @@ -833,7 +884,7 @@ impl Compare { } #[allow(clippy::too_many_arguments)] - fn schema_push_change( + fn schema_record_change( &mut self, dry_run: bool, message: impl ToString, @@ -844,7 +895,7 @@ impl Compare { details: ChangeDetails, ) -> anyhow::Result { if !dry_run { - self.push_change(message, old, new, comparison.into(), class, details); + self.record_change(message, old, new, comparison.into(), class, details); } Ok(false) } diff --git a/tests/cases/cycle-detection/output/add-property-to-mutual-recursion.out b/tests/cases/cycle-detection/output/add-property-to-mutual-recursion.out index dd4bf4c..7cec858 100644 --- a/tests/cases/cycle-detection/output/add-property-to-mutual-recursion.out +++ b/tests/cases/cycle-detection/output/add-property-to-mutual-recursion.out @@ -29,6 +29,17 @@ Result for patch: ], comparison: Output, }, + ChangePath { + old: [ + "#/components/schemas/Person", + "#/paths/~1persons/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/Person", + "#/paths/~1persons/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, ], changes: [ ChangeInfo { diff --git a/tests/cases/cycle-detection/output/asymmetric-unroll-with-property-change.out b/tests/cases/cycle-detection/output/asymmetric-unroll-with-property-change.out index 7261d8a..dab12f0 100644 --- a/tests/cases/cycle-detection/output/asymmetric-unroll-with-property-change.out +++ b/tests/cases/cycle-detection/output/asymmetric-unroll-with-property-change.out @@ -38,37 +38,24 @@ Result for patch: ChangePath { old: [ "#/components/schemas/DirectLoop", - "#/components/schemas/DirectLoop/properties/next/$ref", "#/paths/~1direct-loop/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/LoopIndirect", - "#/components/schemas/DirectLoop/properties/next/$ref", + "#/components/schemas/DirectLoop", "#/paths/~1direct-loop/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, }, - ], - changes: [ - ChangeInfo { - old_subpath: "", - new_subpath: "", - message: "schema metadata changed", - class: Trivial, - details: Metadata, - }, - ], - }, - Change { - paths: [ ChangePath { old: [ "#/components/schemas/DirectLoop", "#/components/schemas/DirectLoop/properties/next/$ref", + "#/components/schemas/DirectLoop/properties/next/$ref", "#/paths/~1direct-loop/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/LoopIndirect", + "#/components/schemas/DirectLoop", + "#/components/schemas/LoopIndirect/properties/next/$ref", "#/components/schemas/DirectLoop/properties/next/$ref", "#/paths/~1direct-loop/get/responses/200/content/application~1json/schema/$ref", ], @@ -77,10 +64,10 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", - message: "object properties changed", - class: Unhandled, + old_subpath: "properties/value", + new_subpath: "properties/value", + message: "schema types changed", + class: Incompatible, details: UnknownDifference, }, ], @@ -89,11 +76,13 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/DirectLoop/properties/value", + "#/components/schemas/DirectLoop", + "#/components/schemas/DirectLoop/properties/next/$ref", "#/paths/~1direct-loop/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/DirectLoop/properties/value", + "#/components/schemas/LoopIndirect", + "#/components/schemas/DirectLoop/properties/next/$ref", "#/paths/~1direct-loop/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -103,8 +92,15 @@ Result for patch: ChangeInfo { old_subpath: "", new_subpath: "", - message: "schema types changed", - class: Incompatible, + message: "schema metadata changed", + class: Trivial, + details: Metadata, + }, + ChangeInfo { + old_subpath: "", + new_subpath: "", + message: "object properties changed", + class: Unhandled, details: UnknownDifference, }, ], diff --git a/tests/cases/cycle-detection/output/change-item-value-type.out b/tests/cases/cycle-detection/output/change-item-value-type.out index da50f5d..a1f4c9b 100644 --- a/tests/cases/cycle-detection/output/change-item-value-type.out +++ b/tests/cases/cycle-detection/output/change-item-value-type.out @@ -17,12 +17,12 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/Item/properties/value", + "#/components/schemas/Item", "#/components/schemas/ItemPage/properties/items/items/$ref", "#/paths/~1items/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/Item/properties/value", + "#/components/schemas/Item", "#/components/schemas/ItemPage/properties/items/items/$ref", "#/paths/~1items/get/responses/200/content/application~1json/schema/$ref", ], @@ -31,8 +31,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/value", + new_subpath: "properties/value", message: "schema types changed", class: Incompatible, details: UnknownDifference, diff --git a/tests/cases/cycle-detection/output/change-mutual-recursion-property.out b/tests/cases/cycle-detection/output/change-mutual-recursion-property.out index 8466bc7..2e4437f 100644 --- a/tests/cases/cycle-detection/output/change-mutual-recursion-property.out +++ b/tests/cases/cycle-detection/output/change-mutual-recursion-property.out @@ -17,22 +17,33 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/Person/properties/name", + "#/components/schemas/Person", "#/components/schemas/Company/properties/ceo/$ref", "#/paths/~1companies/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/Person/properties/name", + "#/components/schemas/Person", "#/components/schemas/Company/properties/ceo/$ref", "#/paths/~1companies/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, }, + ChangePath { + old: [ + "#/components/schemas/Person", + "#/paths/~1persons/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/Person", + "#/paths/~1persons/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/name", + new_subpath: "properties/name", message: "schema types changed", class: Incompatible, details: UnknownDifference, diff --git a/tests/cases/cycle-detection/output/change-three-cycle-property.out b/tests/cases/cycle-detection/output/change-three-cycle-property.out index 3c0d33b..e474c83 100644 --- a/tests/cases/cycle-detection/output/change-three-cycle-property.out +++ b/tests/cases/cycle-detection/output/change-three-cycle-property.out @@ -17,12 +17,12 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/NodeB/properties/label", + "#/components/schemas/NodeB", "#/components/schemas/NodeA/properties/next/$ref", "#/paths/~1three-cycle/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/NodeB/properties/label", + "#/components/schemas/NodeB", "#/components/schemas/NodeA/properties/next/$ref", "#/paths/~1three-cycle/get/responses/200/content/application~1json/schema/$ref", ], @@ -31,8 +31,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/label", + new_subpath: "properties/label", message: "schema types changed", class: Incompatible, details: UnknownDifference, diff --git a/tests/cases/cycle-detection/output/change-wrapped-cycle-property.out b/tests/cases/cycle-detection/output/change-wrapped-cycle-property.out index cd3bea5..ac18bc3 100644 --- a/tests/cases/cycle-detection/output/change-wrapped-cycle-property.out +++ b/tests/cases/cycle-detection/output/change-wrapped-cycle-property.out @@ -17,12 +17,12 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/Wrapped/properties/value", + "#/components/schemas/Wrapped", "#/components/schemas/Wrapper/properties/child/0/$ref", "#/paths/~1wrapped-cycle/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/Wrapped/properties/value", + "#/components/schemas/Wrapped", "#/components/schemas/Wrapper/properties/child/0/$ref", "#/paths/~1wrapped-cycle/get/responses/200/content/application~1json/schema/$ref", ], @@ -31,8 +31,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/value", + new_subpath: "properties/value", message: "schema types changed", class: Incompatible, details: UnknownDifference, diff --git a/tests/cases/cycle-detection/output/swap-alternating-cycle-entry.out b/tests/cases/cycle-detection/output/swap-alternating-cycle-entry.out index c28d3b2..bd5056b 100644 --- a/tests/cases/cycle-detection/output/swap-alternating-cycle-entry.out +++ b/tests/cases/cycle-detection/output/swap-alternating-cycle-entry.out @@ -26,6 +26,21 @@ Result for patch: ], comparison: Output, }, + ChangePath { + old: [ + "#/components/schemas/AltX", + "#/components/schemas/AltY/properties/next/$ref", + "#/components/schemas/AltX/properties/next/$ref", + "#/paths/~1alternating-cycle/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/AltY", + "#/components/schemas/AltX/properties/next/$ref", + "#/components/schemas/AltY/properties/next/$ref", + "#/paths/~1alternating-cycle/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, ], changes: [ ChangeInfo { diff --git a/tests/cases/cycle-detection/output/swap-different-named-cycle.out b/tests/cases/cycle-detection/output/swap-different-named-cycle.out index e6b0321..aaab12b 100644 --- a/tests/cases/cycle-detection/output/swap-different-named-cycle.out +++ b/tests/cases/cycle-detection/output/swap-different-named-cycle.out @@ -35,26 +35,15 @@ Result for patch: ], comparison: Output, }, - ], - changes: [ - ChangeInfo { - old_subpath: "", - new_subpath: "", - message: "schema metadata changed", - class: Trivial, - details: Metadata, - }, - ], - }, - Change { - paths: [ ChangePath { old: [ - "#/components/schemas/SelfCycleA/properties/value", + "#/components/schemas/SelfCycleA", + "#/components/schemas/SelfCycleA/properties/next/$ref", "#/paths/~1self-cycle/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/SelfCycleB/properties/value", + "#/components/schemas/SelfCycleB", + "#/components/schemas/SelfCycleB/properties/next/$ref", "#/paths/~1self-cycle/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -64,6 +53,13 @@ Result for patch: ChangeInfo { old_subpath: "", new_subpath: "", + message: "schema metadata changed", + class: Trivial, + details: Metadata, + }, + ChangeInfo { + old_subpath: "properties/value", + new_subpath: "properties/value", message: "schema types changed", class: Incompatible, details: UnknownDifference, diff --git a/tests/cases/simple/output/add-cookie-parameter.out b/tests/cases/simple/output/add-cookie-parameter.out index 589dbfd..d1c4e05 100644 --- a/tests/cases/simple/output/add-cookie-parameter.out +++ b/tests/cases/simple/output/add-cookie-parameter.out @@ -28,7 +28,7 @@ Result for patch: "#/paths/~1ping/get", ], new: [ - "#/paths/~1ping/get/parameters/0", + "#/paths/~1ping/get", ], comparison: Input, }, @@ -36,7 +36,7 @@ Result for patch: changes: [ ChangeInfo { old_subpath: "", - new_subpath: "", + new_subpath: "parameters/0", message: "A new, optional parameter 'tracking' was added", class: ForwardIncompatible, details: Added, diff --git a/tests/cases/simple/output/add-header-parameter.out b/tests/cases/simple/output/add-header-parameter.out index 4983e6b..2f022a1 100644 --- a/tests/cases/simple/output/add-header-parameter.out +++ b/tests/cases/simple/output/add-header-parameter.out @@ -28,7 +28,7 @@ Result for patch: "#/paths/~1ping/get", ], new: [ - "#/paths/~1ping/get/parameters/0", + "#/paths/~1ping/get", ], comparison: Input, }, @@ -36,7 +36,7 @@ Result for patch: changes: [ ChangeInfo { old_subpath: "", - new_subpath: "", + new_subpath: "parameters/0", message: "A new, optional parameter 'X-Trace-Id' was added", class: ForwardIncompatible, details: Added, diff --git a/tests/cases/simple/output/add-optional-parameter.out b/tests/cases/simple/output/add-optional-parameter.out index 090280d..1d8de1a 100644 --- a/tests/cases/simple/output/add-optional-parameter.out +++ b/tests/cases/simple/output/add-optional-parameter.out @@ -27,7 +27,7 @@ Result for patch: "#/paths/~1hello~1{name}/get", ], new: [ - "#/paths/~1hello~1{name}/get/parameters/2", + "#/paths/~1hello~1{name}/get", ], comparison: Input, }, @@ -35,7 +35,7 @@ Result for patch: changes: [ ChangeInfo { old_subpath: "", - new_subpath: "", + new_subpath: "parameters/2", message: "A new, optional parameter 'limit' was added", class: ForwardIncompatible, details: Added, diff --git a/tests/cases/simple/output/add-required-parameter.out b/tests/cases/simple/output/add-required-parameter.out index ab0ae99..7ae2227 100644 --- a/tests/cases/simple/output/add-required-parameter.out +++ b/tests/cases/simple/output/add-required-parameter.out @@ -27,7 +27,7 @@ Result for patch: "#/paths/~1hello~1{name}/get", ], new: [ - "#/paths/~1hello~1{name}/get/parameters/2", + "#/paths/~1hello~1{name}/get", ], comparison: Input, }, @@ -35,7 +35,7 @@ Result for patch: changes: [ ChangeInfo { old_subpath: "", - new_subpath: "", + new_subpath: "parameters/2", message: "A new, required parameter 'format' was added", class: BackwardIncompatible, details: AddedRequired, diff --git a/tests/cases/simple/output/allof-to-anyof.out b/tests/cases/simple/output/allof-to-anyof.out index b81e3bd..685e0db 100644 --- a/tests/cases/simple/output/allof-to-anyof.out +++ b/tests/cases/simple/output/allof-to-anyof.out @@ -23,11 +23,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/via_allof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/via_allof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -35,8 +35,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/via_allof", + new_subpath: "properties/via_allof", message: "schema metadata changed", class: Trivial, details: Metadata, diff --git a/tests/cases/simple/output/allof-to-oneof-with-type-change.out b/tests/cases/simple/output/allof-to-oneof-with-type-change.out index ed44066..e0a0951 100644 --- a/tests/cases/simple/output/allof-to-oneof-with-type-change.out +++ b/tests/cases/simple/output/allof-to-oneof-with-type-change.out @@ -33,11 +33,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/via_allof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/via_allof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -45,8 +45,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/via_allof", + new_subpath: "properties/via_allof", message: "schema metadata changed", class: Trivial, details: Metadata, @@ -57,22 +57,91 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/SubType/properties/value", + "#/components/schemas/SubType", "#/components/schemas/GreetingResponse/properties/via_allof/0/$ref", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/SubType/properties/value", + "#/components/schemas/SubType", "#/components/schemas/GreetingResponse/properties/via_allof/0/$ref", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_anyof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_anyof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/SubTypeClone/allOf/0/$ref", + "#/components/schemas/GreetingResponse/properties/via_clone/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/SubTypeClone/allOf/0/$ref", + "#/components/schemas/GreetingResponse/properties/via_clone/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_oneof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_oneof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_ref/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_ref/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/RefChainB/$ref", + "#/components/schemas/RefChainA/$ref", + "#/paths/~1refs/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/RefChainB/$ref", + "#/components/schemas/RefChainA/$ref", + "#/paths/~1refs/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/value", + new_subpath: "properties/value", message: "schema types changed", class: Incompatible, details: UnknownDifference, diff --git a/tests/cases/simple/output/allof-to-oneof.out b/tests/cases/simple/output/allof-to-oneof.out index 138bba1..d70bfb2 100644 --- a/tests/cases/simple/output/allof-to-oneof.out +++ b/tests/cases/simple/output/allof-to-oneof.out @@ -24,11 +24,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/via_allof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/via_allof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -36,8 +36,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/via_allof", + new_subpath: "properties/via_allof", message: "schema metadata changed", class: Trivial, details: Metadata, diff --git a/tests/cases/simple/output/allof-to-ref-with-type-change.out b/tests/cases/simple/output/allof-to-ref-with-type-change.out index b9e4d5d..4e12a45 100644 --- a/tests/cases/simple/output/allof-to-ref-with-type-change.out +++ b/tests/cases/simple/output/allof-to-ref-with-type-change.out @@ -32,11 +32,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/via_allof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/via_allof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -44,8 +44,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/via_allof", + new_subpath: "properties/via_allof", message: "schema metadata removed", class: Trivial, details: Metadata, @@ -67,6 +67,75 @@ Result for patch: ], comparison: Output, }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_anyof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_anyof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/SubTypeClone/allOf/0/$ref", + "#/components/schemas/GreetingResponse/properties/via_clone/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/SubTypeClone/allOf/0/$ref", + "#/components/schemas/GreetingResponse/properties/via_clone/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_oneof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_oneof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_ref/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_ref/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/RefChainB/$ref", + "#/components/schemas/RefChainA/$ref", + "#/paths/~1refs/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/RefChainB/$ref", + "#/components/schemas/RefChainA/$ref", + "#/paths/~1refs/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, ], changes: [ ChangeInfo { diff --git a/tests/cases/simple/output/allof-to-ref.out b/tests/cases/simple/output/allof-to-ref.out index f678b9f..fe06127 100644 --- a/tests/cases/simple/output/allof-to-ref.out +++ b/tests/cases/simple/output/allof-to-ref.out @@ -22,11 +22,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/via_allof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/via_allof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -34,8 +34,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/via_allof", + new_subpath: "properties/via_allof", message: "schema metadata removed", class: Trivial, details: Metadata, diff --git a/tests/cases/simple/output/anyof-to-allof.out b/tests/cases/simple/output/anyof-to-allof.out index 5c869bc..69dd62f 100644 --- a/tests/cases/simple/output/anyof-to-allof.out +++ b/tests/cases/simple/output/anyof-to-allof.out @@ -23,11 +23,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/via_anyof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/via_anyof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -35,8 +35,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/via_anyof", + new_subpath: "properties/via_anyof", message: "schema metadata changed", class: Trivial, details: Metadata, diff --git a/tests/cases/simple/output/anyof-to-oneof.out b/tests/cases/simple/output/anyof-to-oneof.out index 0c1e463..bfdc909 100644 --- a/tests/cases/simple/output/anyof-to-oneof.out +++ b/tests/cases/simple/output/anyof-to-oneof.out @@ -24,11 +24,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/via_anyof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/via_anyof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -36,8 +36,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/via_anyof", + new_subpath: "properties/via_anyof", message: "schema metadata changed", class: Trivial, details: Metadata, diff --git a/tests/cases/simple/output/anyof-to-ref.out b/tests/cases/simple/output/anyof-to-ref.out index 4f3a244..cae0870 100644 --- a/tests/cases/simple/output/anyof-to-ref.out +++ b/tests/cases/simple/output/anyof-to-ref.out @@ -22,11 +22,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/via_anyof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/via_anyof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -34,8 +34,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/via_anyof", + new_subpath: "properties/via_anyof", message: "schema metadata removed", class: Trivial, details: Metadata, diff --git a/tests/cases/simple/output/array-items-remove.out b/tests/cases/simple/output/array-items-remove.out index 0d4f31e..3e86169 100644 --- a/tests/cases/simple/output/array-items-remove.out +++ b/tests/cases/simple/output/array-items-remove.out @@ -15,6 +15,17 @@ Result for patch: [ Change { paths: [ + ChangePath { + old: [ + "#/components/schemas/Tree", + "#/paths/~1tree/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/Tree", + "#/paths/~1tree/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, ChangePath { old: [ "#/components/schemas/Tree", @@ -22,7 +33,7 @@ Result for patch: "#/paths/~1tree/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/Tree/properties/children/items", + "#/components/schemas/Tree", "#/paths/~1tree/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -31,7 +42,7 @@ Result for patch: changes: [ ChangeInfo { old_subpath: "", - new_subpath: "", + new_subpath: "properties/children/items", message: "schema types changed", class: Incompatible, details: UnknownDifference, diff --git a/tests/cases/simple/output/boolean-change.out b/tests/cases/simple/output/boolean-change.out index 9d002e0..3bd0fb0 100644 --- a/tests/cases/simple/output/boolean-change.out +++ b/tests/cases/simple/output/boolean-change.out @@ -16,11 +16,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/TypedProperties/properties/enabled", + "#/components/schemas/TypedProperties", "#/paths/~1typed/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/TypedProperties/properties/enabled", + "#/components/schemas/TypedProperties", "#/paths/~1typed/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -28,8 +28,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/enabled", + new_subpath: "properties/enabled", message: "schema metadata changed", class: Trivial, details: Metadata, diff --git a/tests/cases/simple/output/boolean-enum-change.out b/tests/cases/simple/output/boolean-enum-change.out index 37bf0eb..adcf16a 100644 --- a/tests/cases/simple/output/boolean-enum-change.out +++ b/tests/cases/simple/output/boolean-enum-change.out @@ -18,11 +18,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/TypedProperties/properties/enabled", + "#/components/schemas/TypedProperties", "#/paths/~1typed/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/TypedProperties/properties/enabled", + "#/components/schemas/TypedProperties", "#/paths/~1typed/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -30,8 +30,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/enabled", + new_subpath: "properties/enabled", message: "boolean schema changed", class: Unhandled, details: UnknownDifference, diff --git a/tests/cases/simple/output/change-header-parameter.out b/tests/cases/simple/output/change-header-parameter.out index b506dce..87a8902 100644 --- a/tests/cases/simple/output/change-header-parameter.out +++ b/tests/cases/simple/output/change-header-parameter.out @@ -17,18 +17,18 @@ Result for patch: paths: [ ChangePath { old: [ - "#/paths/~1with-header/get/parameters/0/schema", + "#/paths/~1with-header/get", ], new: [ - "#/paths/~1with-header/get/parameters/0/schema", + "#/paths/~1with-header/get", ], comparison: Input, }, ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "parameters/0/schema", + new_subpath: "parameters/0/schema", message: "schema types changed", class: Incompatible, details: UnknownDifference, diff --git a/tests/cases/simple/output/change-operation-parameter-requirement.out b/tests/cases/simple/output/change-operation-parameter-requirement.out index 974f59a..638ed8f 100644 --- a/tests/cases/simple/output/change-operation-parameter-requirement.out +++ b/tests/cases/simple/output/change-operation-parameter-requirement.out @@ -17,18 +17,18 @@ Result for patch: paths: [ ChangePath { old: [ - "#/paths/~1hello~1{name}/get/parameters/1", + "#/paths/~1hello~1{name}/get", ], new: [ - "#/paths/~1hello~1{name}/get/parameters/1", + "#/paths/~1hello~1{name}/get", ], comparison: Input, }, ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "parameters/1", + new_subpath: "parameters/1", message: "The parameter 'language' was optional and is now required", class: BackwardIncompatible, details: MoreStrict, diff --git a/tests/cases/simple/output/change-operation-parameter-type.out b/tests/cases/simple/output/change-operation-parameter-type.out index 9252a75..2c7f4b5 100644 --- a/tests/cases/simple/output/change-operation-parameter-type.out +++ b/tests/cases/simple/output/change-operation-parameter-type.out @@ -17,18 +17,18 @@ Result for patch: paths: [ ChangePath { old: [ - "#/paths/~1hello~1{name}/get/parameters/1/schema", + "#/paths/~1hello~1{name}/get", ], new: [ - "#/paths/~1hello~1{name}/get/parameters/1/schema", + "#/paths/~1hello~1{name}/get", ], comparison: Input, }, ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "parameters/1/schema", + new_subpath: "parameters/1/schema", message: "schema types changed", class: Incompatible, details: UnknownDifference, diff --git a/tests/cases/simple/output/change-property-type.out b/tests/cases/simple/output/change-property-type.out index 0084054..449d40b 100644 --- a/tests/cases/simple/output/change-property-type.out +++ b/tests/cases/simple/output/change-property-type.out @@ -17,11 +17,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/message", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/message", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -29,8 +29,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/message", + new_subpath: "properties/message", message: "schema types changed", class: Incompatible, details: UnknownDifference, diff --git a/tests/cases/simple/output/inline-to-allof.out b/tests/cases/simple/output/inline-to-allof.out index 954ca32..84588b1 100644 --- a/tests/cases/simple/output/inline-to-allof.out +++ b/tests/cases/simple/output/inline-to-allof.out @@ -22,18 +22,18 @@ Result for patch: paths: [ ChangePath { old: [ - "#/paths/~1hello~1{name}/get/parameters/0/schema", + "#/paths/~1hello~1{name}/get", ], new: [ - "#/paths/~1hello~1{name}/get/parameters/0/schema", + "#/paths/~1hello~1{name}/get", ], comparison: Input, }, ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "parameters/0/schema", + new_subpath: "parameters/0/schema", message: "schema metadata added", class: Trivial, details: Metadata, diff --git a/tests/cases/simple/output/input-output-schema-change.out b/tests/cases/simple/output/input-output-schema-change.out index 5c74774..4d30a4e 100644 --- a/tests/cases/simple/output/input-output-schema-change.out +++ b/tests/cases/simple/output/input-output-schema-change.out @@ -17,35 +17,22 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/SharedType/properties/value", + "#/components/schemas/SharedType", "#/paths/~1echo/post/request_body/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/SharedType/properties/value", + "#/components/schemas/SharedType", "#/paths/~1echo/post/request_body/content/application~1json/schema/$ref", ], comparison: Input, }, - ], - changes: [ - ChangeInfo { - old_subpath: "", - new_subpath: "", - message: "schema types changed", - class: Incompatible, - details: UnknownDifference, - }, - ], - }, - Change { - paths: [ ChangePath { old: [ - "#/components/schemas/SharedType/properties/value", + "#/components/schemas/SharedType", "#/paths/~1echo/post/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/SharedType/properties/value", + "#/components/schemas/SharedType", "#/paths/~1echo/post/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -53,8 +40,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/value", + new_subpath: "properties/value", message: "schema types changed", class: Incompatible, details: UnknownDifference, diff --git a/tests/cases/simple/output/integer-format-change.out b/tests/cases/simple/output/integer-format-change.out index 14535bd..311f2cc 100644 --- a/tests/cases/simple/output/integer-format-change.out +++ b/tests/cases/simple/output/integer-format-change.out @@ -16,11 +16,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/TypedProperties/properties/count", + "#/components/schemas/TypedProperties", "#/paths/~1typed/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/TypedProperties/properties/count", + "#/components/schemas/TypedProperties", "#/paths/~1typed/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -28,8 +28,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/count", + new_subpath: "properties/count", message: "integer schema changed", class: Unhandled, details: UnknownDifference, diff --git a/tests/cases/simple/output/modify-cycle-type.out b/tests/cases/simple/output/modify-cycle-type.out index 6fb2fa4..d52e8c3 100644 --- a/tests/cases/simple/output/modify-cycle-type.out +++ b/tests/cases/simple/output/modify-cycle-type.out @@ -20,7 +20,18 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/Tree/properties/children", + "#/components/schemas/Tree", + "#/paths/~1tree/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/Tree", + "#/paths/~1tree/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/Tree", "#/paths/~1tree/get/responses/200/content/application~1json/schema/$ref", ], new: [ @@ -33,7 +44,7 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", + old_subpath: "properties/children", new_subpath: "", message: "schema types changed", class: Incompatible, diff --git a/tests/cases/simple/output/multi-allof-variant-change.out b/tests/cases/simple/output/multi-allof-variant-change.out index b6b73fc..92abdc0 100644 --- a/tests/cases/simple/output/multi-allof-variant-change.out +++ b/tests/cases/simple/output/multi-allof-variant-change.out @@ -17,11 +17,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/MultiAllOf/allOf", + "#/components/schemas/MultiAllOf", "#/paths/~1allof/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/MultiAllOf/allOf", + "#/components/schemas/MultiAllOf", "#/paths/~1allof/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -29,8 +29,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "allOf", + new_subpath: "allOf", message: "allOf with multiple schemas is unhandled", class: Unhandled, details: UnknownDifference, diff --git a/tests/cases/simple/output/multi-oneof-count-change.out b/tests/cases/simple/output/multi-oneof-count-change.out index 94ebd88..96b366f 100644 --- a/tests/cases/simple/output/multi-oneof-count-change.out +++ b/tests/cases/simple/output/multi-oneof-count-change.out @@ -18,11 +18,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/MultiOneOf/oneOf", + "#/components/schemas/MultiOneOf", "#/paths/~1oneof/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/MultiOneOf/oneOf", + "#/components/schemas/MultiOneOf", "#/paths/~1oneof/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -30,8 +30,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "oneOf", + new_subpath: "oneOf", message: "oneOf schema count changed", class: Unhandled, details: UnknownDifference, diff --git a/tests/cases/simple/output/multi-oneof-variant-change.out b/tests/cases/simple/output/multi-oneof-variant-change.out index 95ffac8..253291f 100644 --- a/tests/cases/simple/output/multi-oneof-variant-change.out +++ b/tests/cases/simple/output/multi-oneof-variant-change.out @@ -17,11 +17,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/MultiOneOf/oneOf/0", + "#/components/schemas/MultiOneOf", "#/paths/~1oneof/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/MultiOneOf/oneOf/0", + "#/components/schemas/MultiOneOf", "#/paths/~1oneof/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -29,8 +29,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "oneOf/0", + new_subpath: "oneOf/0", message: "schema types changed", class: Incompatible, details: UnknownDifference, diff --git a/tests/cases/simple/output/not-inner-to-allof.out b/tests/cases/simple/output/not-inner-to-allof.out index 47df17f..772498b 100644 --- a/tests/cases/simple/output/not-inner-to-allof.out +++ b/tests/cases/simple/output/not-inner-to-allof.out @@ -22,11 +22,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/not_a_number/not", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/not_a_number/not", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -34,8 +34,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/not_a_number/not", + new_subpath: "properties/not_a_number/not", message: "schema metadata added", class: Trivial, details: Metadata, diff --git a/tests/cases/simple/output/not-to-allof.out b/tests/cases/simple/output/not-to-allof.out index 4af3db6..5a33737 100644 --- a/tests/cases/simple/output/not-to-allof.out +++ b/tests/cases/simple/output/not-to-allof.out @@ -26,11 +26,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/not_a_number", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/not_a_number", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -38,8 +38,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/not_a_number", + new_subpath: "properties/not_a_number", message: "schema metadata added", class: Trivial, details: Metadata, diff --git a/tests/cases/simple/output/number-constraints-change.out b/tests/cases/simple/output/number-constraints-change.out index 5754482..a4dd7d5 100644 --- a/tests/cases/simple/output/number-constraints-change.out +++ b/tests/cases/simple/output/number-constraints-change.out @@ -16,11 +16,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/TypedProperties/properties/ratio", + "#/components/schemas/TypedProperties", "#/paths/~1typed/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/TypedProperties/properties/ratio", + "#/components/schemas/TypedProperties", "#/paths/~1typed/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -28,8 +28,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/ratio", + new_subpath: "properties/ratio", message: "number schema changed", class: Unhandled, details: UnknownDifference, diff --git a/tests/cases/simple/output/object-additional-props-schema-change.out b/tests/cases/simple/output/object-additional-props-schema-change.out index 3248286..55f9ab9 100644 --- a/tests/cases/simple/output/object-additional-props-schema-change.out +++ b/tests/cases/simple/output/object-additional-props-schema-change.out @@ -17,11 +17,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/ObjectWithConstraints/additionalProperties", + "#/components/schemas/ObjectWithConstraints", "#/paths/~1objects/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/ObjectWithConstraints/additionalProperties", + "#/components/schemas/ObjectWithConstraints", "#/paths/~1objects/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -29,8 +29,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "additionalProperties", + new_subpath: "additionalProperties", message: "schema types changed", class: Incompatible, details: UnknownDifference, diff --git a/tests/cases/simple/output/oneof-to-allof.out b/tests/cases/simple/output/oneof-to-allof.out index c87dc7a..b59dc6c 100644 --- a/tests/cases/simple/output/oneof-to-allof.out +++ b/tests/cases/simple/output/oneof-to-allof.out @@ -24,11 +24,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/via_oneof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/via_oneof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -36,8 +36,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/via_oneof", + new_subpath: "properties/via_oneof", message: "schema metadata changed", class: Trivial, details: Metadata, diff --git a/tests/cases/simple/output/oneof-to-anyof.out b/tests/cases/simple/output/oneof-to-anyof.out index 9351060..b44b667 100644 --- a/tests/cases/simple/output/oneof-to-anyof.out +++ b/tests/cases/simple/output/oneof-to-anyof.out @@ -24,11 +24,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/via_oneof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/via_oneof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -36,8 +36,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/via_oneof", + new_subpath: "properties/via_oneof", message: "schema metadata changed", class: Trivial, details: Metadata, diff --git a/tests/cases/simple/output/oneof-to-ref.out b/tests/cases/simple/output/oneof-to-ref.out index 448379e..bd033c2 100644 --- a/tests/cases/simple/output/oneof-to-ref.out +++ b/tests/cases/simple/output/oneof-to-ref.out @@ -22,11 +22,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/via_oneof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/via_oneof", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -34,8 +34,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/via_oneof", + new_subpath: "properties/via_oneof", message: "schema metadata removed", class: Trivial, details: Metadata, diff --git a/tests/cases/simple/output/param-required-to-optional.out b/tests/cases/simple/output/param-required-to-optional.out index 1419a39..bd4b496 100644 --- a/tests/cases/simple/output/param-required-to-optional.out +++ b/tests/cases/simple/output/param-required-to-optional.out @@ -17,18 +17,18 @@ Result for patch: paths: [ ChangePath { old: [ - "#/paths/~1with-header/get/parameters/0", + "#/paths/~1with-header/get", ], new: [ - "#/paths/~1with-header/get/parameters/0", + "#/paths/~1with-header/get", ], comparison: Input, }, ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "parameters/0", + new_subpath: "parameters/0", message: "The parameter 'X-Request-Id' was required and is now optional", class: ForwardIncompatible, details: LessStrict, diff --git a/tests/cases/simple/output/param-schema-to-content.out b/tests/cases/simple/output/param-schema-to-content.out index 2ff8551..b81f82a 100644 --- a/tests/cases/simple/output/param-schema-to-content.out +++ b/tests/cases/simple/output/param-schema-to-content.out @@ -30,18 +30,18 @@ Result for patch: paths: [ ChangePath { old: [ - "#/paths/~1hello~1{name}/get/parameters/1", + "#/paths/~1hello~1{name}/get", ], new: [ - "#/paths/~1hello~1{name}/get/parameters/1", + "#/paths/~1hello~1{name}/get", ], comparison: Input, }, ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "parameters/1", + new_subpath: "parameters/1", message: "Unhandled change to parameter schema or content", class: Unhandled, details: UnknownDifference, diff --git a/tests/cases/simple/output/ref-chain-change.out b/tests/cases/simple/output/ref-chain-change.out index c6dec90..ea99a45 100644 --- a/tests/cases/simple/output/ref-chain-change.out +++ b/tests/cases/simple/output/ref-chain-change.out @@ -39,27 +39,6 @@ Result for patch: class: Unhandled, details: UnknownDifference, }, - ], - }, - Change { - paths: [ - ChangePath { - old: [ - "#/components/schemas/SubType", - "#/components/schemas/RefChainB/$ref", - "#/components/schemas/RefChainA/$ref", - "#/paths/~1refs/get/responses/200/content/application~1json/schema/$ref", - ], - new: [ - "#/components/schemas/ErrorResponse", - "#/components/schemas/RefChainB/$ref", - "#/components/schemas/RefChainA/$ref", - "#/paths/~1refs/get/responses/200/content/application~1json/schema/$ref", - ], - comparison: Output, - }, - ], - changes: [ ChangeInfo { old_subpath: "", new_subpath: "", diff --git a/tests/cases/simple/output/ref-to-allof.out b/tests/cases/simple/output/ref-to-allof.out index 3316bd2..db9f9a9 100644 --- a/tests/cases/simple/output/ref-to-allof.out +++ b/tests/cases/simple/output/ref-to-allof.out @@ -22,11 +22,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/via_ref", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/via_ref", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -34,8 +34,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/via_ref", + new_subpath: "properties/via_ref", message: "schema metadata added", class: Trivial, details: Metadata, diff --git a/tests/cases/simple/output/ref-to-anyof.out b/tests/cases/simple/output/ref-to-anyof.out index 3349ae3..114b2f8 100644 --- a/tests/cases/simple/output/ref-to-anyof.out +++ b/tests/cases/simple/output/ref-to-anyof.out @@ -22,11 +22,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/via_ref", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/via_ref", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -34,8 +34,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/via_ref", + new_subpath: "properties/via_ref", message: "schema metadata added", class: Trivial, details: Metadata, diff --git a/tests/cases/simple/output/ref-to-inline-allof.out b/tests/cases/simple/output/ref-to-inline-allof.out index 311ff4e..a65e60a 100644 --- a/tests/cases/simple/output/ref-to-inline-allof.out +++ b/tests/cases/simple/output/ref-to-inline-allof.out @@ -28,11 +28,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/via_ref", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/via_ref", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -40,8 +40,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/via_ref", + new_subpath: "properties/via_ref", message: "schema metadata added", class: Trivial, details: Metadata, @@ -57,7 +57,7 @@ Result for patch: "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/via_ref/0", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -66,7 +66,7 @@ Result for patch: changes: [ ChangeInfo { old_subpath: "", - new_subpath: "", + new_subpath: "properties/via_ref/0", message: "schema metadata changed", class: Trivial, details: Metadata, diff --git a/tests/cases/simple/output/ref-to-oneof.out b/tests/cases/simple/output/ref-to-oneof.out index 7224c41..b5b2e71 100644 --- a/tests/cases/simple/output/ref-to-oneof.out +++ b/tests/cases/simple/output/ref-to-oneof.out @@ -22,11 +22,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/via_ref", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/via_ref", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -34,8 +34,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/via_ref", + new_subpath: "properties/via_ref", message: "schema metadata added", class: Trivial, details: Metadata, diff --git a/tests/cases/simple/output/ref-vs-inline-type-change.out b/tests/cases/simple/output/ref-vs-inline-type-change.out index 38fe82d..32592ce 100644 --- a/tests/cases/simple/output/ref-vs-inline-type-change.out +++ b/tests/cases/simple/output/ref-vs-inline-type-change.out @@ -31,13 +31,12 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/SubType/properties/value", - "#/components/schemas/GreetingResponse/properties/via_allof/0/$ref", + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_ref/$ref", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/SubType/properties/value", - "#/components/schemas/GreetingResponse/properties/via_allof/0/$ref", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -45,8 +44,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/value", + new_subpath: "properties/via_ref/properties/value", message: "schema types changed", class: Incompatible, details: UnknownDifference, @@ -57,21 +56,78 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/SubType/properties/value", - "#/components/schemas/GreetingResponse/properties/via_ref/$ref", + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_allof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_allof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_anyof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_anyof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/SubTypeClone/allOf/0/$ref", + "#/components/schemas/GreetingResponse/properties/via_clone/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/SubTypeClone/allOf/0/$ref", + "#/components/schemas/GreetingResponse/properties/via_clone/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_oneof/0/$ref", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/via_ref/properties/value", + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_oneof/0/$ref", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/RefChainB/$ref", + "#/components/schemas/RefChainA/$ref", + "#/paths/~1refs/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/RefChainB/$ref", + "#/components/schemas/RefChainA/$ref", + "#/paths/~1refs/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/value", + new_subpath: "properties/value", message: "schema types changed", class: Incompatible, details: UnknownDifference, diff --git a/tests/cases/simple/output/remove-header-parameter.out b/tests/cases/simple/output/remove-header-parameter.out index ce18379..354fe26 100644 --- a/tests/cases/simple/output/remove-header-parameter.out +++ b/tests/cases/simple/output/remove-header-parameter.out @@ -23,7 +23,7 @@ Result for patch: paths: [ ChangePath { old: [ - "#/paths/~1with-header/get/parameters/0", + "#/paths/~1with-header/get", ], new: [ "#/paths/~1with-header/get", @@ -33,7 +33,7 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", + old_subpath: "parameters/0", new_subpath: "", message: "The parameter 'X-Request-Id' was removed", class: BackwardIncompatible, diff --git a/tests/cases/simple/output/remove-operation-parameter.out b/tests/cases/simple/output/remove-operation-parameter.out index b539ab5..44247a0 100644 --- a/tests/cases/simple/output/remove-operation-parameter.out +++ b/tests/cases/simple/output/remove-operation-parameter.out @@ -24,7 +24,7 @@ Result for patch: paths: [ ChangePath { old: [ - "#/paths/~1hello~1{name}/get/parameters/1", + "#/paths/~1hello~1{name}/get", ], new: [ "#/paths/~1hello~1{name}/get", @@ -34,7 +34,7 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", + old_subpath: "parameters/1", new_subpath: "", message: "The parameter 'language' was removed", class: BackwardIncompatible, diff --git a/tests/cases/simple/output/schema-kind-type-to-oneof.out b/tests/cases/simple/output/schema-kind-type-to-oneof.out index 20a03ed..6da9843 100644 --- a/tests/cases/simple/output/schema-kind-type-to-oneof.out +++ b/tests/cases/simple/output/schema-kind-type-to-oneof.out @@ -38,6 +38,75 @@ Result for patch: ], comparison: Output, }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_anyof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_anyof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/SubTypeClone/allOf/0/$ref", + "#/components/schemas/GreetingResponse/properties/via_clone/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/SubTypeClone/allOf/0/$ref", + "#/components/schemas/GreetingResponse/properties/via_clone/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_oneof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_oneof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_ref/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_ref/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/RefChainB/$ref", + "#/components/schemas/RefChainA/$ref", + "#/paths/~1refs/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/RefChainB/$ref", + "#/components/schemas/RefChainA/$ref", + "#/paths/~1refs/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, ], changes: [ ChangeInfo { diff --git a/tests/cases/simple/output/string-format-change.out b/tests/cases/simple/output/string-format-change.out index ed74d51..21f20d8 100644 --- a/tests/cases/simple/output/string-format-change.out +++ b/tests/cases/simple/output/string-format-change.out @@ -16,11 +16,11 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/message", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/GreetingResponse/properties/message", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, @@ -28,8 +28,8 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/message", + new_subpath: "properties/message", message: "string schema changed", class: Unhandled, details: UnknownDifference, diff --git a/tests/cases/simple/output/type-indirection.out b/tests/cases/simple/output/type-indirection.out index b751f44..a990e30 100644 --- a/tests/cases/simple/output/type-indirection.out +++ b/tests/cases/simple/output/type-indirection.out @@ -32,7 +32,7 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/GreetingResponse/properties/message", + "#/components/schemas/GreetingResponse", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ @@ -45,7 +45,7 @@ Result for patch: ], changes: [ ChangeInfo { - old_subpath: "", + old_subpath: "properties/message", new_subpath: "", message: "schema metadata changed", class: Trivial, diff --git a/tests/cases/simple/output/unhandled-add-prop.out b/tests/cases/simple/output/unhandled-add-prop.out index c45ca6f..2152622 100644 --- a/tests/cases/simple/output/unhandled-add-prop.out +++ b/tests/cases/simple/output/unhandled-add-prop.out @@ -47,23 +47,6 @@ Result for patch: class: Unhandled, details: UnknownDifference, }, - ], - }, - Change { - paths: [ - ChangePath { - old: [ - "#/components/schemas/GreetingResponse", - "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", - ], - new: [ - "#/components/schemas/GreetingResponse", - "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", - ], - comparison: Output, - }, - ], - changes: [ ChangeInfo { old_subpath: "", new_subpath: "", diff --git a/tests/cases/simple/output/wrapper-unchanged-with-type-change.out b/tests/cases/simple/output/wrapper-unchanged-with-type-change.out index 908f776..11c44f6 100644 --- a/tests/cases/simple/output/wrapper-unchanged-with-type-change.out +++ b/tests/cases/simple/output/wrapper-unchanged-with-type-change.out @@ -17,22 +17,91 @@ Result for patch: paths: [ ChangePath { old: [ - "#/components/schemas/SubType/properties/value", + "#/components/schemas/SubType", "#/components/schemas/GreetingResponse/properties/via_allof/0/$ref", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], new: [ - "#/components/schemas/SubType/properties/value", + "#/components/schemas/SubType", "#/components/schemas/GreetingResponse/properties/via_allof/0/$ref", "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", ], comparison: Output, }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_anyof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_anyof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/SubTypeClone/allOf/0/$ref", + "#/components/schemas/GreetingResponse/properties/via_clone/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/SubTypeClone/allOf/0/$ref", + "#/components/schemas/GreetingResponse/properties/via_clone/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_oneof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_oneof/0/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_ref/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/GreetingResponse/properties/via_ref/$ref", + "#/paths/~1hello~1{name}/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, + ChangePath { + old: [ + "#/components/schemas/SubType", + "#/components/schemas/RefChainB/$ref", + "#/components/schemas/RefChainA/$ref", + "#/paths/~1refs/get/responses/200/content/application~1json/schema/$ref", + ], + new: [ + "#/components/schemas/SubType", + "#/components/schemas/RefChainB/$ref", + "#/components/schemas/RefChainA/$ref", + "#/paths/~1refs/get/responses/200/content/application~1json/schema/$ref", + ], + comparison: Output, + }, ], changes: [ ChangeInfo { - old_subpath: "", - new_subpath: "", + old_subpath: "properties/value", + new_subpath: "properties/value", message: "schema types changed", class: Incompatible, details: UnknownDifference,