rspec-json_matchers 0.1.0.alpha.3 → 0.1.0.alpha.4

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +16 -0
  3. data/.rubocop.yml +477 -22
  4. data/.travis.yml +12 -3
  5. data/Appraisals +24 -4
  6. data/Gemfile +1 -0
  7. data/README.md +37 -20
  8. data/Rakefile +1 -0
  9. data/gemfiles/rspec_3_0.gemfile +2 -1
  10. data/gemfiles/rspec_3_1.gemfile +2 -1
  11. data/gemfiles/rspec_3_2.gemfile +2 -1
  12. data/gemfiles/rspec_3_3.gemfile +2 -1
  13. data/gemfiles/rspec_3_4.gemfile +7 -0
  14. data/gemfiles/rspec_3_5.gemfile +7 -0
  15. data/gemfiles/rspec_3_6.gemfile +7 -0
  16. data/lib/rspec/json_matchers/expectation.rb +6 -2
  17. data/lib/rspec/json_matchers/expectations/mixins/built_in.rb +84 -5
  18. data/lib/rspec/json_matchers/expectations/private.rb +4 -2
  19. data/lib/rspec/json_matchers/matchers.rb +0 -1
  20. data/lib/rspec/json_matchers/matchers/be_json_matcher.rb +0 -14
  21. data/lib/rspec/json_matchers/matchers/be_json_with_content_matcher.rb +144 -5
  22. data/lib/rspec/json_matchers/utils/key_path/extraction.rb +8 -8
  23. data/lib/rspec/json_matchers/utils/key_path/path.rb +1 -1
  24. data/lib/rspec/json_matchers/version.rb +1 -1
  25. data/rspec-json_matchers.gemspec +3 -2
  26. metadata +16 -13
  27. data/lib/rspec/json_matchers/comparers.rb +0 -13
  28. data/lib/rspec/json_matchers/comparers/abstract_comparer.rb +0 -323
  29. data/lib/rspec/json_matchers/comparers/comparison_result.rb +0 -22
  30. data/lib/rspec/json_matchers/comparers/exact_keys_comparer.rb +0 -27
  31. data/lib/rspec/json_matchers/comparers/include_keys_comparer.rb +0 -26
  32. data/lib/rspec/json_matchers/matchers/be_json_with_sizes_matcher.rb +0 -24
  33. data/lib/rspec/json_matchers/matchers/be_json_with_something_matcher.rb +0 -188
data/.travis.yml CHANGED
@@ -2,18 +2,27 @@
2
2
  # http://docs.travis-ci.com/user/workers/container-based-infrastructure/
3
3
  sudo: false
4
4
  language: ruby
5
+ before_install:
6
+ # This is required since 2.6.8 has issue when installing `rainbow`
7
+ # https://github.com/sickill/rainbow/issues/48
8
+ - gem update --system
9
+ - gem --version
5
10
  cache:
6
11
  - bundler
7
12
  rvm:
8
- - 2.0
9
- - 2.1
10
- - 2.2
13
+ - 2.1.10
14
+ - 2.2.6
15
+ - 2.3.3
16
+ - 2.4.0
11
17
  - ruby-head
12
18
  gemfile:
13
19
  - gemfiles/rspec_3_0.gemfile
14
20
  - gemfiles/rspec_3_1.gemfile
15
21
  - gemfiles/rspec_3_2.gemfile
16
22
  - gemfiles/rspec_3_3.gemfile
23
+ - gemfiles/rspec_3_4.gemfile
24
+ - gemfiles/rspec_3_5.gemfile
25
+ - gemfiles/rspec_3_6.gemfile
17
26
  matrix:
18
27
  fast_finish: true
19
28
  allow_failures:
data/Appraisals CHANGED
@@ -1,16 +1,36 @@
1
1
 
2
2
  appraise "rspec_3_0" do
3
- gem "rspec", "3.0"
3
+ gem "rspec", "~> 3.0.0"
4
+ # https://github.com/rspec/rspec-core/issues/2205
5
+ gem "rake", "< 12"
4
6
  end
5
7
 
6
8
  appraise "rspec_3_1" do
7
- gem "rspec", "3.1"
9
+ gem "rspec", "~> 3.1.0"
10
+ # https://github.com/rspec/rspec-core/issues/2205
11
+ gem "rake", "< 12"
8
12
  end
9
13
 
10
14
  appraise "rspec_3_2" do
11
- gem "rspec", "3.2"
15
+ gem "rspec", "~> 3.2.0"
16
+ # https://github.com/rspec/rspec-core/issues/2205
17
+ gem "rake", "< 12"
12
18
  end
13
19
 
14
20
  appraise "rspec_3_3" do
15
- gem "rspec", "3.3"
21
+ gem "rspec", "~> 3.3.0"
22
+ # https://github.com/rspec/rspec-core/issues/2205
23
+ gem "rake", "< 12"
24
+ end
25
+
26
+ appraise "rspec_3_4" do
27
+ gem "rspec", "~> 3.4.0"
28
+ end
29
+
30
+ appraise "rspec_3_5" do
31
+ gem "rspec", "~> 3.5.0"
32
+ end
33
+
34
+ appraise "rspec_3_6" do
35
+ gem "rspec", ">= 3.6.0.beta2", "< 3.7.0"
16
36
  end
data/Gemfile CHANGED
@@ -1,3 +1,4 @@
1
+ # rubocop:disable Style/MethodCallWithArgsParentheses
1
2
  source "https://rubygems.org"
2
3
 
3
4
  # Specify your gem's dependencies in rspec-json_matchers.gemspec
data/README.md CHANGED
@@ -114,14 +114,6 @@ specify { expect({a: 1, b: 2}.to_json).to be_json.with_content({a: 1}) } # => p
114
114
  specify { expect({a: 1}.to_json).to be_json.with_content({a: 1, b: 2}) } # => fail
115
115
  ```
116
116
 
117
- It's possible to make examples fail when the object represented by JSON string in `subject`
118
- contains more keys than that in expectation using `with_exact_keys`.
119
-
120
- ```ruby
121
- # The spec can be set to fail when actual has more keys than expected
122
- specify { expect({a: 1, b: 2}.to_json).to be_json.with_content({a: 1}).with_exact_keys } # => fail
123
- ```
124
-
125
117
  A "path" can also be specified for testing deeply nested data.
126
118
 
127
119
  ```ruby
@@ -476,32 +468,57 @@ specify do
476
468
  end # => fail
477
469
  ```
478
470
 
471
+ It's possible to make examples fail when the object represented by JSON string in `subject`
472
+ contains more keys than that in expectation using `HashWithContent` & `#with_exact_keys`.
473
+ `HashWithContent` is the expectation class that is automatically used when a `Hash` is passed.
474
+
475
+
476
+ ```ruby
477
+ # The spec can be set to fail when actual has more keys than expected
478
+ specify do
479
+ expect({a: 1, b: 2}.to_json).
480
+ to be_json.
481
+ with_content(
482
+ expectations::HashWithContent[{a: 1}].with_exact_keys
483
+ )
484
+ # => fail
485
+ end
486
+ ```
487
+
488
+ #### Custom/Complex Expectations NOT included on purpose
489
+
490
+ ##### Date
491
+ In [`airborne`](https://github.com/brooklynDev/airborne) you can validate the value as a "date" (and "time").
492
+ However "date/time" is not part of the JSON specification.
493
+ Some people use a string with a format specified in ISO to represent a time, but a [Unix time](https://en.wikipedia.org/wiki/Unix_time).
494
+ So this gem does not try to be "smart" to have a "generic" expectation for "date/time".
495
+ New expectations might be added in the future, to the core gem or a new extension gem, for common formats of "date" values.
496
+ There is no clear schedule for the addition yet, so you should try to add your own expectation class to suit your application.
497
+
479
498
 
480
499
  ### Matcher `be_json.with_sizes`
481
500
 
482
- This is a more convenient version of using `be_json.with_sizes` only with `ArrayWithSize`.
483
- Since you can just pass `Fixnum`, `Bignum` or `Range` without typing `ArrayWithSize`.
484
- Things like `with_exact_keys` & `at_path` can still be used.
501
+ Used to have in earlier alpha versions.
502
+ Indended to ease the migration from other gems but
503
+ it also makes the gem more difficult to maintain.
504
+ Removed in later alpha version(s).
505
+
506
+ Just use `ArrayWithSize`
507
+
485
508
 
486
509
  ```ruby
487
510
  specify do
488
511
  expect({a: [1]}.to_json).to be_json.
489
- with_sizes(a: 1)
512
+ with_content(a: ArrayWithSize[1])
490
513
  end # => pass
491
514
  specify do
492
515
  expect({a: [1]}.to_json).to be_json.
493
- with_sizes(a: (0..2))
516
+ with_content(a: ArrayWithSize[(0..2)])
494
517
  end # => pass
495
518
  specify do
496
519
  expect({a: [1]}.to_json).to be_json.
497
- with_sizes(a: 1.1)
520
+ with_content(a: ArrayWithSize[1.1])
498
521
  end # => error
499
-
500
- # But you can no longer pass multiple "expectations" for `AnyOf` behaviour
501
- specify do
502
- expect({a: [1]}.to_json).to be_json.
503
- with_content(a: [0, 1, 3])
504
- end # => fail, expects {a: [[], [1], [1, 2, 3]])
505
522
  ```
506
523
 
507
524
 
data/Rakefile CHANGED
@@ -1,3 +1,4 @@
1
+ # rubocop:disable Style/MethodCallWithArgsParentheses
1
2
  require "appraisal"
2
3
  require "rubygems"
3
4
  require "bundler/gem_tasks"
@@ -2,6 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rspec", "3.0"
5
+ gem "rspec", "~> 3.0.0"
6
+ gem "rake", "< 12"
6
7
 
7
8
  gemspec :path => "../"
@@ -2,6 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rspec", "3.1"
5
+ gem "rspec", "~> 3.1.0"
6
+ gem "rake", "< 12"
6
7
 
7
8
  gemspec :path => "../"
@@ -2,6 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rspec", "3.2"
5
+ gem "rspec", "~> 3.2.0"
6
+ gem "rake", "< 12"
6
7
 
7
8
  gemspec :path => "../"
@@ -2,6 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rspec", "3.3"
5
+ gem "rspec", "~> 3.3.0"
6
+ gem "rake", "< 12"
6
7
 
7
8
  gemspec :path => "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rspec", "~> 3.4.0"
6
+
7
+ gemspec :path => "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rspec", "~> 3.5.0"
6
+
7
+ gemspec :path => "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rspec", ">= 3.6.0.beta2", "< 3.7.0"
6
+
7
+ gemspec :path => "../"
@@ -91,10 +91,13 @@ module RSpec
91
91
  OBJECT_CLASS_TO_EXPECTATION_HASH = {
92
92
  Regexp => -> (obj) { Expectations::Private::MatchingRegexp[obj] },
93
93
  Range => -> (obj) { Expectations::Private::InRange[obj] },
94
+ Hash => -> (obj) { Expectations::Mixins::BuiltIn::HashWithContent[obj] },
94
95
  }.freeze
95
96
  private_constant :OBJECT_CLASS_TO_EXPECTATION_HASH
96
97
 
97
- attr_reader(*[:object])
98
+ attr_reader(
99
+ :object,
100
+ )
98
101
 
99
102
  def expectation_by_class
100
103
  if instance_variable_defined?(:@expectation_by_object_class)
@@ -104,7 +107,8 @@ module RSpec
104
107
  proc = OBJECT_CLASS_TO_EXPECTATION_HASH[object.class]
105
108
  return nil if proc.nil?
106
109
 
107
- proc.call(object)
110
+ # Assign (cache) and return
111
+ @expectation_by_object_class = proc.call(object)
108
112
  end
109
113
 
110
114
  def expectation_by_ancestors
@@ -1,3 +1,4 @@
1
+ require "set"
1
2
  require "abstract_class"
2
3
 
3
4
  require_relative "../core"
@@ -150,11 +151,22 @@ module RSpec
150
151
  class ArrayWithSize < AnyOf
151
152
  # `Fixnum` & `Bignum` will be returned instead of `Integer`
152
153
  # in `#class` for numbers
153
- EXPECTED_VALUE_CLASS_TO_EXPECTATION_CLASS_MAPPING = {
154
- Fixnum => -> (v) { Expectations::Private::Eq[v] },
155
- Bignum => -> (v) { Expectations::Private::Eq[v] },
156
- Range => -> (v) { Expectations::Private::InRange[v] },
157
- }.freeze
154
+ # But since 2.4.x it will be `Integer`
155
+ EXPECTED_VALUE_CLASS_TO_EXPECTATION_CLASS_MAPPING = begin
156
+ {
157
+ Range => -> (v) { Expectations::Private::InRange[v] },
158
+ Integer => -> (v) { Expectations::Private::Eq[v] },
159
+ }.tap do |result_hash|
160
+ # This fix is similar to
161
+ # https://github.com/rails/rails/pull/26732
162
+ next if 1.class == Integer
163
+
164
+ result_hash.merge!(
165
+ Fixnum => -> (v) { Expectations::Private::Eq[v] },
166
+ Bignum => -> (v) { Expectations::Private::Eq[v] },
167
+ )
168
+ end
169
+ end.freeze
158
170
  private_constant :EXPECTED_VALUE_CLASS_TO_EXPECTATION_CLASS_MAPPING
159
171
 
160
172
  class << self
@@ -182,6 +194,73 @@ module RSpec
182
194
  super(value.size)
183
195
  end
184
196
  end
197
+
198
+ # Takes a {Hash}
199
+ # Validates `value` to be {Hash} and
200
+ # contain expected keys & values
201
+ # Extra keys in `value` will not be treated as "unexpected"
202
+ # Unless {#with_exact_keys} is used
203
+ class HashWithContent < Expectations::Core::SingleValueCallableExpectation
204
+ EXPECTED_CLASS = ::Hash
205
+ private_constant :EXPECTED_CLASS
206
+
207
+ def expect?(value)
208
+ matches_expected_class?(value) &&
209
+ matches_content_expectations?(value) &&
210
+ matches_keys_exactly?(value)
211
+ end
212
+
213
+ # After calling this method
214
+ # Any extra key in `value` will be marked as "unexpected"
215
+ #
216
+ # By default the expectation won't care about extra keys in `value`
217
+ def with_exact_keys
218
+ @require_exact_key_matches = true
219
+ self
220
+ end
221
+
222
+ private
223
+
224
+ attr_reader :require_exact_key_matches
225
+ alias_method(
226
+ :require_exact_key_matches?,
227
+ :require_exact_key_matches,
228
+ )
229
+ attr_reader :expected_value
230
+
231
+ def matches_expected_class?(value)
232
+ value.is_a?(::Hash)
233
+ end
234
+
235
+ def matches_keys_exactly?(actual_value)
236
+ return true unless require_exact_key_matches?
237
+
238
+ ::Set.new(expected_value.keys.map(&:to_s)) ==
239
+ ::Set.new(actual_value.keys.map(&:to_s))
240
+ end
241
+
242
+ def matches_content_expectations?(actual_value)
243
+ expected_value.each_pair.all? do |exp_key, exp_value|
244
+ unless actual_value.key?(exp_key) ||
245
+ actual_value.key?(exp_key.to_s)
246
+ return false
247
+ end
248
+
249
+ value_from_actual = actual_value.fetch(exp_key) do
250
+ actual_value.fetch(exp_key.to_s)
251
+ end
252
+ Expectation.build(exp_value).expect?(value_from_actual)
253
+ end
254
+ end
255
+
256
+ def initialize(value)
257
+ unless value.is_a?(::Hash)
258
+ fail ArgumentError, "a #{EXPECTED_CLASS} is required"
259
+ end
260
+
261
+ @expected_value = value
262
+ end
263
+ end
185
264
  end
186
265
  end
187
266
  end
@@ -135,8 +135,10 @@ module RSpec
135
135
 
136
136
  def initialize(value)
137
137
  unless value.respond_to?(:call)
138
- fail ArgumentError,
139
- "an object which respond to `:call` is required"
138
+ fail(
139
+ ArgumentError,
140
+ "an object which respond to `:call` is required",
141
+ )
140
142
  end
141
143
  @callable = value
142
144
  end
@@ -1,6 +1,5 @@
1
1
  require_relative "matchers/be_json_matcher"
2
2
  require_relative "matchers/be_json_with_content_matcher"
3
- require_relative "matchers/be_json_with_sizes_matcher"
4
3
 
5
4
  module RSpec
6
5
  module JsonMatchers
@@ -45,19 +45,6 @@ module RSpec
45
45
  BeJsonWithContentMatcher.new(expected)
46
46
  end
47
47
 
48
- # @api
49
- #
50
- # Get a matcher that try to match the content of actual
51
- # with nested expectations about array sizes
52
- #
53
- # @param expected [Hash, Array, Object]
54
- # the expectation object
55
- #
56
- # @return [BeJsonWithSizesMatcher] a matcher object
57
- def with_sizes(expected)
58
- BeJsonWithSizesMatcher.new(expected)
59
- end
60
-
61
48
  # Expectation description in spec result summary
62
49
  #
63
50
  # @return [String]
@@ -95,4 +82,3 @@ end
95
82
  # since the classes are only required
96
83
  # on runtime but not load time
97
84
  require_relative "be_json_with_content_matcher"
98
- require_relative "be_json_with_sizes_matcher"
@@ -1,19 +1,158 @@
1
1
  require "json"
2
2
  require "awesome_print"
3
3
 
4
- require_relative "be_json_with_something_matcher"
5
- require_relative "../comparers"
4
+ require_relative "be_json_matcher"
6
5
  require_relative "../utils"
7
6
 
8
7
  module RSpec
9
8
  module JsonMatchers
10
9
  module Matchers
11
10
  # @api private
12
- class BeJsonWithContentMatcher < BeJsonWithSomethingMatcher
11
+ # @abstract
12
+ #
13
+ # Parent of matcher classes that requires {#at_path} & {#with_exact_keys}
14
+ # This is not merged with {BeJsonMatcher}
15
+ # since it should be able to be used alone
16
+ class BeJsonWithContentMatcher < BeJsonMatcher
17
+ attr_reader(
18
+ :path,
19
+ )
20
+
21
+ def initialize(expected)
22
+ @expected = expected
23
+ @path = JsonMatchers::Utils::KeyPath::Path.new("")
24
+ end
25
+
26
+ def matches?(*_args)
27
+ super && has_valid_path? && expected_and_actual_matched?
28
+ end
29
+
30
+ def does_not_match?(*args)
31
+ !matches?(*args) && has_valid_path?
32
+ end
33
+
34
+ # Override {BeJsonMatcher#actual}
35
+ # It return actual object extracted by {#path}
36
+ # And also detect & set state for path error
37
+ # (either it's invalid or fails to extract)
38
+ #
39
+ # @return [Object]
40
+ # extracted object but could be object in the middle
41
+ # when extraction failed
42
+ def actual
43
+ result = path.extract(super)
44
+ has_path_error! if result.failed?
45
+ result.object
46
+ end
47
+
48
+ # Sets the path to be used for object,
49
+ # to avoid passing a deep nested
50
+ # {Hash} or {Array} as expectation
51
+ # Defaults to "" (if this is not called)
52
+ #
53
+ # The path uses period (".") as separator for parts
54
+ # Also period cannot be used as path name as a side-effect
55
+ #
56
+ # This does NOT raise error if the path is invalid
57
+ # (like having 2 periods, 1 period at the start/end of string)
58
+ # But it will fail the example with both `should` & `should_not`
59
+ #
60
+ # @param path [String] the "path" to be used
61
+ #
62
+ # @return [BeJsonWithSomethingMatcher] the match itself
63
+ #
64
+ # @throw [TypeError] when input is not a string
65
+ def at_path(path)
66
+ @path = JsonMatchers::Utils::KeyPath::Path.new(path)
67
+ self
68
+ end
69
+
70
+ # Overrides {BeJsonMatcher#failure_message}
71
+ def failure_message
72
+ return super if has_parser_error?
73
+ failure_message_for(true)
74
+ end
75
+
76
+ # Overrides {BeJsonMatcher#failure_message_when_negated}
77
+ def failure_message_when_negated
78
+ return super if has_parser_error?
79
+ failure_message_for(false)
80
+ end
81
+
13
82
  private
14
83
 
15
- def value_matching_proc
16
- -> (expected, actual) { Expectation.build(expected).expect?(actual) }
84
+ attr_reader(
85
+ :expected,
86
+ )
87
+
88
+ def failure_message_for(should_match)
89
+ return invalid_path_message unless has_valid_path?
90
+ return path_error_message if has_path_error?
91
+
92
+ inspection_messages(should_match)
93
+ end
94
+
95
+ # @return [Bool] Whether `expected` & `parsed` are "equal"
96
+ def expected_and_actual_matched?
97
+ extracted_actual = actual
98
+ return false if has_path_error?
99
+
100
+ expectation = Expectation.build(expected)
101
+ expectation.expect?(extracted_actual)
102
+ end
103
+
104
+ def reasons
105
+ @reasons ||= []
106
+ end
107
+
108
+ def inspection_messages(should_match)
109
+ [
110
+ ["expected", inspection_messages_prefix(should_match), "to match:"].
111
+ compact.map(&:strip).join(" "),
112
+ expected.awesome_inspect(indent: -2),
113
+ "",
114
+ "actual:",
115
+ actual.awesome_inspect(indent: -2),
116
+ "",
117
+ inspection_message_for_reason,
118
+ ].join("\n")
119
+ end
120
+
121
+ def inspection_messages_prefix(should_match)
122
+ should_match ? nil : "not"
123
+ end
124
+
125
+ def inspection_message_for_reason
126
+ reasons.any? ? "reason/path: #{reasons.reverse.join('.')}" : nil
127
+ end
128
+
129
+ def original_actual
130
+ @actual
131
+ end
132
+
133
+ def has_path_error?
134
+ @has_path_error
135
+ end
136
+
137
+ def has_path_error!
138
+ @has_path_error = true
139
+ end
140
+
141
+ # For both positive and negative
142
+ def path_error_message
143
+ [
144
+ %(path "#{path}" does not exists in actual: ),
145
+ original_actual.awesome_inspect(indent: -2),
146
+ ].join("\n")
147
+ end
148
+
149
+ def has_valid_path?
150
+ (path.nil? || path.valid?)
151
+ end
152
+
153
+ # For both positive and negative
154
+ def invalid_path_message
155
+ %(path "#{path}" is invalid)
17
156
  end
18
157
  end
19
158
  end