rspec-json_api 1.5.0 → 2.0.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.
@@ -24,6 +24,11 @@ module RSpec
24
24
  # @return [Boolean] true if the actual JSON matches the expected schema, false otherwise.
25
25
  def matches?(actual)
26
26
  @diff = nil
27
+ @actual = actual
28
+ @type_error = !actual.is_a?(String)
29
+
30
+ return false if @type_error
31
+
27
32
  @actual = JSON.parse(actual, symbolize_names: true)
28
33
 
29
34
  RSpec::JsonApi::SchemaMatch.match(@actual, expected)
@@ -32,9 +37,23 @@ module RSpec
32
37
  false
33
38
  end
34
39
 
40
+ # A non-String actual is a mistake in the spec rather than a fact about the
41
+ # response, so the negated form has to fail rather than pass by default.
42
+ # @param actual [String] The JSON string to test against the expected schema.
43
+ # @return [Boolean] true if the actual JSON does not match the expected schema.
44
+ def does_not_match?(actual)
45
+ !matches?(actual) && !@type_error
46
+ end
47
+
48
+ def description
49
+ "match JSON schema"
50
+ end
51
+
35
52
  # Provides a failure message for when the JSON data does not match the expected schema.
36
53
  # @return [String] A descriptive message detailing the mismatch between expected and actual JSON.
37
54
  def failure_message
55
+ return type_error_message if @type_error
56
+
38
57
  <<~MSG
39
58
  expected: #{expected}
40
59
  got: #{actual}
@@ -46,8 +65,10 @@ module RSpec
46
65
 
47
66
  # Provides a failure message for when the JSON data matches the expected schema, but it was expected not to.
48
67
  # This is used in negative matchers.
49
- # @return [self] Returns itself, but typically this method should be implemented to return a descriptive message
68
+ # @return [String] A descriptive message indicating the JSON was expected not to match the schema.
50
69
  def failure_message_when_negated
70
+ return type_error_message if @type_error
71
+
51
72
  "expected the JSON data not to match the provided schema, but it did."
52
73
  end
53
74
 
@@ -58,6 +79,10 @@ module RSpec
58
79
  def diff
59
80
  @diff ||= Diffy::Diff.new(expected, actual, context: 5)
60
81
  end
82
+
83
+ def type_error_message
84
+ "expected a JSON String to match against the schema, got #{actual.class}"
85
+ end
61
86
  end
62
87
  end
63
88
  end
@@ -10,30 +10,28 @@ module RSpec
10
10
  module SchemaMatch
11
11
  module_function
12
12
 
13
- # Top-level comparison. Applies the shape guards (class equality and, for
14
- # objects, key-set equality) before recursing.
13
+ # Top-level comparison. Applies shape guards to objects and collections,
14
+ # then uses the same value dispatch as nested schema values.
15
15
  def match(actual, expected)
16
- return false unless actual.instance_of?(expected.class)
17
-
18
16
  case expected
19
17
  when Array
20
18
  compare_array(actual, expected)
21
19
  when Hash
20
+ return false unless actual.is_a?(Hash)
22
21
  return false unless same_key_structure?(actual, expected)
23
22
 
24
23
  compare(actual, expected)
25
24
  else
26
- compare_simple_value(actual, expected)
25
+ compare_values(actual, expected)
27
26
  end
28
27
  end
29
28
 
30
29
  def same_key_structure?(actual, expected)
31
- Traversal.deep_sort(Traversal.deep_keys(actual)) ==
32
- Traversal.deep_sort(Traversal.deep_keys(expected))
30
+ Traversal.same_key_structure?(actual, expected)
33
31
  end
34
32
 
35
33
  def compare(actual, expected)
36
- return false if actual.blank? && expected.present?
34
+ return false unless actual.is_a?(Hash)
37
35
 
38
36
  keys = Traversal.deep_key_paths(expected) | Traversal.deep_key_paths(actual)
39
37
 
@@ -76,14 +74,40 @@ module RSpec
76
74
  end
77
75
 
78
76
  def compare_regexp(actual_value, expected_value)
79
- expected_value.match?(actual_value.to_s)
77
+ actual_value.is_a?(String) && expected_value.match?(actual_value)
80
78
  end
81
79
 
80
+ # A schema Proc describes the constraints for a value; it is called without
81
+ # arguments and must return the option Hash. A Proc that expects the value
82
+ # as an argument is a common misreading of the DSL, and calling it here
83
+ # would raise a bare "wrong number of arguments" from deep in the matcher.
82
84
  def compare_proc(actual_value, expected_value)
83
- Constraints.match(actual_value, expected_value.call)
85
+ if declares_value_parameter?(expected_value)
86
+ raise ArgumentError,
87
+ "schema Proc must take no arguments; " \
88
+ "write -> { { lambda: ->(value) { ... } } } to test the value itself"
89
+ end
90
+
91
+ options = expected_value.call
92
+ raise ArgumentError, "schema Proc must return an options Hash, got #{options.class}" unless options.is_a?(Hash)
93
+
94
+ Constraints.match(actual_value, options)
84
95
  end
85
96
 
97
+ # A non-lambda Proc reports its block parameters as optional, so
98
+ # `proc { |value| ... }` has to be caught on the parameter list rather
99
+ # than on arity. A bare splat states no expectation and is left alone.
100
+ def declares_value_parameter?(callable)
101
+ callable.parameters.any? { |type, _name| %i[req opt keyreq].include?(type) }
102
+ end
103
+
104
+ # A list schema only ever matches an actual Array. Without this guard the
105
+ # branches below call Array methods on whatever the response contained, so
106
+ # a null or a scalar where a list was expected raised NoMethodError
107
+ # instead of failing the match.
86
108
  def compare_array(actual_value, expected_value)
109
+ return false unless actual_value.is_a?(Array)
110
+
87
111
  if simple_type?(expected_value)
88
112
  compare_typed_array(actual_value, expected_value)
89
113
  elsif interface?(expected_value)
@@ -112,10 +136,10 @@ module RSpec
112
136
 
113
137
  # Any other array => element-by-element match, sizes must be equal.
114
138
  def compare_exact_array(actual_value, expected_value)
115
- return false if actual_value&.size != expected_value&.size
139
+ return false if actual_value.size != expected_value.size
116
140
 
117
141
  expected_value.each_with_index.all? do |elem, index|
118
- elem.is_a?(Hash) ? compare(actual_value[index], elem) : compare_simple_value(actual_value[index], elem)
142
+ elem.is_a?(Hash) ? match(actual_value[index], elem) : compare_values(actual_value[index], elem)
119
143
  end
120
144
  end
121
145
 
@@ -44,6 +44,22 @@ module RSpec
44
44
  .map { |element| element.is_a?(Array) ? deep_sort(element) : element }
45
45
  .sort_by { |element| element.is_a?(Array) ? element.first.to_s : element.to_s }
46
46
  end
47
+
48
+ # Whether two hashes have the same keys under the same parent objects.
49
+ def same_key_structure?(actual, expected)
50
+ same_keys?(actual, expected) && actual.all? do |key, actual_value|
51
+ same_nested_key_structure?(actual_value, expected[key])
52
+ end
53
+ end
54
+
55
+ def same_keys?(actual, expected)
56
+ actual.size == expected.size && actual.each_key.all? { |key| expected.key?(key) }
57
+ end
58
+
59
+ def same_nested_key_structure?(actual, expected)
60
+ nested = actual.is_a?(Hash) || expected.is_a?(Hash)
61
+ !nested || (actual.is_a?(Hash) && expected.is_a?(Hash) && same_key_structure?(actual, expected))
62
+ end
47
63
  end
48
64
  end
49
65
  end
@@ -3,7 +3,11 @@
3
3
  module RSpec
4
4
  module JsonApi
5
5
  module Types
6
- URI = URI::DEFAULT_PARSER.make_regexp
6
+ # URI::DEFAULT_PARSER.make_regexp is unanchored, and comparison uses
7
+ # Regexp#match?, so on its own it accepts any string that merely contains
8
+ # a URI ("see https://example.com for details"). \A...\z holds the whole
9
+ # value to the pattern, the same way EMAIL and UUID already are anchored.
10
+ URI = /\A#{::URI::DEFAULT_PARSER.make_regexp}\z/
7
11
  end
8
12
  end
9
13
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module JsonApi
5
- VERSION = "1.5.0"
5
+ VERSION = "2.0.0"
6
6
  end
7
7
  end
@@ -4,10 +4,11 @@
4
4
  require "json"
5
5
  require "uri"
6
6
  require "diffy"
7
- require "active_support/core_ext/object/blank"
7
+ require "rspec/expectations"
8
8
 
9
9
  # Load the json_api parts
10
10
  require "rspec/json_api/version"
11
+ require "rspec/json_api/blank"
11
12
  require "rspec/json_api/traversal"
12
13
  require "rspec/json_api/constraints"
13
14
  require "rspec/json_api/schema_match"
metadata CHANGED
@@ -1,28 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-json_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Gajowiak
8
- bindir: exe
8
+ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: activesupport
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - ">="
17
- - !ruby/object:Gem::Version
18
- version: 6.1.4.1
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - ">="
24
- - !ruby/object:Gem::Version
25
- version: 6.1.4.1
26
12
  - !ruby/object:Gem::Dependency
27
13
  name: diffy
28
14
  requirement: !ruby/object:Gem::Requirement
@@ -38,67 +24,38 @@ dependencies:
38
24
  - !ruby/object:Gem::Version
39
25
  version: 3.4.2
40
26
  - !ruby/object:Gem::Dependency
41
- name: railties
42
- requirement: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: 6.1.4.1
47
- type: :runtime
48
- prerelease: false
49
- version_requirements: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - ">="
52
- - !ruby/object:Gem::Version
53
- version: 6.1.4.1
54
- - !ruby/object:Gem::Dependency
55
- name: rspec-rails
27
+ name: rspec-expectations
56
28
  requirement: !ruby/object:Gem::Requirement
57
29
  requirements:
58
- - - ">="
30
+ - - "~>"
59
31
  - !ruby/object:Gem::Version
60
- version: 5.0.2
32
+ version: '3.0'
61
33
  type: :runtime
62
34
  prerelease: false
63
35
  version_requirements: !ruby/object:Gem::Requirement
64
36
  requirements:
65
- - - ">="
37
+ - - "~>"
66
38
  - !ruby/object:Gem::Version
67
- version: 5.0.2
39
+ version: '3.0'
68
40
  email:
69
41
  - m.gajowiak@nomtek.com
70
42
  executables: []
71
43
  extensions: []
72
44
  extra_rdoc_files: []
73
45
  files:
74
- - ".gitattributes"
75
- - ".github/workflows/main.yml"
76
- - ".gitignore"
77
- - ".rspec"
78
- - ".rubocop.yml"
79
- - ".ruby-version"
80
46
  - CHANGELOG.md
81
- - CODE_OF_CONDUCT.md
82
- - Gemfile
83
- - Gemfile.lock
47
+ - CONTRIBUTING.md
84
48
  - LICENSE.txt
85
49
  - README.md
86
- - Rakefile
87
- - bin/console
88
- - bin/setup
89
- - gemfiles/rails_6_1.gemfile
90
- - gemfiles/rails_7_1.gemfile
91
- - gemfiles/rails_7_2.gemfile
92
- - gemfiles/rails_8_0.gemfile
93
- - gemfiles/rails_8_1.gemfile
50
+ - SECURITY.md
94
51
  - lib/generators/rspec/json_api/install/install_generator.rb
95
52
  - lib/generators/rspec/json_api/install/templates/rspec/json_api/interfaces/.empty_directory
96
53
  - lib/generators/rspec/json_api/install/templates/rspec/json_api/types/.empty_directory
97
54
  - lib/generators/rspec/json_api/interface/interface_generator.rb
98
55
  - lib/generators/rspec/json_api/type/type_generator.rb
99
56
  - lib/rspec/json_api.rb
57
+ - lib/rspec/json_api/blank.rb
100
58
  - lib/rspec/json_api/constraints.rb
101
- - lib/rspec/json_api/interfaces/example_interface.rb
102
59
  - lib/rspec/json_api/matchers.rb
103
60
  - lib/rspec/json_api/matchers/have_no_content.rb
104
61
  - lib/rspec/json_api/matchers/match_json_schema.rb
@@ -108,7 +65,6 @@ files:
108
65
  - lib/rspec/json_api/types/uri.rb
109
66
  - lib/rspec/json_api/types/uuid.rb
110
67
  - lib/rspec/json_api/version.rb
111
- - rspec-json_api.gemspec
112
68
  homepage: https://github.com/nomtek/rspec-json_api
113
69
  licenses:
114
70
  - MIT
@@ -131,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
131
87
  - !ruby/object:Gem::Version
132
88
  version: '0'
133
89
  requirements: []
134
- rubygems_version: 4.0.3
90
+ rubygems_version: 3.6.9
135
91
  specification_version: 4
136
92
  summary: RSpec extension to test JSON API response.
137
93
  test_files: []
data/.gitattributes DELETED
@@ -1,28 +0,0 @@
1
- # Set default line ending behavior for all files
2
- * text=lf
3
-
4
- # Explicitly declare text files you want to always be normalized and converted
5
- # to native line endings on checkout.
6
- *.rb text=lf
7
- *.rake text=lf
8
- *.gemspec text=lf
9
- Gemfile text=lf
10
- Gemfile.lock text=lf
11
- Rakefile text=lf
12
- *.md text=lf
13
- *.yml text=lf
14
- *.yaml text=lf
15
- *.json text=lf
16
- *.txt text=lf
17
- *.sh text=lf
18
-
19
- # Denote all files that are truly binary and should not be modified.
20
- *.png binary
21
- *.jpg binary
22
- *.jpeg binary
23
- *.gif binary
24
- *.ico binary
25
- *.pdf binary
26
- *.zip binary
27
- *.tar binary
28
- *.gz binary
@@ -1,45 +0,0 @@
1
- name: Ruby
2
-
3
- on: [push, pull_request]
4
-
5
- jobs:
6
- test:
7
- runs-on: ubuntu-latest
8
- name: "rspec — Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }}"
9
- strategy:
10
- fail-fast: false
11
- matrix:
12
- # Covers the supported range from the floor (Ruby 3.2 / Rails 6.1) to
13
- # the current ceiling (Ruby 3.4 / Rails 8.1). Curated pairs avoid
14
- # Ruby/Rails combinations that are not mutually supported.
15
- include:
16
- - { ruby: "3.2", gemfile: rails_6_1 }
17
- - { ruby: "3.2", gemfile: rails_7_1 }
18
- - { ruby: "3.3", gemfile: rails_7_2 }
19
- - { ruby: "3.3", gemfile: rails_8_0 }
20
- - { ruby: "3.4", gemfile: rails_8_0 }
21
- - { ruby: "3.4", gemfile: rails_8_1 }
22
- env:
23
- BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
24
- steps:
25
- - uses: actions/checkout@v4
26
- - name: Set up Ruby
27
- uses: ruby/setup-ruby@v1
28
- with:
29
- ruby-version: ${{ matrix.ruby }}
30
- bundler-cache: true
31
- - name: Run rspec
32
- run: bundle exec rspec
33
-
34
- lint:
35
- runs-on: ubuntu-latest
36
- name: rubocop
37
- steps:
38
- - uses: actions/checkout@v4
39
- - name: Set up Ruby
40
- uses: ruby/setup-ruby@v1
41
- with:
42
- ruby-version: "3.2"
43
- bundler-cache: true
44
- - name: Run rubocop
45
- run: bundle exec rubocop
data/.gitignore DELETED
@@ -1,17 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /doc/
6
- /pkg/
7
- /spec/reports/
8
- /tmp/
9
-
10
- # rspec failure tracking
11
- .rspec_status
12
-
13
- # built gem files
14
- *.gem
15
-
16
- # per-appraisal lockfiles (resolved fresh in CI)
17
- gemfiles/*.gemfile.lock
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
data/.rubocop.yml DELETED
@@ -1,41 +0,0 @@
1
- # Append to (rather than replace) RuboCop's default AllCops/Exclude, so the
2
- # defaults such as vendor/**/* stay excluded. Without this, CI installs gems
3
- # into vendor/bundle (bundler-cache) and RuboCop would lint third-party gems.
4
- inherit_mode:
5
- merge:
6
- - Exclude
7
-
8
- AllCops:
9
- TargetRubyVersion: 3.2
10
- NewCops: enable
11
- SuggestExtensions: false
12
- Exclude:
13
- - "README.md"
14
- - "vendor/**/*"
15
- - "gemfiles/vendor/**/*"
16
-
17
- Style/StringLiterals:
18
- Enabled: true
19
- EnforcedStyle: double_quotes
20
-
21
- Style/StringLiteralsInInterpolation:
22
- Enabled: true
23
- EnforcedStyle: double_quotes
24
-
25
- Layout/LineLength:
26
- Max: 120
27
-
28
- Metrics/BlockLength:
29
- Exclude:
30
- - "spec/**/*_spec.rb"
31
-
32
- Style/Documentation:
33
- Enabled: false
34
-
35
- Naming/PredicatePrefix:
36
- Exclude:
37
- - "lib/rspec/json_api/matchers.rb"
38
-
39
- Naming/PredicateMethod:
40
- Exclude:
41
- - "lib/rspec/json_api/schema_match.rb"
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- 3.2.2
data/CODE_OF_CONDUCT.md DELETED
@@ -1,84 +0,0 @@
1
- # Contributor Covenant Code of Conduct
2
-
3
- ## Our Pledge
4
-
5
- We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
-
7
- We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
-
9
- ## Our Standards
10
-
11
- Examples of behavior that contributes to a positive environment for our community include:
12
-
13
- * Demonstrating empathy and kindness toward other people
14
- * Being respectful of differing opinions, viewpoints, and experiences
15
- * Giving and gracefully accepting constructive feedback
16
- * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
- * Focusing on what is best not just for us as individuals, but for the overall community
18
-
19
- Examples of unacceptable behavior include:
20
-
21
- * The use of sexualized language or imagery, and sexual attention or
22
- advances of any kind
23
- * Trolling, insulting or derogatory comments, and personal or political attacks
24
- * Public or private harassment
25
- * Publishing others' private information, such as a physical or email
26
- address, without their explicit permission
27
- * Other conduct which could reasonably be considered inappropriate in a
28
- professional setting
29
-
30
- ## Enforcement Responsibilities
31
-
32
- Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
-
34
- Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
-
36
- ## Scope
37
-
38
- This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
-
40
- ## Enforcement
41
-
42
- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at m.gajowiak@nomtek.com. All complaints will be reviewed and investigated promptly and fairly.
43
-
44
- All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
-
46
- ## Enforcement Guidelines
47
-
48
- Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
-
50
- ### 1. Correction
51
-
52
- **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
-
54
- **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
-
56
- ### 2. Warning
57
-
58
- **Community Impact**: A violation through a single incident or series of actions.
59
-
60
- **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
-
62
- ### 3. Temporary Ban
63
-
64
- **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
-
66
- **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
-
68
- ### 4. Permanent Ban
69
-
70
- **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
-
72
- **Consequence**: A permanent ban from any sort of public interaction within the community.
73
-
74
- ## Attribution
75
-
76
- This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
- available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
-
79
- Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
-
81
- [homepage]: https://www.contributor-covenant.org
82
-
83
- For answers to common questions about this code of conduct, see the FAQ at
84
- https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/Gemfile DELETED
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Bundler version: 4.0.4 (see Gemfile.lock BUNDLED WITH section)
4
- source "https://rubygems.org"
5
- git_source(:github) { |repo| "https://github.com/#{repo}.git" }
6
-
7
- # Specify your gem's dependencies in rspec-json_api.gemspec
8
- gemspec
9
-
10
- gem "activesupport", ">= 6.1.4.1"
11
- gem "diffy", "~> 3.4"
12
- gem "rake", "~> 13.2"
13
- gem "rspec-rails", ">= 5.0.2"
14
- gem "rubocop", "~> 1.65"