feat: add breaking change detection - #1482
Conversation
bb80b5c to
9b796b0
Compare
| def _entry(path, change_type, old_value=None, new_value=None): | ||
| return ChangelogEntry(path=path, type=change_type, old_value=old_value, new_value=new_value) | ||
|
|
||
|
|
There was a problem hiding this comment.
When looking at the tests, I cannot really see how the command works from a CLI perspective?
There was a problem hiding this comment.
Added some tests. Let me know if more are needed!
jschoedl
left a comment
There was a problem hiding this comment.
Hi, thank you for the PR!
| def _is_schema_property_path(segments: list[str]) -> bool: | ||
| try: | ||
| properties_index = segments.index("properties") | ||
| except ValueError: | ||
| return False | ||
| return segments[0] == "schema" and properties_index + 1 < len(segments) |
There was a problem hiding this comment.
This returns True if any of the segments is properties, even if it is not the last one. E.g. deleting a description (located at properties.some_property.description) is mis-classified as deleting the property itself, although this is actually no breaking change.
We should check for properties being the segment before the last one
There was a problem hiding this comment.
This file should not be placed in the testing directory, because it hides the datacontract breaking functionality between the list of different testing backends.
Instead, I'd vote for a seperate page "Compare contract versions" (or similar name) at the top level, which should include both changelog and breaking. As there is no seperate documentation of changelog yet, you don't need to write it - but at least a link to the auto-generated command reference would be nice.
| def detect(self, changelog: ChangelogResult) -> BreakingChangeResult: | ||
| entries = [self._classify(entry) for entry in changelog.entries] | ||
| summary = [self._summarize(entry, entries) for entry in changelog.summary] | ||
| return BreakingChangeResult(v1=changelog.v1, v2=changelog.v2, summary=summary, entries=entries) |
There was a problem hiding this comment.
_summarize(entry, entries) loops each of the changelog entries, i.e. we have quadratic runtime complexity. If there are a lot of entries (e.g. a few thousands), this can take a while.
To avoid that, the entries parameter should not get all of the entries, but only the relevant ones, e.g. like this:
| def detect(self, changelog: ChangelogResult) -> BreakingChangeResult: | |
| entries = [self._classify(entry) for entry in changelog.entries] | |
| summary = [self._summarize(entry, entries) for entry in changelog.summary] | |
| return BreakingChangeResult(v1=changelog.v1, v2=changelog.v2, summary=summary, entries=entries) | |
| def detect(self, changelog: ChangelogResult) -> BreakingChangeResult: | |
| entries = [self._classify(entry) for entry in changelog.entries] | |
| entries_by_prefix = defaultdict(list) | |
| for entry in entries: | |
| prefix = "" | |
| for segment in entry.path.split("."): | |
| prefix = f"{prefix}.{segment}" if prefix else segment | |
| entries_by_prefix[prefix].append(entry) | |
| summary = [self._summarize(entry, entries_by_prefix.get(entry.path, [])) for entry in changelog.summary] | |
| return BreakingChangeResult(v1=changelog.v1, v2=changelog.v2, summary=summary, entries=entries) |
| POST a JSON body with `v1` (source/before) and `v2` (target/after) as YAML strings. | ||
| """, | ||
| ) | ||
| async def breaking_endpoint( |
There was a problem hiding this comment.
| async def breaking_endpoint( | |
| def breaking_endpoint( |
This is declared as async, but actually only does blocking CPU and I/O.
| class EnumConstraintRule(BreakingChangeRule): | ||
| priority = 65 | ||
| rule_id = "enum-constraint-changed" | ||
|
|
||
| def evaluate(self, entry: ChangelogEntry) -> RuleEvaluation | None: | ||
| if not entry.path.endswith(".enum"): | ||
| return None | ||
| old = _parse_sequence(entry.old_value) | ||
| new = _parse_sequence(entry.new_value) | ||
| if old is not None and new is not None: | ||
| if set(old) - set(new): | ||
| level = BreakingChangeLevel.ERROR | ||
| elif set(new) - set(old): | ||
| level = BreakingChangeLevel.INFO | ||
| else: | ||
| level = BreakingChangeLevel.INFO | ||
| else: | ||
| level = BreakingChangeLevel.WARNING | ||
| return RuleEvaluation(self.rule_id, level, _change_message("enum constraint", entry)) |
There was a problem hiding this comment.
enum is not supported in ODCS. As it was supported in the old DCS format, there might be some locations in code or docs where we didn't clean it up yet, but today, this code is not reachable.
The new way is to specify a quality check with validValues: https://bitol-io.github.io/open-data-contract-standard/v3.1.0/data-quality/#invalid-values
| old = _parse_number(entry.old_value) | ||
| new = _parse_number(entry.new_value) | ||
| if old is None or new is None: | ||
| level = BreakingChangeLevel.WARNING | ||
| elif _is_tightening(entry.path, old, new): | ||
| level = BreakingChangeLevel.ERROR | ||
| else: | ||
| level = BreakingChangeLevel.INFO |
There was a problem hiding this comment.
_parse_number in its current form does not work for logicalType: date yet
|
|
||
| try: | ||
| result = DataContract(data_contract_file=v1_path).breaking(DataContract(data_contract_file=v2_path)) | ||
| return result |
There was a problem hiding this comment.
This includes internal information that is not needed by the caller such as the full file paths of the tempdir. For security reasons, it would be best to keep the response minimal as it was done with the ChangelogResponse a few lines before this one.
| @app.post( | ||
| "/breaking", | ||
| tags=["breaking"], | ||
| summary="Show compatibility impact between two data contracts.", | ||
| description=""" | ||
| Compare two ODCS data contract YAMLs and classify their backward-compatibility impact. | ||
| POST a JSON body with `v1` (source/before) and `v2` (target/after) as YAML strings. | ||
| """, | ||
| ) |
There was a problem hiding this comment.
Please add operation_id, response_description and responses to document the API endpoint (see the other endpoints for examples)
There was a problem hiding this comment.
The priority integer seems a bit odd to me. Why not list all the rules in a list where the first one wins?
- Add BreakingChangesResponse model to provide detailed breaking change information. - Update breaking endpoint to return structured response for compatibility impact. - Introduce new rules for detecting breaking changes in schema and properties. - Enhance tests for breaking changes detection and validation rules. - Create documentation for comparing contract versions and breaking changes. - Clean up and reorganize existing documentation for clarity.
Hi, thanks for review, I pushed fixes :) |
Closes #1016
uv run pytest)uv run ruff check --fix && uv run ruff format)