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

Sign up to get free protection for your applications and to get access to all the features.
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