rspec-json_matchers 0.1.0.alpha.1 → 0.1.0.alpha.2

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.
@@ -1,6 +1,6 @@
1
- require_relative "matchers/be_json_matcher"
2
- require_relative "matchers/be_json_with_content_matcher"
3
- require_relative "matchers/be_json_with_sizes_matcher"
1
+ require_relative "matchers/be_json_matcher"
2
+ require_relative "matchers/be_json_with_content_matcher"
3
+ require_relative "matchers/be_json_with_sizes_matcher"
4
4
 
5
5
  module RSpec
6
6
  module JsonMatchers
@@ -2,6 +2,8 @@ require "json"
2
2
 
3
3
  module RSpec
4
4
  module JsonMatchers
5
+ # Mixin Module to be included into RSpec
6
+ # Other files will define the same module and add methods to this module
5
7
  module Matchers
6
8
  # @api
7
9
  #
@@ -13,6 +15,8 @@ module RSpec
13
15
  end
14
16
 
15
17
  # @api private
18
+ #
19
+ # The implementation for {Matchers#be_json}
16
20
  class BeJsonMatcher
17
21
  attr_reader :actual
18
22
 
@@ -61,32 +65,38 @@ module RSpec
61
65
  "be a valid JSON string"
62
66
  end
63
67
 
64
- # Failure message displayed when a positive example failed (e.g. using `should`)
68
+ # Failure message displayed when a positive example failed
69
+ # (e.g. using `should`)
65
70
  #
66
71
  # @return [String]
67
72
  def failure_message_for_positive
68
73
  "expected value to be parsed as JSON, but failed"
69
74
  end
70
- alias :failure_message :failure_message_for_positive
75
+ alias_method :failure_message,
76
+ :failure_message_for_positive
71
77
 
72
- # Failure message displayed when a negative example failed (e.g. using `should_not`)
78
+ # Failure message displayed when a negative example failed
79
+ # (e.g. using `should_not`)
73
80
  #
74
81
  # @return [String]
75
82
  def failure_message_for_negative
76
83
  "expected value not to be parsed as JSON, but succeeded"
77
84
  end
78
- alias :failure_message_when_negated :failure_message_for_negative
85
+ alias_method :failure_message_when_negated,
86
+ :failure_message_for_negative
79
87
 
80
88
  private
81
89
 
82
90
  def has_parser_error?
83
- !!@has_parser_error
91
+ @has_parser_error
84
92
  end
85
93
  end
86
94
  end
87
95
  end
88
96
  end
89
97
 
90
- # These files are required since the classes are only required on runtime, not load time
98
+ # These files are required here
99
+ # since the classes are only required
100
+ # on runtime but not load time
91
101
  require_relative "be_json_with_content_matcher"
92
102
  require_relative "be_json_with_sizes_matcher"
@@ -13,7 +13,10 @@ module RSpec
13
13
  private
14
14
 
15
15
  def value_matching_proc
16
- -> (expected, actual) { Expectations::Private::ArrayWithSize[expected].expect?(actual) }
16
+ lambda do |expected, actual|
17
+ Expectations::Private::ArrayWithSize[expected].
18
+ expect?(actual)
19
+ end
17
20
  end
18
21
  end
19
22
  end
@@ -12,15 +12,12 @@ module RSpec
12
12
  # @abstract
13
13
  #
14
14
  # Parent of matcher classes that requires {#at_path} & {#with_exact_keys}
15
- # This is not merged with {BeJsonMatcher} since it should be able to be used alone
15
+ # This is not merged with {BeJsonMatcher}
16
+ # since it should be able to be used alone
16
17
  class BeJsonWithSomethingMatcher < BeJsonMatcher
17
18
  extend AbstractClass
18
19
 
19
- attr_reader *[
20
- :expected,
21
- :path,
22
- :with_exact_keys,
23
- ]
20
+ attr_reader(*[:expected, :path, :with_exact_keys])
24
21
  alias_method :with_exact_keys?, :with_exact_keys
25
22
 
26
23
  def initialize(expected)
@@ -37,25 +34,28 @@ module RSpec
37
34
  !matches?(*args) && has_valid_path?
38
35
  end
39
36
 
40
- def description
41
- super
42
- end
43
-
44
37
  # Override {BeJsonMatcher#actual}
45
38
  # It return actual object extracted by {#path}
46
- # And also detect & set state for path error (either it's invalid or fails to extract)
39
+ # And also detect & set state for path error
40
+ # (either it's invalid or fails to extract)
47
41
  #
48
- # @return [Object] extracted object but could be object in the middle when extraction failed
42
+ # @return [Object]
43
+ # extracted object but could be object in the middle
44
+ # when extraction failed
49
45
  def actual
50
46
  result = path.extract(super)
51
47
  has_path_error! if result.failed?
52
48
  result.object
53
49
  end
54
50
 
55
- # Sets the path to be used for object, to avoid passing a deep nested {Hash} or {Array} as expectation
51
+ # Sets the path to be used for object,
52
+ # to avoid passing a deep nested
53
+ # {Hash} or {Array} as expectation
56
54
  # Defaults to "" (if this is not called)
55
+ #
57
56
  # The path uses period (".") as separator for parts
58
- # (also period cannot be used as path name as a side-effect, but who does?)
57
+ # Also period cannot be used as path name as a side-effect
58
+ #
59
59
  # This does NOT raise error if the path is invalid
60
60
  # (like having 2 periods, 1 period at the start/end of string)
61
61
  # But it will fail the example with both `should` & `should_not`
@@ -78,39 +78,43 @@ module RSpec
78
78
  # makes the matcher to pass the example
79
79
  # when actual has more elements than expected and expectation passes
80
80
  #
81
- # @param exactly [Boolean] whether the matcher should match keys in actual & expected exactly
81
+ # @param exactly [Boolean]
82
+ # whether the matcher should match keys in actual & expected exactly
82
83
  #
83
84
  # @return (see #at_path)
84
85
  def with_exact_keys(exactly = true)
85
- @with_exact_keys = !!exactly
86
+ @with_exact_keys = exactly
86
87
  self
87
88
  end
88
89
 
90
+ # Overrides {BeJsonMatcher#failure_message_for_positive}
89
91
  def failure_message_for_positive
90
92
  return super if has_parser_error?
91
- return invalid_path_message unless has_valid_path?
92
- return path_error_message if has_path_error?
93
-
94
- inspection_messages(true)
93
+ failure_message_for(true)
95
94
  end
96
- alias :failure_message :failure_message_for_positive
97
95
 
96
+ # Overrides {BeJsonMatcher#failure_message_for_negative}
98
97
  def failure_message_for_negative
99
98
  return super if has_parser_error?
99
+ failure_message_for(false)
100
+ end
101
+
102
+ private
103
+
104
+ def failure_message_for(should_match)
100
105
  return invalid_path_message unless has_valid_path?
101
106
  return path_error_message if has_path_error?
102
107
 
103
- inspection_messages(false)
108
+ inspection_messages(should_match)
104
109
  end
105
- alias :failure_message_when_negated :failure_message_for_negative
106
-
107
- private
108
110
 
109
111
  # @return [Bool] Whether `expected` & `parsed` are "equal"
110
112
  def expected_and_actual_matched?
111
113
  extracted_actual = actual
112
114
  return false if has_path_error?
113
- result = comparer_klass.new(extracted_actual, expected, reasons, value_matching_proc).compare
115
+ result = comparer_klass.
116
+ new(extracted_actual, expected, reasons, value_matching_proc).
117
+ compare
114
118
 
115
119
  result.matched?.tap do |matched|
116
120
  @reasons = result.reasons unless matched
@@ -122,18 +126,24 @@ module RSpec
122
126
  end
123
127
 
124
128
  def inspection_messages(should_match)
125
- prefix = !!should_match ? nil : "not"
126
-
127
- messages = [
128
- ["expected", prefix, "to match:"].compact.map(&:strip).join(" "),
129
+ [
130
+ ["expected", inspection_messages_prefix(should_match), "to match:"].
131
+ compact.map(&:strip).join(" "),
129
132
  expected.awesome_inspect(indent: -2),
130
133
  "",
131
134
  "actual:",
132
135
  actual.awesome_inspect(indent: -2),
133
136
  "",
134
- ]
135
- messages.push "reason/path: #{reasons.reverse.join(".")}" unless reasons.empty?
136
- messages.join("\n")
137
+ inspection_message_for_reason,
138
+ ].join("\n")
139
+ end
140
+
141
+ def inspection_messages_prefix(should_match)
142
+ should_match ? nil : "not"
143
+ end
144
+
145
+ def inspection_message_for_reason
146
+ reasons.any? ? "reason/path: #{reasons.reverse.join('.')}" : nil
137
147
  end
138
148
 
139
149
  def original_actual
@@ -141,7 +151,7 @@ module RSpec
141
151
  end
142
152
 
143
153
  def has_path_error?
144
- !!@has_path_error
154
+ @has_path_error
145
155
  end
146
156
 
147
157
  def has_path_error!
@@ -150,8 +160,8 @@ module RSpec
150
160
 
151
161
  # For both positive and negative
152
162
  def path_error_message
153
- %Q|path "#{path}" does not exists in actual: |
154
163
  [
164
+ %(path "#{path}" does not exists in actual: ),
155
165
  original_actual.awesome_inspect(indent: -2),
156
166
  ].join("\n")
157
167
  end
@@ -162,11 +172,15 @@ module RSpec
162
172
 
163
173
  # For both positive and negative
164
174
  def invalid_path_message
165
- %Q|path "#{path}" is invalid|
175
+ %(path "#{path}" is invalid)
166
176
  end
167
177
 
168
178
  def comparer_klass
169
- with_exact_keys? ? Comparers::ExactKeysComparer : Comparers::IncludeKeysComparer
179
+ if with_exact_keys?
180
+ Comparers::ExactKeysComparer
181
+ else
182
+ Comparers::IncludeKeysComparer
183
+ end
170
184
  end
171
185
  end
172
186
  end
@@ -25,9 +25,10 @@ module RSpec
25
25
  end
26
26
 
27
27
  def extract
28
- COLLECTION_TYPE_TO_VALUE_EXTRACTION_PROC_MAP.fetch(collection.class) do
29
- raise TypeError
30
- end.call(collection)
28
+ COLLECTION_TYPE_TO_VALUE_EXTRACTION_PROC_MAP.
29
+ fetch(collection.class) do
30
+ fail TypeError
31
+ end.call(collection)
31
32
  end
32
33
  end
33
34
  end
@@ -0,0 +1,149 @@
1
+ require_relative "path"
2
+
3
+ module RSpec
4
+ module JsonMatchers
5
+ module Utils
6
+ # @api private
7
+ module KeyPath
8
+ # Represents an extractor that performs the extraction
9
+ # with a {#path} & {#object}
10
+ class Extraction
11
+ # Create a new extractor with the "source object"
12
+ # and the path to be used for extracting our target object
13
+ #
14
+ # @param object [Object]
15
+ # The source object to extract our target object from
16
+ # @param path [String, Path]
17
+ # the path of target object
18
+ # Will convert into {Path}
19
+ #
20
+ # @see JsonMatchers::Matchers::BeJsonWithSomethingMatcher#at_path
21
+ def initialize(object, path)
22
+ @object = object
23
+ @path = KeyPath::Path.new(path)
24
+
25
+ @failed = false
26
+ end
27
+
28
+ # Actually perform the extraction and return the result
29
+ # Since the object could be falsy,
30
+ # an object of custom class is returned instead of the object only
31
+ #
32
+ # Assume the path to be valid
33
+ #
34
+ # @return (see Path#extract)
35
+ def extract
36
+ path.each_path_part do |path_part|
37
+ result = extract_object_with_path_part(path_part)
38
+ return Result.new(object, false) if result.failed?
39
+
40
+ self.object = result.object
41
+ end
42
+
43
+ Result.new(object, true)
44
+ end
45
+
46
+ private
47
+
48
+ attr_accessor(*[
49
+ :object,
50
+ ])
51
+ attr_reader(*[
52
+ :path,
53
+ :failed,
54
+ ])
55
+ alias_method :failed?, :failed
56
+
57
+ # @param path_part [String]
58
+ # One part of {#path}
59
+ def extract_object_with_path_part(path_part)
60
+ ExtractionWithOnePathPart.new(object, path_part).extract
61
+ end
62
+
63
+ def fail!
64
+ @failed = true
65
+ self
66
+ end
67
+
68
+ # @api private
69
+ #
70
+ # Internal implementation for an extraction operation with
71
+ # only one part of the path
72
+ class ExtractionWithOnePathPart
73
+ # Create a new extractor with the "source object"
74
+ # and the path to be used for extracting our target object
75
+ #
76
+ # @param object [Object]
77
+ # The source object to extract our target object from
78
+ # @param path_part [String]
79
+ # a part of {KeyPath::Path} of target object
80
+ def initialize(object, path_part)
81
+ @object = object
82
+ @path_part = path_part
83
+ end
84
+
85
+ # Actually perform the extraction and return the result
86
+ # Since the object could be falsy,
87
+ # an object of custom class is returned instead of the object only
88
+ #
89
+ # @return (see Extraction#extract)
90
+ def extract
91
+ case object
92
+ when Hash
93
+ extract_object_from_hash
94
+ when Array
95
+ extract_object_from_array
96
+ else
97
+ # Disallow non JSON collection type
98
+ Result.new(object, false)
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def extract_object_from_hash
105
+ # Allow nil as object, but disallow key to be absent
106
+ return Result.new(object, false) unless object.key?(path_part)
107
+
108
+ Result.new(object.fetch(path_part), true)
109
+ end
110
+
111
+ def extract_object_from_array
112
+ index = path_part.to_i
113
+ # Disallow index to be out of range
114
+ # Disallow negative number as index
115
+ unless (path_part =~ /\A\d+\z/) && index < object.size
116
+ return Result.new(object, false)
117
+ end
118
+
119
+ Result.new(object.slice(index), true)
120
+ end
121
+
122
+ attr_accessor(*[
123
+ :object,
124
+ ])
125
+ attr_reader(*[
126
+ :path_part,
127
+ ])
128
+ end
129
+
130
+ # @api private
131
+ #
132
+ # Only as a value object for internal communication
133
+ class Result
134
+ attr_reader :object, :successful
135
+
136
+ def initialize(object, successful)
137
+ @object = object
138
+ @successful = successful
139
+ end
140
+
141
+ def failed?
142
+ !successful
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -1,31 +1,37 @@
1
- require_relative "extraction_result"
2
- require_relative "extractor"
1
+ require_relative "extraction"
3
2
 
4
3
  module RSpec
5
4
  module JsonMatchers
6
5
  module Utils
7
6
  # @api private
8
7
  module KeyPath
8
+ # Represents a path pointing to an element
9
+ # of a {Hash} or {Array}
9
10
  class Path
10
11
  # The "path part" separator
11
12
  # Period is used since it's the least used char as part of a key name
12
13
  # (it can never by used for index anyway)
13
- # As a side effect this char CANNOT be used, escaping is not planned to be added
14
+ # As a side effect this char CANNOT be used,
15
+ # escaping is not planned to be added
14
16
  PATH_PART_SPLITTER = ".".freeze
15
17
  # The regular expression for checking "invalid" path
16
18
  # The separator should NOT at the start/end of the string,
17
19
  # or repeating itself without other chars in between
18
- INVALID_PATH_REGEX = %r_(
20
+ INVALID_PATH_REGEX = /
21
+ (
19
22
  ^#{Regexp.escape(PATH_PART_SPLITTER)}
20
23
  |
21
24
  #{Regexp.escape(PATH_PART_SPLITTER)}{2,}
22
25
  |
23
26
  #{Regexp.escape(PATH_PART_SPLITTER)}$
24
- )_x.freeze
27
+ )
28
+ /x.freeze
25
29
 
26
30
  # Creates a {Path}
27
- # with a {String} (mainly from external) (will store it internally)
28
- # or a {Path} (mainly from internal) (will get and assign the string path it internally)
31
+ # with a {String} (mainly from external)
32
+ # (will store it internally)
33
+ # or a {Path} (mainly from internal)
34
+ # (will get and assign the string path it internally)
29
35
  #
30
36
  # @note
31
37
  # It does not copy the string object since there is not need
@@ -43,7 +49,7 @@ module RSpec
43
49
  when String
44
50
  @string_path = path
45
51
  else
46
- raise TypeError, "Only String and Path is expected"
52
+ fail TypeError, "Only String and Path is expected"
47
53
  end
48
54
  end
49
55
 
@@ -69,11 +75,11 @@ module RSpec
69
75
  # @param object [Object]
70
76
  # The "source object" to extract our "target object" from
71
77
  #
72
- # @return [ExtractionResult] The result of object extraction
78
+ # @return [Extraction::Result] The result of object extraction
73
79
  def extract(object)
74
- return ExtractionResult.new(object, true) if empty?
75
- return ExtractionResult.new(object, false) if invalid?
76
- Extractor.new(object, self).extract
80
+ return Extraction::Result.new(object, true) if empty?
81
+ return Extraction::Result.new(object, false) if invalid?
82
+ Extraction.new(object, self).extract
77
83
  end
78
84
 
79
85
  protected