super_diff 0.1.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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +174 -0
  3. data/lib/super_diff/csi/color_helper.rb +52 -0
  4. data/lib/super_diff/csi/eight_bit_color.rb +131 -0
  5. data/lib/super_diff/csi/eight_bit_sequence.rb +27 -0
  6. data/lib/super_diff/csi/four_bit_color.rb +80 -0
  7. data/lib/super_diff/csi/four_bit_sequence.rb +24 -0
  8. data/lib/super_diff/csi/reset_sequence.rb +9 -0
  9. data/lib/super_diff/csi/sequence.rb +22 -0
  10. data/lib/super_diff/csi/twenty_four_bit_color.rb +41 -0
  11. data/lib/super_diff/csi/twenty_four_bit_sequence.rb +27 -0
  12. data/lib/super_diff/csi.rb +29 -0
  13. data/lib/super_diff/diff_formatter.rb +37 -0
  14. data/lib/super_diff/diff_formatters/array.rb +21 -0
  15. data/lib/super_diff/diff_formatters/base.rb +37 -0
  16. data/lib/super_diff/diff_formatters/collection.rb +107 -0
  17. data/lib/super_diff/diff_formatters/hash.rb +34 -0
  18. data/lib/super_diff/diff_formatters/multi_line_string.rb +31 -0
  19. data/lib/super_diff/diff_formatters/object.rb +27 -0
  20. data/lib/super_diff/diff_formatters.rb +5 -0
  21. data/lib/super_diff/differ.rb +48 -0
  22. data/lib/super_diff/differs/array.rb +24 -0
  23. data/lib/super_diff/differs/base.rb +42 -0
  24. data/lib/super_diff/differs/empty.rb +13 -0
  25. data/lib/super_diff/differs/hash.rb +24 -0
  26. data/lib/super_diff/differs/multi_line_string.rb +27 -0
  27. data/lib/super_diff/differs/object.rb +68 -0
  28. data/lib/super_diff/differs.rb +5 -0
  29. data/lib/super_diff/equality_matcher.rb +45 -0
  30. data/lib/super_diff/equality_matchers/array.rb +44 -0
  31. data/lib/super_diff/equality_matchers/base.rb +42 -0
  32. data/lib/super_diff/equality_matchers/hash.rb +44 -0
  33. data/lib/super_diff/equality_matchers/multi_line_string.rb +44 -0
  34. data/lib/super_diff/equality_matchers/object.rb +18 -0
  35. data/lib/super_diff/equality_matchers/single_line_string.rb +28 -0
  36. data/lib/super_diff/equality_matchers.rb +5 -0
  37. data/lib/super_diff/errors.rb +20 -0
  38. data/lib/super_diff/helpers.rb +96 -0
  39. data/lib/super_diff/operation_sequences/array.rb +14 -0
  40. data/lib/super_diff/operation_sequences/base.rb +11 -0
  41. data/lib/super_diff/operation_sequences/hash.rb +14 -0
  42. data/lib/super_diff/operation_sequences/object.rb +14 -0
  43. data/lib/super_diff/operational_sequencer.rb +43 -0
  44. data/lib/super_diff/operational_sequencers/array.rb +127 -0
  45. data/lib/super_diff/operational_sequencers/base.rb +97 -0
  46. data/lib/super_diff/operational_sequencers/hash.rb +82 -0
  47. data/lib/super_diff/operational_sequencers/multi_line_string.rb +85 -0
  48. data/lib/super_diff/operational_sequencers/object.rb +96 -0
  49. data/lib/super_diff/operational_sequencers.rb +5 -0
  50. data/lib/super_diff/operations/binary_operation.rb +47 -0
  51. data/lib/super_diff/operations/unary_operation.rb +25 -0
  52. data/lib/super_diff/rspec/differ.rb +30 -0
  53. data/lib/super_diff/rspec/monkey_patches.rb +122 -0
  54. data/lib/super_diff/rspec.rb +19 -0
  55. data/lib/super_diff/value_inspection.rb +11 -0
  56. data/lib/super_diff/version.rb +3 -0
  57. data/lib/super_diff.rb +50 -0
  58. data/spec/examples.txt +46 -0
  59. data/spec/integration/rspec_spec.rb +261 -0
  60. data/spec/spec_helper.rb +44 -0
  61. data/spec/support/color_helper.rb +49 -0
  62. data/spec/support/command_runner.rb +279 -0
  63. data/spec/support/integration/matchers/produce_output_when_run_matcher.rb +76 -0
  64. data/spec/support/person.rb +23 -0
  65. data/spec/support/person_diff_formatter.rb +15 -0
  66. data/spec/support/person_operation_sequence.rb +14 -0
  67. data/spec/support/person_operational_sequencer.rb +19 -0
  68. data/spec/unit/equality_matcher_spec.rb +1233 -0
  69. data/super_diff.gemspec +23 -0
  70. metadata +153 -0
@@ -0,0 +1,44 @@
1
+ module SuperDiff
2
+ module EqualityMatchers
3
+ class MultiLineString < Base
4
+ def self.applies_to?(value)
5
+ value.is_a?(::String) && value.include?("\n")
6
+ end
7
+
8
+ def fail
9
+ <<~OUTPUT.strip
10
+ Differing strings.
11
+
12
+ #{
13
+ Helpers.style(
14
+ :deleted,
15
+ "Expected: #{Helpers.inspect_object(expected)}",
16
+ )
17
+ }
18
+ #{
19
+ Helpers.style(
20
+ :inserted,
21
+ " Actual: #{Helpers.inspect_object(actual)}",
22
+ )
23
+ }
24
+
25
+ Diff:
26
+
27
+ #{diff}
28
+ OUTPUT
29
+ end
30
+
31
+ private
32
+
33
+ def diff
34
+ Differs::MultiLineString.call(
35
+ expected,
36
+ actual,
37
+ indent_level: 0,
38
+ extra_operational_sequencer_classes: extra_operational_sequencer_classes,
39
+ extra_diff_formatter_classes: extra_diff_formatter_classes,
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,18 @@
1
+ module SuperDiff
2
+ module EqualityMatchers
3
+ class Object < Base
4
+ def self.applies_to?(value)
5
+ value.class == ::Object
6
+ end
7
+
8
+ def fail
9
+ <<~OUTPUT.strip
10
+ Differing #{Helpers.plural_type_for(actual)}.
11
+
12
+ #{Helpers.style :deleted, "Expected: #{expected.inspect}"}
13
+ #{Helpers.style :inserted, " Actual: #{actual.inspect}"}
14
+ OUTPUT
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,28 @@
1
+ module SuperDiff
2
+ module EqualityMatchers
3
+ class SingleLineString < Base
4
+ def self.applies_to?(value)
5
+ value.class == ::String
6
+ end
7
+
8
+ def fail
9
+ <<~OUTPUT.strip
10
+ Differing strings.
11
+
12
+ #{
13
+ Helpers.style(
14
+ :deleted,
15
+ "Expected: #{Helpers.inspect_object(expected)}",
16
+ )
17
+ }
18
+ #{
19
+ Helpers.style(
20
+ :inserted,
21
+ " Actual: #{Helpers.inspect_object(actual)}",
22
+ )
23
+ }
24
+ OUTPUT
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ module SuperDiff
2
+ module EqualityMatchers
3
+ DEFAULTS = [Array, Hash, MultiLineString, SingleLineString, Object].freeze
4
+ end
5
+ end
@@ -0,0 +1,20 @@
1
+ module SuperDiff
2
+ class NoOperationalSequencerAvailableError < StandardError
3
+ def self.create(expected, actual)
4
+ allocate.tap do |error|
5
+ error.expected = expected
6
+ error.actual = actual
7
+ error.__send__(:initialize)
8
+ end
9
+ end
10
+
11
+ attr_accessor :expected, :actual
12
+
13
+ def initialize
14
+ super(<<-MESSAGE)
15
+ There is no operational sequencer available to handle an "expected" value of
16
+ type #{expected.class} and an "actual" value of type #{actual.class}.
17
+ MESSAGE
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,96 @@
1
+ module SuperDiff
2
+ module Helpers
3
+ COLORS = { normal: :plain, inserted: :green, deleted: :red }.freeze
4
+
5
+ def self.style(style_name, text)
6
+ Csi::ColorHelper.public_send(COLORS.fetch(style_name), text)
7
+ end
8
+
9
+ def self.plural_type_for(value)
10
+ case value
11
+ when Numeric then "numbers"
12
+ when String then "strings"
13
+ when Symbol then "symbols"
14
+ else "objects"
15
+ end
16
+ end
17
+
18
+ def self.inspect_object(value_to_inspect, single_line: true)
19
+ case value_to_inspect
20
+ when ::Hash
21
+ inspect_hash(value_to_inspect, single_line: single_line)
22
+ when String
23
+ inspect_string(value_to_inspect)
24
+ when ::Array
25
+ inspect_array(value_to_inspect)
26
+ else
27
+ inspect_unclassified_object(value_to_inspect, single_line: single_line)
28
+ end
29
+ end
30
+
31
+ def self.inspect_hash(hash, single_line: true)
32
+ contents = hash.map do |key, value|
33
+ if key.is_a?(Symbol)
34
+ "#{key}: #{inspect_object(value)}"
35
+ else
36
+ "#{inspect_object(key)} => #{inspect_object(value)}"
37
+ end
38
+ end
39
+
40
+ if single_line
41
+ ["{", contents.join(", "), "}"].join(" ")
42
+ else
43
+ ValueInspection.new(
44
+ beginning: "{",
45
+ middle: contents.map.with_index do |line, index|
46
+ if index < contents.size - 1
47
+ line + ","
48
+ else
49
+ line
50
+ end
51
+ end,
52
+ end: "}",
53
+ )
54
+ end
55
+ end
56
+ private_class_method :inspect_hash
57
+
58
+ def self.inspect_string(string)
59
+ newline = "⏎"
60
+ string.gsub(/\r\n/, newline).gsub(/\n/, newline).inspect
61
+ end
62
+ private_class_method :inspect_string
63
+
64
+ def self.inspect_array(array)
65
+ "[" + array.map { |element| inspect_object(element) }.join(", ") + "]"
66
+ end
67
+ private_class_method :inspect_array
68
+
69
+ def self.inspect_unclassified_object(object, single_line: true)
70
+ if object.respond_to?(:attributes_for_super_diff)
71
+ attributes = object.attributes_for_super_diff
72
+ inspected_attributes =
73
+ attributes.map.with_index do |(key, value), index|
74
+ "#{key}: #{value.inspect}".tap do |line|
75
+ if index < attributes.size - 1
76
+ line << ","
77
+ end
78
+ end
79
+ end
80
+
81
+ if single_line
82
+ "#<#{object.class} #{inspected_attributes.join(" ")}>"
83
+ else
84
+ ValueInspection.new(
85
+ beginning: "#<#{object.class} {",
86
+ middle: inspected_attributes,
87
+ end: "}>",
88
+ )
89
+ end
90
+ else
91
+ object.inspect
92
+ end
93
+ end
94
+ private_class_method :inspect_unclassified_object
95
+ end
96
+ end
@@ -0,0 +1,14 @@
1
+ module SuperDiff
2
+ module OperationSequences
3
+ class Array < Base
4
+ def to_diff(indent_level:, collection_prefix:, add_comma:)
5
+ DiffFormatters::Array.call(
6
+ self,
7
+ indent_level: indent_level,
8
+ collection_prefix: collection_prefix,
9
+ add_comma: add_comma,
10
+ )
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ module SuperDiff
2
+ module OperationSequences
3
+ class Base < SimpleDelegator
4
+ # rubocop:disable Lint/UnusedMethodArgument
5
+ def to_diff(indent_level:, add_comma:, collection_prefix: nil)
6
+ raise NotImplementedError
7
+ end
8
+ # rubocop:enable Lint/UnusedMethodArgument
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ module SuperDiff
2
+ module OperationSequences
3
+ class Hash < Base
4
+ def to_diff(indent_level:, collection_prefix:, add_comma:)
5
+ DiffFormatters::Hash.call(
6
+ self,
7
+ indent_level: indent_level,
8
+ collection_prefix: collection_prefix,
9
+ add_comma: add_comma,
10
+ )
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module SuperDiff
2
+ module OperationSequences
3
+ class Object < Base
4
+ def to_diff(indent_level:, collection_prefix:, add_comma:)
5
+ DiffFormatters::Object.call(
6
+ self,
7
+ indent_level: indent_level,
8
+ collection_prefix: collection_prefix,
9
+ add_comma: add_comma,
10
+ )
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,43 @@
1
+ module SuperDiff
2
+ class OperationalSequencer
3
+ def self.call(*args)
4
+ new(*args).call
5
+ end
6
+
7
+ def initialize(
8
+ expected:,
9
+ actual:,
10
+ extra_classes: [],
11
+ extra_diff_formatter_classes: []
12
+ )
13
+ @expected = expected
14
+ @actual = actual
15
+ @extra_classes = extra_classes
16
+ @extra_diff_formatter_classes = extra_diff_formatter_classes
17
+ end
18
+
19
+ def call
20
+ if resolved_class
21
+ resolved_class.call(
22
+ expected: expected,
23
+ actual: actual,
24
+ extra_operational_sequencer_classes: extra_classes,
25
+ extra_diff_formatter_classes: extra_diff_formatter_classes,
26
+ )
27
+ else
28
+ raise NoOperationalSequencerAvailableError.create(expected, actual)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :expected, :actual, :extra_classes,
35
+ :extra_diff_formatter_classes
36
+
37
+ def resolved_class
38
+ (OperationalSequencers::DEFAULTS + extra_classes).find do |klass|
39
+ klass.applies_to?(expected) && klass.applies_to?(actual)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,127 @@
1
+ module SuperDiff
2
+ module OperationalSequencers
3
+ class Array < Base
4
+ def self.applies_to?(value)
5
+ value.is_a?(::Array)
6
+ end
7
+
8
+ def initialize(*args)
9
+ super(*args)
10
+
11
+ @lcs_callbacks = LcsCallbacks.new(
12
+ expected: expected,
13
+ actual: actual,
14
+ extra_operational_sequencer_classes: extra_operational_sequencer_classes,
15
+ extra_diff_formatter_classes: extra_diff_formatter_classes,
16
+ )
17
+ end
18
+
19
+ def call
20
+ Diff::LCS.traverse_balanced(expected, actual, lcs_callbacks)
21
+ lcs_callbacks.operations
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :lcs_callbacks
27
+
28
+ class LcsCallbacks
29
+ attr_reader :operations
30
+
31
+ def initialize(
32
+ expected:,
33
+ actual:,
34
+ extra_operational_sequencer_classes:,
35
+ extra_diff_formatter_classes:
36
+ )
37
+ @expected = expected
38
+ @actual = actual
39
+ @operations = OperationSequences::Array.new([])
40
+ @extra_operational_sequencer_classes =
41
+ extra_operational_sequencer_classes
42
+ @extra_diff_formatter_classes = extra_diff_formatter_classes
43
+ end
44
+
45
+ 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
+ )
53
+ end
54
+
55
+ def discard_a(event)
56
+ operations << ::SuperDiff::Operations::UnaryOperation.new(
57
+ name: :delete,
58
+ collection: expected,
59
+ key: event.old_position,
60
+ value: event.old_element,
61
+ index: event.old_position,
62
+ )
63
+ end
64
+
65
+ def discard_b(event)
66
+ operations << ::SuperDiff::Operations::UnaryOperation.new(
67
+ name: :insert,
68
+ collection: actual,
69
+ key: event.new_position,
70
+ value: event.new_element,
71
+ index: event.new_position,
72
+ )
73
+ end
74
+
75
+ def change(event)
76
+ child_operations = sequence(event.old_element, event.new_element)
77
+
78
+ if child_operations
79
+ operations << ::SuperDiff::Operations::BinaryOperation.new(
80
+ name: :change,
81
+ left_collection: expected,
82
+ right_collection: actual,
83
+ left_key: event.old_position,
84
+ right_key: event.new_position,
85
+ left_value: event.old_element,
86
+ right_value: event.new_element,
87
+ left_index: event.old_position,
88
+ right_index: event.new_position,
89
+ child_operations: child_operations,
90
+ )
91
+ else
92
+ operations << Operations::UnaryOperation.new(
93
+ name: :delete,
94
+ collection: expected,
95
+ key: event.old_position,
96
+ value: event.old_element,
97
+ index: event.old_position,
98
+ )
99
+ operations << Operations::UnaryOperation.new(
100
+ name: :insert,
101
+ collection: actual,
102
+ key: event.new_position,
103
+ value: event.new_element,
104
+ index: event.new_position,
105
+ )
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ attr_reader :expected, :actual, :extra_operational_sequencer_classes,
112
+ :extra_diff_formatter_classes
113
+
114
+ def sequence(expected, actual)
115
+ OperationalSequencer.call(
116
+ expected: expected,
117
+ actual: actual,
118
+ extra_classes: extra_operational_sequencer_classes,
119
+ extra_diff_formatter_classes: extra_diff_formatter_classes,
120
+ )
121
+ rescue NoOperationalSequencerAvailableError
122
+ nil
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,97 @@
1
+ module SuperDiff
2
+ module OperationalSequencers
3
+ class Base
4
+ def self.applies_to?(_value)
5
+ raise NotImplementedError
6
+ end
7
+
8
+ def self.call(*args)
9
+ new(*args).call
10
+ end
11
+
12
+ def initialize(
13
+ expected:,
14
+ actual:,
15
+ extra_operational_sequencer_classes: [],
16
+ extra_diff_formatter_classes: []
17
+ )
18
+ @expected = expected
19
+ @actual = actual
20
+ @extra_operational_sequencer_classes =
21
+ extra_operational_sequencer_classes
22
+ @extra_diff_formatter_classes = extra_diff_formatter_classes
23
+ end
24
+
25
+ def call
26
+ i = 0
27
+ operations = operation_sequence_class.new([])
28
+
29
+ while i < unary_operations.length
30
+ operation = unary_operations[i]
31
+ next_operation = unary_operations[i + 1]
32
+ child_operations = possible_comparison_of(operation, next_operation)
33
+
34
+ if child_operations
35
+ operations << Operations::BinaryOperation.new(
36
+ name: :change,
37
+ left_collection: operation.collection,
38
+ right_collection: next_operation.collection,
39
+ left_key: operation.key,
40
+ right_key: operation.key,
41
+ left_value: operation.collection[operation.key],
42
+ right_value: next_operation.collection[operation.key],
43
+ left_index: operation.index,
44
+ right_index: operation.index,
45
+ child_operations: child_operations,
46
+ )
47
+ i += 2
48
+ else
49
+ operations << operation
50
+ i += 1
51
+ end
52
+ end
53
+
54
+ operations
55
+ end
56
+
57
+ protected
58
+
59
+ def unary_operations
60
+ raise NotImplementedError
61
+ end
62
+
63
+ def operation_sequence_class
64
+ raise NotImplementedError
65
+ end
66
+
67
+ private
68
+
69
+ attr_reader :expected, :actual, :extra_operational_sequencer_classes,
70
+ :extra_diff_formatter_classes
71
+
72
+ def possible_comparison_of(operation, next_operation)
73
+ if should_compare?(operation, next_operation)
74
+ sequence(operation, next_operation)
75
+ end
76
+ end
77
+
78
+ def should_compare?(operation, next_operation)
79
+ next_operation &&
80
+ operation.name == :delete &&
81
+ next_operation.name == :insert &&
82
+ next_operation.index == operation.index
83
+ end
84
+
85
+ def sequence(operation, next_operation)
86
+ OperationalSequencer.call(
87
+ expected: operation.value,
88
+ actual: next_operation.value,
89
+ extra_classes: extra_operational_sequencer_classes,
90
+ extra_diff_formatter_classes: extra_diff_formatter_classes,
91
+ )
92
+ rescue NoOperationalSequencerAvailableError
93
+ nil
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,82 @@
1
+ module SuperDiff
2
+ module OperationalSequencers
3
+ class Hash < Base
4
+ def self.applies_to?(value)
5
+ value.is_a?(::Hash)
6
+ end
7
+
8
+ protected
9
+
10
+ def unary_operations
11
+ all_keys.reduce([]) do |operations, key|
12
+ possibly_add_noop_to(operations, key)
13
+ possibly_add_delete_to(operations, key)
14
+ possibly_add_insert_to(operations, key)
15
+ operations
16
+ end
17
+ end
18
+
19
+ def operation_sequence_class
20
+ OperationSequences::Hash
21
+ end
22
+
23
+ private
24
+
25
+ def all_keys
26
+ (expected.keys | actual.keys)
27
+ end
28
+
29
+ def possibly_add_noop_to(operations, key)
30
+ if should_add_noop_operation?(key)
31
+ operations << Operations::UnaryOperation.new(
32
+ name: :noop,
33
+ collection: actual,
34
+ key: key,
35
+ index: all_keys.index(key),
36
+ value: actual[key],
37
+ )
38
+ end
39
+ end
40
+
41
+ def should_add_noop_operation?(key)
42
+ expected.include?(key) &&
43
+ actual.include?(key) &&
44
+ expected[key] == actual[key]
45
+ end
46
+
47
+ def possibly_add_delete_to(operations, key)
48
+ if should_add_delete_operation?(key)
49
+ operations << Operations::UnaryOperation.new(
50
+ name: :delete,
51
+ collection: expected,
52
+ key: key,
53
+ index: all_keys.index(key),
54
+ value: expected[key],
55
+ )
56
+ end
57
+ end
58
+
59
+ def should_add_delete_operation?(key)
60
+ expected.include?(key) &&
61
+ (!actual.include?(key) || expected[key] != actual[key])
62
+ end
63
+
64
+ def possibly_add_insert_to(operations, key)
65
+ if should_add_insert_operation?(key)
66
+ operations << Operations::UnaryOperation.new(
67
+ name: :insert,
68
+ collection: actual,
69
+ key: key,
70
+ index: all_keys.index(key),
71
+ value: actual[key],
72
+ )
73
+ end
74
+ end
75
+
76
+ def should_add_insert_operation?(key)
77
+ !expected.include?(key) ||
78
+ (actual.include?(key) && expected[key] != actual[key])
79
+ end
80
+ end
81
+ end
82
+ end