super_diff 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -8
  3. data/lib/super_diff.rb +20 -11
  4. data/lib/super_diff/active_record.rb +20 -24
  5. data/lib/super_diff/active_record/diff_formatters/active_record_relation.rb +3 -3
  6. data/lib/super_diff/active_record/differs/active_record_relation.rb +3 -5
  7. data/lib/super_diff/active_record/object_inspection/inspectors/active_record_model.rb +32 -22
  8. data/lib/super_diff/active_record/object_inspection/inspectors/active_record_relation.rb +17 -7
  9. data/lib/super_diff/active_record/operation_tree_builders.rb +14 -0
  10. data/lib/super_diff/active_record/{operational_sequencers → operation_tree_builders}/active_record_model.rb +2 -2
  11. data/lib/super_diff/active_record/{operational_sequencers → operation_tree_builders}/active_record_relation.rb +4 -4
  12. data/lib/super_diff/active_record/{operation_sequences.rb → operation_trees.rb} +2 -2
  13. data/lib/super_diff/active_record/{operation_sequences → operation_trees}/active_record_relation.rb +2 -2
  14. data/lib/super_diff/active_support.rb +16 -19
  15. data/lib/super_diff/active_support/diff_formatters/hash_with_indifferent_access.rb +3 -3
  16. data/lib/super_diff/active_support/differs/hash_with_indifferent_access.rb +3 -5
  17. data/lib/super_diff/active_support/object_inspection/inspectors/hash_with_indifferent_access.rb +17 -7
  18. data/lib/super_diff/active_support/operation_tree_builders.rb +10 -0
  19. data/lib/super_diff/active_support/{operational_sequencers → operation_tree_builders}/hash_with_indifferent_access.rb +2 -2
  20. data/lib/super_diff/active_support/{operation_sequences.rb → operation_trees.rb} +2 -2
  21. data/lib/super_diff/active_support/{operation_sequences → operation_trees}/hash_with_indifferent_access.rb +2 -2
  22. data/lib/super_diff/configuration.rb +60 -0
  23. data/lib/super_diff/diff_formatters.rb +3 -3
  24. data/lib/super_diff/diff_formatters/array.rb +3 -3
  25. data/lib/super_diff/diff_formatters/base.rb +3 -2
  26. data/lib/super_diff/diff_formatters/collection.rb +2 -2
  27. data/lib/super_diff/diff_formatters/custom_object.rb +3 -3
  28. data/lib/super_diff/diff_formatters/default_object.rb +6 -8
  29. data/lib/super_diff/diff_formatters/defaults.rb +10 -0
  30. data/lib/super_diff/diff_formatters/hash.rb +3 -3
  31. data/lib/super_diff/diff_formatters/main.rb +41 -0
  32. data/lib/super_diff/diff_formatters/multiline_string.rb +3 -3
  33. data/lib/super_diff/differs.rb +4 -11
  34. data/lib/super_diff/differs/array.rb +2 -11
  35. data/lib/super_diff/differs/base.rb +20 -3
  36. data/lib/super_diff/differs/custom_object.rb +2 -11
  37. data/lib/super_diff/differs/default_object.rb +2 -8
  38. data/lib/super_diff/differs/defaults.rb +12 -0
  39. data/lib/super_diff/differs/hash.rb +2 -11
  40. data/lib/super_diff/differs/main.rb +48 -0
  41. data/lib/super_diff/differs/multiline_string.rb +2 -14
  42. data/lib/super_diff/differs/time_like.rb +15 -0
  43. data/lib/super_diff/equality_matchers.rb +3 -9
  44. data/lib/super_diff/equality_matchers/array.rb +1 -7
  45. data/lib/super_diff/equality_matchers/base.rb +1 -1
  46. data/lib/super_diff/equality_matchers/default.rb +1 -7
  47. data/lib/super_diff/equality_matchers/defaults.rb +12 -0
  48. data/lib/super_diff/equality_matchers/hash.rb +1 -7
  49. data/lib/super_diff/equality_matchers/main.rb +21 -0
  50. data/lib/super_diff/equality_matchers/multiline_string.rb +1 -7
  51. data/lib/super_diff/errors.rb +16 -0
  52. data/lib/super_diff/errors/no_diff_formatter_available_error.rb +21 -0
  53. data/lib/super_diff/errors/no_differ_available_error.rb +24 -0
  54. data/lib/super_diff/errors/no_operational_sequencer_available_error.rb +22 -0
  55. data/lib/super_diff/implementation_checks.rb +19 -0
  56. data/lib/super_diff/object_inspection.rb +1 -10
  57. data/lib/super_diff/object_inspection/inspection_tree.rb +6 -2
  58. data/lib/super_diff/object_inspection/inspectors.rb +5 -1
  59. data/lib/super_diff/object_inspection/inspectors/array.rb +20 -10
  60. data/lib/super_diff/object_inspection/inspectors/base.rb +36 -0
  61. data/lib/super_diff/object_inspection/inspectors/custom_object.rb +24 -14
  62. data/lib/super_diff/object_inspection/inspectors/default_object.rb +44 -30
  63. data/lib/super_diff/object_inspection/inspectors/defaults.rb +15 -0
  64. data/lib/super_diff/object_inspection/inspectors/hash.rb +20 -10
  65. data/lib/super_diff/object_inspection/inspectors/main.rb +35 -0
  66. data/lib/super_diff/object_inspection/inspectors/primitive.rb +20 -5
  67. data/lib/super_diff/object_inspection/inspectors/string.rb +15 -5
  68. data/lib/super_diff/object_inspection/inspectors/time_like.rb +23 -0
  69. data/lib/super_diff/object_inspection/nodes/inspection.rb +9 -2
  70. data/lib/super_diff/operation_tree_builders.rb +18 -0
  71. data/lib/super_diff/{operational_sequencers → operation_tree_builders}/array.rb +38 -59
  72. data/lib/super_diff/operation_tree_builders/base.rb +98 -0
  73. data/lib/super_diff/{operational_sequencers → operation_tree_builders}/custom_object.rb +3 -3
  74. data/lib/super_diff/{operational_sequencers → operation_tree_builders}/default_object.rb +8 -3
  75. data/lib/super_diff/operation_tree_builders/defaults.rb +5 -0
  76. data/lib/super_diff/operation_tree_builders/hash.rb +226 -0
  77. data/lib/super_diff/operation_tree_builders/main.rb +42 -0
  78. data/lib/super_diff/{operational_sequencers → operation_tree_builders}/multiline_string.rb +3 -3
  79. data/lib/super_diff/operation_tree_builders/time_like.rb +34 -0
  80. data/lib/super_diff/operation_trees.rb +13 -0
  81. data/lib/super_diff/{operation_sequences → operation_trees}/array.rb +5 -1
  82. data/lib/super_diff/{operation_sequences → operation_trees}/base.rb +7 -1
  83. data/lib/super_diff/{operation_sequences → operation_trees}/custom_object.rb +5 -1
  84. data/lib/super_diff/{operation_sequences → operation_trees}/default_object.rb +10 -8
  85. data/lib/super_diff/operation_trees/defaults.rb +5 -0
  86. data/lib/super_diff/{operation_sequences → operation_trees}/hash.rb +5 -1
  87. data/lib/super_diff/operation_trees/main.rb +35 -0
  88. data/lib/super_diff/operation_trees/multiline_string.rb +18 -0
  89. data/lib/super_diff/operations/unary_operation.rb +3 -0
  90. data/lib/super_diff/rspec.rb +45 -13
  91. data/lib/super_diff/rspec/augmented_matcher.rb +1 -1
  92. data/lib/super_diff/rspec/differ.rb +2 -17
  93. data/lib/super_diff/rspec/differs/collection_containing_exactly.rb +2 -7
  94. data/lib/super_diff/rspec/differs/collection_including.rb +2 -7
  95. data/lib/super_diff/rspec/differs/hash_including.rb +2 -7
  96. data/lib/super_diff/rspec/differs/object_having_attributes.rb +2 -7
  97. data/lib/super_diff/rspec/matcher_text_builders/match.rb +1 -1
  98. data/lib/super_diff/rspec/matcher_text_builders/respond_to.rb +1 -1
  99. data/lib/super_diff/rspec/matcher_text_template.rb +1 -1
  100. data/lib/super_diff/rspec/object_inspection.rb +0 -1
  101. data/lib/super_diff/rspec/object_inspection/inspectors.rb +16 -0
  102. data/lib/super_diff/rspec/object_inspection/inspectors/collection_containing_exactly.rb +17 -8
  103. data/lib/super_diff/rspec/object_inspection/inspectors/collection_including.rb +15 -9
  104. data/lib/super_diff/rspec/object_inspection/inspectors/hash_including.rb +20 -10
  105. data/lib/super_diff/rspec/object_inspection/inspectors/instance_of.rb +23 -0
  106. data/lib/super_diff/rspec/object_inspection/inspectors/kind_of.rb +23 -0
  107. data/lib/super_diff/rspec/object_inspection/inspectors/object_having_attributes.rb +20 -11
  108. data/lib/super_diff/rspec/object_inspection/inspectors/primitive.rb +13 -0
  109. data/lib/super_diff/rspec/object_inspection/inspectors/value_within.rb +29 -0
  110. data/lib/super_diff/rspec/operation_tree_builders.rb +22 -0
  111. data/lib/super_diff/rspec/{operational_sequencers → operation_tree_builders}/collection_containing_exactly.rb +5 -5
  112. data/lib/super_diff/rspec/{operational_sequencers → operation_tree_builders}/collection_including.rb +2 -2
  113. data/lib/super_diff/rspec/{operational_sequencers → operation_tree_builders}/hash_including.rb +3 -11
  114. data/lib/super_diff/rspec/{operational_sequencers → operation_tree_builders}/object_having_attributes.rb +4 -8
  115. data/lib/super_diff/version.rb +1 -1
  116. data/spec/examples.txt +397 -393
  117. data/spec/integration/rspec/have_attributes_matcher_spec.rb +354 -227
  118. data/spec/integration/rspec/include_matcher_spec.rb +2 -2
  119. data/spec/integration/rspec/unhandled_errors_spec.rb +68 -12
  120. data/spec/support/command_runner.rb +3 -0
  121. data/spec/support/integration/helpers.rb +12 -96
  122. data/spec/support/integration/matchers/produce_output_when_run_matcher.rb +14 -29
  123. data/spec/support/integration/test_programs/base.rb +120 -0
  124. data/spec/support/integration/test_programs/plain.rb +13 -0
  125. data/spec/support/integration/test_programs/rspec_active_record.rb +17 -0
  126. data/spec/support/integration/test_programs/rspec_rails.rb +17 -0
  127. data/spec/support/models/active_record/person.rb +4 -11
  128. data/spec/support/models/active_record/shipping_address.rb +10 -14
  129. data/spec/support/object_id.rb +6 -5
  130. data/spec/tmp/integration_spec.rb +15 -0
  131. data/spec/unit/{equality_matcher_spec.rb → equality_matchers/main_spec.rb} +157 -1
  132. data/spec/unit/object_inspection_spec.rb +77 -1
  133. data/super_diff.gemspec +0 -1
  134. metadata +72 -64
  135. data/lib/super_diff/active_record/object_inspection/map_extension.rb +0 -18
  136. data/lib/super_diff/active_record/operational_sequencers.rb +0 -14
  137. data/lib/super_diff/active_support/object_inspection/map_extension.rb +0 -15
  138. data/lib/super_diff/active_support/operational_sequencers.rb +0 -10
  139. data/lib/super_diff/diff_formatter.rb +0 -32
  140. data/lib/super_diff/differ.rb +0 -51
  141. data/lib/super_diff/differs/time.rb +0 -24
  142. data/lib/super_diff/equality_matcher.rb +0 -32
  143. data/lib/super_diff/no_differ_available_error.rb +0 -22
  144. data/lib/super_diff/no_operational_sequencer_available_error.rb +0 -20
  145. data/lib/super_diff/object_inspection/inspector.rb +0 -27
  146. data/lib/super_diff/object_inspection/inspectors/time.rb +0 -13
  147. data/lib/super_diff/object_inspection/map.rb +0 -30
  148. data/lib/super_diff/operation_sequences.rb +0 -9
  149. data/lib/super_diff/operational_sequencer.rb +0 -48
  150. data/lib/super_diff/operational_sequencers.rb +0 -17
  151. data/lib/super_diff/operational_sequencers/base.rb +0 -89
  152. data/lib/super_diff/operational_sequencers/hash.rb +0 -85
  153. data/lib/super_diff/operational_sequencers/time_like.rb +0 -30
  154. data/lib/super_diff/rspec/configuration.rb +0 -31
  155. data/lib/super_diff/rspec/object_inspection/map_extension.rb +0 -23
  156. data/lib/super_diff/rspec/operational_sequencers.rb +0 -22
@@ -2,9 +2,16 @@ module SuperDiff
2
2
  module ObjectInspection
3
3
  module Nodes
4
4
  class Inspection < Base
5
- def evaluate(_object, indent_level:, as_single_line:)
5
+ def evaluate(object, indent_level:, as_single_line:)
6
+ value =
7
+ if block
8
+ tree.evaluate_block(object, &block)
9
+ else
10
+ immediate_value
11
+ end
12
+
6
13
  SuperDiff::ObjectInspection.inspect(
7
- immediate_value,
14
+ value,
8
15
  indent_level: indent_level,
9
16
  as_single_line: as_single_line,
10
17
  )
@@ -0,0 +1,18 @@
1
+ module SuperDiff
2
+ module OperationTreeBuilders
3
+ autoload :Array, "super_diff/operation_tree_builders/array"
4
+ autoload :Base, "super_diff/operation_tree_builders/base"
5
+ autoload :CustomObject, "super_diff/operation_tree_builders/custom_object"
6
+ autoload :DefaultObject, "super_diff/operation_tree_builders/default_object"
7
+ autoload :Hash, "super_diff/operation_tree_builders/hash"
8
+ autoload :Main, "super_diff/operation_tree_builders/main"
9
+ # TODO: Where is this used?
10
+ autoload(
11
+ :MultilineString,
12
+ "super_diff/operation_tree_builders/multiline_string",
13
+ )
14
+ autoload :TimeLike, "super_diff/operation_tree_builders/time_like"
15
+ end
16
+ end
17
+
18
+ require "super_diff/operation_tree_builders/defaults"
@@ -1,5 +1,5 @@
1
1
  module SuperDiff
2
- module OperationalSequencers
2
+ module OperationTreeBuilders
3
3
  class Array < Base
4
4
  def self.applies_to?(expected, actual)
5
5
  expected.is_a?(::Array) && actual.is_a?(::Array)
@@ -7,72 +7,40 @@ module SuperDiff
7
7
 
8
8
  def call
9
9
  Diff::LCS.traverse_balanced(expected, actual, lcs_callbacks)
10
- operations
10
+ operation_tree
11
11
  end
12
12
 
13
13
  private
14
14
 
15
15
  def lcs_callbacks
16
16
  @_lcs_callbacks ||= LcsCallbacks.new(
17
- operations: operations,
17
+ operation_tree: operation_tree,
18
18
  expected: expected,
19
19
  actual: actual,
20
- extra_operational_sequencer_classes: extra_operational_sequencer_classes,
21
- extra_diff_formatter_classes: extra_diff_formatter_classes,
22
20
  sequence: method(:sequence),
23
21
  )
24
22
  end
25
23
 
26
- def operations
27
- @_operations ||= OperationSequences::Array.new([])
24
+ def operation_tree
25
+ @_operation_tree ||= OperationTrees::Array.new([])
28
26
  end
29
27
 
30
28
  class LcsCallbacks
31
29
  extend AttrExtras.mixin
32
30
 
33
- pattr_initialize(
34
- [
35
- :operations!,
36
- :expected!,
37
- :actual!,
38
- :extra_operational_sequencer_classes!,
39
- :extra_diff_formatter_classes!,
40
- :sequence!
41
- ],
42
- )
43
- public :operations
31
+ pattr_initialize [:operation_tree!, :expected!, :actual!, :sequence!]
32
+ public :operation_tree
44
33
 
45
34
  def match(event)
46
- operations << ::SuperDiff::Operations::UnaryOperation.new(
47
- name: :noop,
48
- collection: actual,
49
- key: event.new_position,
50
- value: event.new_element,
51
- index: event.new_position,
52
- index_in_collection: actual.index(event.new_element),
53
- )
35
+ add_noop_operation(event)
54
36
  end
55
37
 
56
38
  def discard_a(event)
57
- operations << ::SuperDiff::Operations::UnaryOperation.new(
58
- name: :delete,
59
- collection: expected,
60
- key: event.old_position,
61
- value: event.old_element,
62
- index: event.old_position,
63
- index_in_collection: expected.index(event.old_element),
64
- )
39
+ add_delete_operation(event)
65
40
  end
66
41
 
67
42
  def discard_b(event)
68
- operations << ::SuperDiff::Operations::UnaryOperation.new(
69
- name: :insert,
70
- collection: actual,
71
- key: event.new_position,
72
- value: event.new_element,
73
- index: event.new_position,
74
- index_in_collection: actual.index(event.new_element),
75
- )
43
+ add_insert_operation(event)
76
44
  end
77
45
 
78
46
  def change(event)
@@ -88,23 +56,8 @@ module SuperDiff
88
56
 
89
57
  private
90
58
 
91
- def add_change_operation(event, child_operations)
92
- operations << ::SuperDiff::Operations::BinaryOperation.new(
93
- name: :change,
94
- left_collection: expected,
95
- right_collection: actual,
96
- left_key: event.old_position,
97
- right_key: event.new_position,
98
- left_value: event.old_element,
99
- right_value: event.new_element,
100
- left_index: event.old_position,
101
- right_index: event.new_position,
102
- child_operations: child_operations,
103
- )
104
- end
105
-
106
59
  def add_delete_operation(event)
107
- operations << Operations::UnaryOperation.new(
60
+ operation_tree << Operations::UnaryOperation.new(
108
61
  name: :delete,
109
62
  collection: expected,
110
63
  key: event.old_position,
@@ -115,7 +68,7 @@ module SuperDiff
115
68
  end
116
69
 
117
70
  def add_insert_operation(event)
118
- operations << Operations::UnaryOperation.new(
71
+ operation_tree << Operations::UnaryOperation.new(
119
72
  name: :insert,
120
73
  collection: actual,
121
74
  key: event.new_position,
@@ -124,6 +77,32 @@ module SuperDiff
124
77
  index_in_collection: actual.index(event.new_element),
125
78
  )
126
79
  end
80
+
81
+ def add_noop_operation(event)
82
+ operation_tree << Operations::UnaryOperation.new(
83
+ name: :noop,
84
+ collection: actual,
85
+ key: event.new_position,
86
+ value: event.new_element,
87
+ index: event.new_position,
88
+ index_in_collection: actual.index(event.new_element),
89
+ )
90
+ end
91
+
92
+ def add_change_operation(event, child_operations)
93
+ operation_tree << Operations::BinaryOperation.new(
94
+ name: :change,
95
+ left_collection: expected,
96
+ right_collection: actual,
97
+ left_key: event.old_position,
98
+ right_key: event.new_position,
99
+ left_value: event.old_element,
100
+ right_value: event.new_element,
101
+ left_index: event.old_position,
102
+ right_index: event.new_position,
103
+ child_operations: child_operations,
104
+ )
105
+ end
127
106
  end
128
107
  end
129
108
  end
@@ -0,0 +1,98 @@
1
+ module SuperDiff
2
+ module OperationTreeBuilders
3
+ class Base
4
+ def self.applies_to?(_expected, _actual)
5
+ raise NotImplementedError
6
+ end
7
+
8
+ extend AttrExtras.mixin
9
+ include ImplementationChecks
10
+
11
+ method_object [:expected!, :actual!]
12
+
13
+ def call
14
+ operation_tree
15
+ end
16
+
17
+ protected
18
+
19
+ def unary_operations
20
+ unimplemented_instance_method!
21
+ end
22
+
23
+ def build_operation_tree
24
+ unimplemented_instance_method!
25
+ end
26
+
27
+ private
28
+
29
+ def operation_tree
30
+ unary_operations = self.unary_operations
31
+ operation_tree = build_operation_tree
32
+ unmatched_delete_operations = []
33
+
34
+ unary_operations.each_with_index do |operation, index|
35
+ if (
36
+ operation.name == :insert &&
37
+ (delete_operation = unmatched_delete_operations.find { |op| op.key == operation.key }) &&
38
+ (insert_operation = operation)
39
+ )
40
+ unmatched_delete_operations.delete(delete_operation)
41
+
42
+ if (child_operations = possible_comparison_of(
43
+ delete_operation,
44
+ insert_operation,
45
+ ))
46
+ operation_tree.delete(delete_operation)
47
+ operation_tree << Operations::BinaryOperation.new(
48
+ name: :change,
49
+ left_collection: delete_operation.collection,
50
+ right_collection: insert_operation.collection,
51
+ left_key: delete_operation.key,
52
+ right_key: insert_operation.key,
53
+ left_value: delete_operation.collection[operation.key],
54
+ right_value: insert_operation.collection[operation.key],
55
+ left_index: delete_operation.index_in_collection,
56
+ right_index: insert_operation.index_in_collection,
57
+ child_operations: child_operations,
58
+ )
59
+ else
60
+ operation_tree << insert_operation
61
+ end
62
+ else
63
+ if operation.name == :delete
64
+ unmatched_delete_operations << operation
65
+ end
66
+
67
+ operation_tree << operation
68
+ end
69
+ end
70
+
71
+ operation_tree
72
+ end
73
+
74
+ def possible_comparison_of(operation, next_operation)
75
+ if should_compare?(operation, next_operation)
76
+ sequence(operation.value, next_operation.value)
77
+ else
78
+ nil
79
+ end
80
+ end
81
+
82
+ def should_compare?(operation, next_operation)
83
+ next_operation &&
84
+ operation.name == :delete &&
85
+ next_operation.name == :insert &&
86
+ next_operation.key == operation.key
87
+ end
88
+
89
+ def sequence(expected, actual)
90
+ OperationTreeBuilders::Main.call(
91
+ expected: expected,
92
+ actual: actual,
93
+ all_or_nothing: false,
94
+ )
95
+ end
96
+ end
97
+ end
98
+ end
@@ -1,5 +1,5 @@
1
1
  module SuperDiff
2
- module OperationalSequencers
2
+ module OperationTreeBuilders
3
3
  class CustomObject < DefaultObject
4
4
  def self.applies_to?(expected, actual)
5
5
  expected.class == actual.class &&
@@ -7,9 +7,9 @@ module SuperDiff
7
7
  actual.respond_to?(:attributes_for_super_diff)
8
8
  end
9
9
 
10
- def build_operation_sequencer
10
+ def build_operation_tree
11
11
  # XXX This assumes that `expected` and `actual` are the same
12
- OperationSequences::CustomObject.new([], value_class: expected.class)
12
+ OperationTrees::CustomObject.new([], value_class: expected.class)
13
13
  end
14
14
 
15
15
  def attribute_names
@@ -1,5 +1,5 @@
1
1
  module SuperDiff
2
- module OperationalSequencers
2
+ module OperationTreeBuilders
3
3
  class DefaultObject < Base
4
4
  def self.applies_to?(_expected, _actual)
5
5
  true
@@ -22,9 +22,14 @@ module SuperDiff
22
22
  end
23
23
  end
24
24
 
25
- def build_operation_sequencer
25
+ def build_operation_tree
26
26
  # XXX This assumes that `expected` and `actual` are the same
27
- OperationSequences::DefaultObject.new([], value_class: expected.class)
27
+ # TODO: Does this need to be find_operation_tree_for?
28
+ OperationTrees::DefaultObject.new([], value_class: expected.class)
29
+ end
30
+
31
+ def find_operation_tree_for(value)
32
+ OperationTrees::Main.call(value)
28
33
  end
29
34
 
30
35
  def attribute_names
@@ -0,0 +1,5 @@
1
+ module SuperDiff
2
+ module OperationTreeBuilders
3
+ DEFAULTS = [Array, Hash, CustomObject].freeze
4
+ end
5
+ end
@@ -0,0 +1,226 @@
1
+ module SuperDiff
2
+ module OperationTreeBuilders
3
+ class Hash < Base
4
+ def self.applies_to?(expected, actual)
5
+ expected.is_a?(::Hash) && actual.is_a?(::Hash)
6
+ end
7
+
8
+ protected
9
+
10
+ def unary_operations
11
+ unary_operations_using_variant_of_patience_algorithm
12
+ end
13
+
14
+ def build_operation_tree
15
+ OperationTrees::Hash.new([])
16
+ end
17
+
18
+ private
19
+
20
+ def unary_operations_using_variant_of_patience_algorithm
21
+ operations = []
22
+ aks, eks = actual.keys, expected.keys
23
+ previous_ei, ei = nil, 0
24
+ ai = 0
25
+
26
+ # When diffing a hash, we're more interested in the 'actual' version
27
+ # than the 'expected' version, because that's the ultimate truth.
28
+ # Therefore, the diff is presented from the perspective of the 'actual'
29
+ # hash, and we start off by looping over it.
30
+ while ai < aks.size
31
+ ak = aks[ai]
32
+ av, ev = actual[ak], expected[ak]
33
+ # While we iterate over 'actual' in order, we jump all over
34
+ # 'expected', trying to match up its keys with the keys in 'actual' as
35
+ # much as possible.
36
+ ei = eks.index(ak)
37
+
38
+ if should_add_noop_operation?(ak)
39
+ # (If we're here, it probably means that the key we're pointing to
40
+ # in the 'actual' and 'expected' hashes have the same value.)
41
+
42
+ if ei && previous_ei && (ei - previous_ei) > 1
43
+ # If we've jumped from one operation in the 'expected' hash to
44
+ # another operation later in 'expected' (due to the fact that the
45
+ # 'expected' hash is in a different order than 'actual'), collect
46
+ # any delete operations in between and add them to our operations
47
+ # array as deletes before adding the noop. If we don't do this
48
+ # now, then those deletes will disappear. (Again, we are mainly
49
+ # iterating over 'actual', so this is the only way to catch all of
50
+ # the keys in 'expected'.)
51
+ (previous_ei + 1).upto(ei - 1) do |ei2|
52
+ ek = eks[ei2]
53
+ ev2, av2 = expected[ek], actual[ek]
54
+
55
+ if (
56
+ (!actual.include?(ek) || ev != av2) &&
57
+ operations.none? { |operation|
58
+ [:delete, :noop].include?(operation.name) &&
59
+ operation.key == ek
60
+ }
61
+ )
62
+ operations << Operations::UnaryOperation.new(
63
+ name: :delete,
64
+ collection: expected,
65
+ key: ek,
66
+ value: ev2,
67
+ index: ei2,
68
+ index_in_collection: ei2,
69
+ )
70
+ end
71
+ end
72
+ end
73
+
74
+ operations << Operations::UnaryOperation.new(
75
+ name: :noop,
76
+ collection: actual,
77
+ key: ak,
78
+ value: av,
79
+ index: ai,
80
+ index_in_collection: ai,
81
+ )
82
+ else
83
+ # (If we're here, it probably means that the key in 'actual' isn't
84
+ # present in 'expected' or the values don't match.)
85
+
86
+ if (
87
+ (operations.empty? || operations.last.name == :noop) &&
88
+ (ai == 0 || eks.include?(aks[ai - 1]))
89
+ )
90
+ # If we go from a match in the last iteration to a missing or
91
+ # extra key in this one, or we're at the first key in 'actual' and
92
+ # it's missing or extra, look for deletes in the 'expected' hash
93
+ # and add them to our list of operations before we add the
94
+ # inserts. In most cases we will accomplish this by backtracking a
95
+ # bit to the key in 'expected' that matched the key in 'actual' we
96
+ # processed in the previous iteration (or just the first key in
97
+ # 'expected' if this is the first key in 'actual'), and then
98
+ # iterating from there through 'expected' until we reach the end
99
+ # or we hit some other condition (see below).
100
+
101
+ start_index =
102
+ if ai > 0
103
+ eks.index(aks[ai - 1]) + 1
104
+ else
105
+ 0
106
+ end
107
+
108
+ start_index.upto(eks.size - 1) do |ei2|
109
+ ek = eks[ei2]
110
+ ev, av2 = expected[ek], actual[ek]
111
+
112
+ if actual.include?(ek) && ev == av2
113
+ # If the key in 'expected' we've landed on happens to be a
114
+ # match in 'actual', then stop, because it's going to be
115
+ # handled in some future iteration of the 'actual' loop.
116
+ break
117
+ elsif (
118
+ aks[ai + 1 .. -1].any? { |k|
119
+ expected.include?(k) && expected[k] != actual[k]
120
+ }
121
+ )
122
+ # While we backtracked a bit to iterate over 'expected', we
123
+ # now have to look ahead. If we will end up encountering a
124
+ # insert that matches this delete later, stop and go back to
125
+ # iterating over 'actual'. This is because the delete we would
126
+ # have added now will be added later when we encounter the
127
+ # associated insert, so we don't want to add it twice.
128
+ break
129
+ else
130
+ operations << Operations::UnaryOperation.new(
131
+ name: :delete,
132
+ collection: expected,
133
+ key: ek,
134
+ value: ev,
135
+ index: ei2,
136
+ index_in_collection: ei2,
137
+ )
138
+ end
139
+
140
+ if ek == ak && ev != av
141
+ # If we're pointing to the same key in 'expected' as in
142
+ # 'actual', but with different values, go ahead and add an
143
+ # insert now to accompany the delete added above. That way
144
+ # they appear together, which will be easier to read.
145
+ operations << Operations::UnaryOperation.new(
146
+ name: :insert,
147
+ collection: actual,
148
+ key: ak,
149
+ value: av,
150
+ index: ai,
151
+ index_in_collection: ai,
152
+ )
153
+ end
154
+ end
155
+ end
156
+
157
+ if (
158
+ expected.include?(ak) &&
159
+ ev != av &&
160
+ operations.none? { |op| op.name == :delete && op.key == ak }
161
+ )
162
+ # If we're here, it means that we didn't encounter any delete
163
+ # operations above for whatever reason and so we need to add a
164
+ # delete to represent the fact that the value for this key has
165
+ # changed.
166
+ operations << Operations::UnaryOperation.new(
167
+ name: :delete,
168
+ collection: expected,
169
+ key: ak,
170
+ value: expected[ak],
171
+ index: ei,
172
+ index_in_collection: ei,
173
+ )
174
+ end
175
+
176
+ if operations.none? { |op| op.name == :insert && op.key == ak }
177
+ # If we're here, it means that we didn't encounter any insert
178
+ # operations above. Since we already handled delete, the only
179
+ # alternative is that this key must not exist in 'expected', so
180
+ # we need to add an insert.
181
+ operations << Operations::UnaryOperation.new(
182
+ name: :insert,
183
+ collection: actual,
184
+ key: ak,
185
+ value: av,
186
+ index: ai,
187
+ index_in_collection: ai,
188
+ )
189
+ end
190
+ end
191
+
192
+ ai += 1
193
+ previous_ei = ei
194
+ end
195
+
196
+ # The last thing to do is this: if there are keys in 'expected' that
197
+ # aren't in 'actual', and they aren't associated with any inserts to
198
+ # where they would have been added above, tack those deletes onto the
199
+ # end of our operations array.
200
+ (eks - aks - operations.map(&:key)).each do |ek|
201
+ ei = eks.index(ek)
202
+ ev = expected[ek]
203
+
204
+ operations << Operations::UnaryOperation.new(
205
+ name: :delete,
206
+ collection: expected,
207
+ key: ek,
208
+ value: ev,
209
+ index: ei,
210
+ index_in_collection: ei,
211
+ )
212
+ end
213
+
214
+ operations
215
+ end
216
+
217
+ def should_add_noop_operation?(key)
218
+ expected.include?(key) && expected[key] == actual[key]
219
+ end
220
+
221
+ def all_keys
222
+ actual.keys | expected.keys
223
+ end
224
+ end
225
+ end
226
+ end