Skip to content

Commit a81ac3b

Browse files
domdomeggclaudeuntitaker
authored
feat: Add enum support with context-aware breaking change detection (#46)
* feat: Add enum support with context-aware breaking change detection Implements detection of JSON Schema enum changes, resolving issue #38. Changes: - Add EnumAdd and EnumRemove change types with context flags - Track whether enum constraint is being added/removed entirely - Smart breaking change logic: - Adding values to existing enum: non-breaking (accepts more) - Removing values from existing enum: breaking (rejects data) - Adding enum constraint: breaking (restricts values) - Removing enum constraint: non-breaking (relaxes values) - Comprehensive test coverage with 9 test cases including the real-world scenario from the original issue Fixes #38 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix screwed up merge --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Markus Unterwaditzer <markus-tarpit+git@unterwaditzer.net>
1 parent 8078870 commit a81ac3b

20 files changed

Lines changed: 424 additions & 0 deletions

src/diff_walker.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,39 @@ impl<F: FnMut(Change)> DiffWalker<F> {
387387
}
388388
}
389389

390+
fn diff_enum(&mut self, json_path: &str, lhs: &mut SchemaObject, rhs: &mut SchemaObject) {
391+
let lhs_enum = lhs.enum_values.as_deref().unwrap_or(&[]);
392+
let rhs_enum = rhs.enum_values.as_deref().unwrap_or(&[]);
393+
let lhs_has_no_enum = lhs.enum_values.is_none();
394+
let rhs_has_no_enum = rhs.enum_values.is_none();
395+
396+
// Find removed enum values (in lhs but not in rhs)
397+
for lhs_value in lhs_enum {
398+
if !rhs_enum.contains(lhs_value) {
399+
(self.cb)(Change {
400+
path: json_path.to_owned(),
401+
change: ChangeKind::EnumRemove {
402+
removed: lhs_value.clone(),
403+
rhs_has_no_enum,
404+
},
405+
});
406+
}
407+
}
408+
409+
// Find added enum values (in rhs but not in lhs)
410+
for rhs_value in rhs_enum {
411+
if !lhs_enum.contains(rhs_value) {
412+
(self.cb)(Change {
413+
path: json_path.to_owned(),
414+
change: ChangeKind::EnumAdd {
415+
added: rhs_value.clone(),
416+
lhs_has_no_enum,
417+
},
418+
});
419+
}
420+
}
421+
}
422+
390423
fn diff_pattern(&mut self, json_path: &str, lhs: &mut SchemaObject, rhs: &mut SchemaObject) {
391424
let lhs_pattern = &lhs.string().pattern;
392425
let rhs_pattern = &rhs.string().pattern;
@@ -594,6 +627,7 @@ impl<F: FnMut(Change)> DiffWalker<F> {
594627
}
595628
self.diff_const(json_path, lhs, rhs);
596629
self.diff_format(json_path, lhs, rhs);
630+
self.diff_enum(json_path, lhs, rhs);
597631
self.diff_pattern(json_path, lhs, rhs);
598632
self.diff_min_length(json_path, lhs, rhs);
599633
self.diff_max_length(json_path, lhs, rhs);

src/types.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,22 @@ pub enum ChangeKind {
124124
/// The new format value.
125125
new_format: String,
126126
},
127+
/// An enum value has been added to the allowed values.
128+
EnumAdd {
129+
/// The value that was added to the enum.
130+
added: serde_json::Value,
131+
/// Whether the enum constraint was added (lhs had no enum).
132+
/// If true, this is breaking as it adds a new constraint.
133+
lhs_has_no_enum: bool,
134+
},
135+
/// An enum value has been removed from the allowed values.
136+
EnumRemove {
137+
/// The value that was removed from the enum.
138+
removed: serde_json::Value,
139+
/// Whether the entire enum constraint was removed (rhs has no enum).
140+
/// If true, this is non-breaking as it relaxes the constraint.
141+
rhs_has_no_enum: bool,
142+
},
127143
/// A pattern constraint has been added.
128144
PatternAdd {
129145
/// The pattern that was added.
@@ -221,6 +237,16 @@ impl ChangeKind {
221237
Self::FormatAdd { .. } => true,
222238
Self::FormatRemove { .. } => false,
223239
Self::FormatChange { .. } => true,
240+
// EnumAdd is breaking only if it adds a new enum constraint (lhs had no enum).
241+
// Adding values to an existing enum is non-breaking (accepts more data).
242+
Self::EnumAdd {
243+
lhs_has_no_enum, ..
244+
} => *lhs_has_no_enum,
245+
// EnumRemove is breaking if removing values from a surviving enum constraint.
246+
// Removing the entire enum constraint is non-breaking (accepts more data).
247+
Self::EnumRemove {
248+
rhs_has_no_enum, ..
249+
} => !rhs_has_no_enum,
224250
// Pattern changes are conservatively treated as breaking.
225251
// Determining if one regex is a subset of another requires complex analysis.
226252
Self::PatternAdd { .. } => true,

tests/fixtures/enum/add.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"lhs": {
3+
"type": "string",
4+
"enum": ["error", "warning", "info"]
5+
},
6+
"rhs": {
7+
"type": "string",
8+
"enum": ["error", "warning", "info", "debug"]
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"lhs": {
3+
"type": "string",
4+
"enum": ["error", "warning", "debug"]
5+
},
6+
"rhs": {
7+
"type": "string",
8+
"enum": ["error", "info", "debug"]
9+
}
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"lhs": {
3+
"type": "string"
4+
},
5+
"rhs": {
6+
"type": "string",
7+
"enum": ["error", "warning", "info"]
8+
}
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"lhs": {
3+
"enum": ["error", 1, null, true]
4+
},
5+
"rhs": {
6+
"enum": ["error", 1, null, true, "warning"]
7+
}
8+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"lhs": {
3+
"type": "integer",
4+
"enum": [1, 2, 3]
5+
},
6+
"rhs": {
7+
"type": "integer",
8+
"enum": [1, 2, 3, 4]
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"lhs": {
3+
"type": "string",
4+
"enum": ["expectct", "expectstaple", "transaction", "default"]
5+
},
6+
"rhs": {
7+
"type": "string",
8+
"enum": ["expectct", "expectstaple", "transaction", "default", "generic"]
9+
}
10+
}

tests/fixtures/enum/remove.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"lhs": {
3+
"type": "string",
4+
"enum": ["error", "warning", "info", "debug"]
5+
},
6+
"rhs": {
7+
"type": "string",
8+
"enum": ["error", "warning", "info"]
9+
}
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"lhs": {
3+
"type": "string",
4+
"enum": ["error", "warning", "info"]
5+
},
6+
"rhs": {
7+
"type": "string"
8+
}
9+
}

0 commit comments

Comments
 (0)