rspec-json_api 1.4.0 → 1.5.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.
@@ -8,12 +8,14 @@ module Rspec
8
8
 
9
9
  def copy_interface_file
10
10
  create_file "spec/rspec/json_api/interfaces/#{file_name}.rb", <<~FILE
11
+ # frozen_string_literal: true
12
+
11
13
  module RSpec
12
14
  module JsonApi
13
15
  module Interfaces
14
16
  #{file_name.upcase} = {
15
17
  # name: String
16
- }
18
+ }.freeze
17
19
  end
18
20
  end
19
21
  end
@@ -1,23 +1,25 @@
1
- # frozen_string_literal: true
2
-
3
- module Rspec
4
- module JsonApi
5
- module Generators
6
- class TypeGenerator < Rails::Generators::NamedBase
7
- source_root File.expand_path("templates", __dir__)
8
-
9
- def copy_type_file
10
- create_file "spec/rspec/json_api/types/#{file_name}.rb", <<~FILE
11
- module RSpec
12
- module JsonApi
13
- module Types
14
- #{file_name.upcase} = //
15
- end
16
- end
17
- end
18
- FILE
19
- end
20
- end
21
- end
22
- end
23
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Rspec
4
+ module JsonApi
5
+ module Generators
6
+ class TypeGenerator < Rails::Generators::NamedBase
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ def copy_type_file
10
+ create_file "spec/rspec/json_api/types/#{file_name}.rb", <<~FILE
11
+ # frozen_string_literal: true
12
+
13
+ module RSpec
14
+ module JsonApi
15
+ module Types
16
+ #{file_name.upcase} = //
17
+ end
18
+ end
19
+ end
20
+ FILE
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module JsonApi
5
+ # Constraints evaluates the option hash produced by a schema Proc
6
+ # (e.g. `-> { { type: Integer, min: 1, max: 10, allow_blank: true } }`)
7
+ # against an actual value.
8
+ #
9
+ # allow_blank is a modifier, not a constraint of its own: when it is true a
10
+ # blank value is accepted and the remaining options are skipped; otherwise
11
+ # the value is checked against every other option.
12
+ module Constraints
13
+ module_function
14
+
15
+ SUPPORTED_OPTIONS = %i[allow_blank type value min max inclusion regex lambda].freeze
16
+
17
+ # @param value [Object] the actual value being matched.
18
+ # @param options [Hash] the option hash returned by the schema Proc.
19
+ # @return [Boolean] true when the value satisfies the options.
20
+ # @raise [ArgumentError] when an option key is not supported.
21
+ def match(value, options)
22
+ validate!(options)
23
+
24
+ return true if value.blank? && options[:allow_blank]
25
+
26
+ options.except(:allow_blank).all? do |option, condition|
27
+ satisfies?(value, option, condition)
28
+ end
29
+ end
30
+
31
+ def validate!(options)
32
+ unknown = options.keys - SUPPORTED_OPTIONS
33
+ return if unknown.empty?
34
+
35
+ raise ArgumentError, "Unsupported match option(s): #{unknown.join(", ")}"
36
+ end
37
+
38
+ def satisfies?(value, option, condition)
39
+ case option
40
+ when :type then value.instance_of?(condition)
41
+ when :value then value == condition
42
+ when :inclusion then condition.include?(value)
43
+ when :regex then condition.match?(value.to_s)
44
+ when :lambda then condition.call(value)
45
+ when :min, :max then within_bound?(value, option, condition)
46
+ end
47
+ end
48
+
49
+ def within_bound?(value, option, condition)
50
+ return false unless value.is_a?(Numeric) && condition.is_a?(Numeric)
51
+
52
+ option == :min ? value >= condition : value <= condition
53
+ end
54
+ end
55
+ end
56
+ end
@@ -23,18 +23,13 @@ module RSpec
23
23
  # @param actual [String] The JSON string to test against the expected schema.
24
24
  # @return [Boolean] true if the actual JSON matches the expected schema, false otherwise.
25
25
  def matches?(actual)
26
+ @diff = nil
26
27
  @actual = JSON.parse(actual, symbolize_names: true)
27
- @diff = Diffy::Diff.new(expected, @actual, context: 5)
28
28
 
29
- return false unless @actual.instance_of?(expected.class)
30
-
31
- if expected.instance_of?(Array)
32
- RSpec::JsonApi::CompareArray.compare(@actual, expected)
33
- else
34
- return false unless @actual.deep_keys.deep_sort == expected.deep_keys.deep_sort
35
-
36
- RSpec::JsonApi::CompareHash.compare(@actual, expected)
37
- end
29
+ RSpec::JsonApi::SchemaMatch.match(@actual, expected)
30
+ rescue JSON::ParserError
31
+ @actual = actual
32
+ false
38
33
  end
39
34
 
40
35
  # Provides a failure message for when the JSON data does not match the expected schema.
@@ -45,7 +40,7 @@ module RSpec
45
40
  got: #{actual}
46
41
 
47
42
  Diff:
48
- #{@diff}
43
+ #{diff}
49
44
  MSG
50
45
  end
51
46
 
@@ -55,6 +50,14 @@ module RSpec
55
50
  def failure_message_when_negated
56
51
  "expected the JSON data not to match the provided schema, but it did."
57
52
  end
53
+
54
+ private
55
+
56
+ # The diff is only needed to render a failure message, so it is built
57
+ # lazily and memoized rather than on every matches? call.
58
+ def diff
59
+ @diff ||= Diffy::Diff.new(expected, actual, context: 5)
60
+ end
58
61
  end
59
62
  end
60
63
  end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module JsonApi
5
+ # SchemaMatch compares parsed JSON (a Hash, an Array, or a scalar) against an
6
+ # expected schema. It is the single entry point behind the match_json_schema
7
+ # matcher: callers hand it the actual and expected values and it dispatches on
8
+ # shape internally, so the matcher does not need to know whether it is looking
9
+ # at an object or a collection.
10
+ module SchemaMatch
11
+ module_function
12
+
13
+ # Top-level comparison. Applies the shape guards (class equality and, for
14
+ # objects, key-set equality) before recursing.
15
+ def match(actual, expected)
16
+ return false unless actual.instance_of?(expected.class)
17
+
18
+ case expected
19
+ when Array
20
+ compare_array(actual, expected)
21
+ when Hash
22
+ return false unless same_key_structure?(actual, expected)
23
+
24
+ compare(actual, expected)
25
+ else
26
+ compare_simple_value(actual, expected)
27
+ end
28
+ end
29
+
30
+ def same_key_structure?(actual, expected)
31
+ Traversal.deep_sort(Traversal.deep_keys(actual)) ==
32
+ Traversal.deep_sort(Traversal.deep_keys(expected))
33
+ end
34
+
35
+ def compare(actual, expected)
36
+ return false if actual.blank? && expected.present?
37
+
38
+ keys = Traversal.deep_key_paths(expected) | Traversal.deep_key_paths(actual)
39
+
40
+ compare_key_paths_and_values(keys, actual, expected)
41
+ end
42
+
43
+ def compare_key_paths_and_values(keys, actual, expected)
44
+ keys.all? do |key_path|
45
+ actual_value = dig_path(actual, key_path)
46
+ expected_value = dig_path(expected, key_path)
47
+
48
+ compare_values(actual_value, expected_value)
49
+ end
50
+ end
51
+
52
+ # Digs a key path without raising when an intermediate value is not a Hash.
53
+ # Plain Hash#dig raises TypeError if it walks into a scalar (e.g. a schema
54
+ # expects a nested object but the actual value is a String), so a mismatch
55
+ # would crash instead of failing the match.
56
+ def dig_path(data, key_path)
57
+ key_path.reduce(data) do |value, key|
58
+ break nil unless value.is_a?(Hash)
59
+
60
+ value[key]
61
+ end
62
+ end
63
+
64
+ def compare_values(actual_value, expected_value)
65
+ case expected_value
66
+ when Class then compare_class(actual_value, expected_value)
67
+ when Regexp then compare_regexp(actual_value, expected_value)
68
+ when Proc then compare_proc(actual_value, expected_value)
69
+ when Array then compare_array(actual_value, expected_value)
70
+ else compare_simple_value(actual_value, expected_value)
71
+ end
72
+ end
73
+
74
+ def compare_class(actual_value, expected_value)
75
+ actual_value.instance_of?(expected_value)
76
+ end
77
+
78
+ def compare_regexp(actual_value, expected_value)
79
+ expected_value.match?(actual_value.to_s)
80
+ end
81
+
82
+ def compare_proc(actual_value, expected_value)
83
+ Constraints.match(actual_value, expected_value.call)
84
+ end
85
+
86
+ def compare_array(actual_value, expected_value)
87
+ if simple_type?(expected_value)
88
+ compare_typed_array(actual_value, expected_value)
89
+ elsif interface?(expected_value)
90
+ compare_interface_array(actual_value, expected_value)
91
+ else
92
+ compare_exact_array(actual_value, expected_value)
93
+ end
94
+ end
95
+
96
+ # [SomeClass] => every element must be an instance of SomeClass.
97
+ def compare_typed_array(actual_value, expected_value)
98
+ type = expected_value[0]
99
+
100
+ actual_value.all? { |elem| compare_class(elem, type) }
101
+ end
102
+
103
+ # [{ ...interface... }] => every element must match the single interface.
104
+ # Elements go through match (not compare) so each one is held to the same
105
+ # key-structure guard as a top-level object; otherwise an element with an
106
+ # extra null-valued key would slip through (nil == nil).
107
+ def compare_interface_array(actual_value, expected_value)
108
+ interface = expected_value[0]
109
+
110
+ actual_value.all? { |elem| match(elem, interface) }
111
+ end
112
+
113
+ # Any other array => element-by-element match, sizes must be equal.
114
+ def compare_exact_array(actual_value, expected_value)
115
+ return false if actual_value&.size != expected_value&.size
116
+
117
+ 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)
119
+ end
120
+ end
121
+
122
+ def compare_simple_value(actual_value, expected_value)
123
+ actual_value == expected_value
124
+ end
125
+
126
+ def simple_type?(expected_value)
127
+ expected_value.size == 1 && expected_value[0].instance_of?(Class)
128
+ end
129
+
130
+ def interface?(expected_value)
131
+ expected_value.size == 1 && expected_value[0].is_a?(Hash)
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module JsonApi
5
+ # Traversal holds the structural helpers used to compare JSON shapes:
6
+ # collecting the nested key structure of a Hash and sorting it into a
7
+ # canonical order. These were previously monkey-patched onto core Hash and
8
+ # Array; keeping them here means the gem no longer mutates those classes in
9
+ # the host application.
10
+ module Traversal
11
+ module_function
12
+
13
+ # The nested keys of a hash, with each nested hash's keys inlined as an
14
+ # array, e.g. { a: 1, b: { c: 2 } } => [:a, :b, [:c]].
15
+ def deep_keys(hash)
16
+ hash.each_with_object([]) do |(key, value), keys|
17
+ keys << key
18
+ keys << deep_keys(value) if value.respond_to?(:keys)
19
+ end
20
+ end
21
+
22
+ # Every leaf key path of a hash, e.g. { a: { b: 1 }, c: 2 } => [[:a, :b], [:c]].
23
+ def deep_key_paths(hash)
24
+ stack = hash.map { |key, value| [[key], value] }
25
+ key_map = []
26
+
27
+ until stack.empty?
28
+ key, value = stack.pop
29
+
30
+ key_map << key unless value.is_a?(Hash)
31
+
32
+ next unless value.is_a?(Hash)
33
+
34
+ value.each { |k, v| stack.push([key.dup << k, v]) }
35
+ end
36
+
37
+ key_map.reverse
38
+ end
39
+
40
+ # Recursively sorts an array (and any nested arrays) into a canonical order
41
+ # so two key structures can be compared regardless of original order.
42
+ def deep_sort(array)
43
+ array
44
+ .map { |element| element.is_a?(Array) ? deep_sort(element) : element }
45
+ .sort_by { |element| element.is_a?(Array) ? element.first.to_s : element.to_s }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -3,7 +3,7 @@
3
3
  module RSpec
4
4
  module JsonApi
5
5
  module Types
6
- UUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/
6
+ UUID = /\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\z/
7
7
  end
8
8
  end
9
9
  end
@@ -1,7 +1,7 @@
1
- # frozen_string_literal: true
2
-
3
- module RSpec
4
- module JsonApi
5
- VERSION = "1.4.0"
6
- end
7
- end
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module JsonApi
5
+ VERSION = "1.5.0"
6
+ end
7
+ end
@@ -1,25 +1,23 @@
1
- # frozen_string_literal: true
2
-
3
- # Load 3rd party libraries
4
- require "json"
5
- require "diffy"
6
- require "active_support/core_ext/object/blank"
7
-
8
- # Load the json_api parts
9
- require "rspec/json_api/version"
10
- require "rspec/json_api/compare_hash"
11
- require "rspec/json_api/compare_array"
12
-
13
- # Load extensions
14
- require "extensions/hash"
15
- require "extensions/array"
16
-
17
- # Load matchers
18
- require "rspec/json_api/matchers"
19
- require "rspec/json_api/matchers/match_json_schema"
20
- require "rspec/json_api/matchers/have_no_content"
21
-
22
- # Load defined types
23
- require "rspec/json_api/types/email"
24
- require "rspec/json_api/types/uri"
25
- require "rspec/json_api/types/uuid"
1
+ # frozen_string_literal: true
2
+
3
+ # Load 3rd party libraries
4
+ require "json"
5
+ require "uri"
6
+ require "diffy"
7
+ require "active_support/core_ext/object/blank"
8
+
9
+ # Load the json_api parts
10
+ require "rspec/json_api/version"
11
+ require "rspec/json_api/traversal"
12
+ require "rspec/json_api/constraints"
13
+ require "rspec/json_api/schema_match"
14
+
15
+ # Load matchers
16
+ require "rspec/json_api/matchers"
17
+ require "rspec/json_api/matchers/match_json_schema"
18
+ require "rspec/json_api/matchers/have_no_content"
19
+
20
+ # Load defined types
21
+ require "rspec/json_api/types/email"
22
+ require "rspec/json_api/types/uri"
23
+ require "rspec/json_api/types/uuid"
@@ -1,38 +1,42 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/rspec/json_api/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "rspec-json_api"
7
- spec.version = RSpec::JsonApi::VERSION
8
- spec.authors = ["Michal Gajowiak"]
9
- spec.email = ["m.gajowiak@nomtek.com"]
10
-
11
- spec.summary = "RSpec extension to test JSON API response."
12
- spec.homepage = "https://github.com/nomtek/rspec-json_api"
13
- spec.license = "MIT"
14
- spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
15
-
16
- spec.metadata["homepage_uri"] = spec.homepage
17
- spec.metadata["source_code_uri"] = "https://github.com/nomtek/rspec-json_api"
18
- spec.metadata["changelog_uri"] = "https://github.com/nomtek/rspec-json_api/blob/master/CHANGELOG.md"
19
- spec.metadata["rubygems_mfa_required"] = "true"
20
-
21
- # Specify which files should be added to the gem when it is released.
22
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
- spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
25
- end
26
- spec.bindir = "exe"
27
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
- spec.require_paths = ["lib"]
29
-
30
- # Uncomment to register a new dependency of your gem
31
- spec.add_dependency "activesupport", ">= 6.1.4.1"
32
- spec.add_dependency "diffy", ">= 3.4.2"
33
- spec.add_dependency "rails", ">= 6.1.4.1"
34
- spec.add_dependency "rspec-rails", ">= 5.0.2"
35
-
36
- # For more information and examples about making a new gem, checkout our
37
- # guide at: https://bundler.io/guides/creating_gem.html
38
- end
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/rspec/json_api/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "rspec-json_api"
7
+ spec.version = RSpec::JsonApi::VERSION
8
+ spec.authors = ["Michal Gajowiak"]
9
+ spec.email = ["m.gajowiak@nomtek.com"]
10
+
11
+ spec.summary = "RSpec extension to test JSON API response."
12
+ spec.homepage = "https://github.com/nomtek/rspec-json_api"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/nomtek/rspec-json_api"
18
+ spec.metadata["changelog_uri"] = "https://github.com/nomtek/rspec-json_api/blob/master/CHANGELOG.md"
19
+ spec.metadata["rubygems_mfa_required"] = "true"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ # Runtime dependencies. The gem only needs ActiveSupport's blank?/present?
31
+ # core extensions and Rails::Generators (which lives in railties); depending
32
+ # on the full "rails" meta-gem would force ActiveRecord, ActionCable,
33
+ # ActionMailer, ActionMailbox, ActiveStorage, ActionText, etc. on every
34
+ # consumer of a JSON-matcher gem. The >= 6.1.4.1 floor is unchanged.
35
+ spec.add_dependency "activesupport", ">= 6.1.4.1"
36
+ spec.add_dependency "diffy", ">= 3.4.2"
37
+ spec.add_dependency "railties", ">= 6.1.4.1"
38
+ spec.add_dependency "rspec-rails", ">= 5.0.2"
39
+
40
+ # For more information and examples about making a new gem, checkout our
41
+ # guide at: https://bundler.io/guides/creating_gem.html
42
+ end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-json_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Gajowiak
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-01-23 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activesupport
@@ -39,7 +38,7 @@ dependencies:
39
38
  - !ruby/object:Gem::Version
40
39
  version: 3.4.2
41
40
  - !ruby/object:Gem::Dependency
42
- name: rails
41
+ name: railties
43
42
  requirement: !ruby/object:Gem::Requirement
44
43
  requirements:
45
44
  - - ">="
@@ -66,13 +65,13 @@ dependencies:
66
65
  - - ">="
67
66
  - !ruby/object:Gem::Version
68
67
  version: 5.0.2
69
- description:
70
68
  email:
71
69
  - m.gajowiak@nomtek.com
72
70
  executables: []
73
71
  extensions: []
74
72
  extra_rdoc_files: []
75
73
  files:
74
+ - ".gitattributes"
76
75
  - ".github/workflows/main.yml"
77
76
  - ".gitignore"
78
77
  - ".rspec"
@@ -87,21 +86,24 @@ files:
87
86
  - Rakefile
88
87
  - bin/console
89
88
  - bin/setup
90
- - lib/extensions/array.rb
91
- - lib/extensions/hash.rb
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
92
94
  - lib/generators/rspec/json_api/install/install_generator.rb
93
95
  - lib/generators/rspec/json_api/install/templates/rspec/json_api/interfaces/.empty_directory
94
96
  - lib/generators/rspec/json_api/install/templates/rspec/json_api/types/.empty_directory
95
97
  - lib/generators/rspec/json_api/interface/interface_generator.rb
96
- - lib/generators/rspec/json_api/interface/templates/interface.erb
97
98
  - lib/generators/rspec/json_api/type/type_generator.rb
98
99
  - lib/rspec/json_api.rb
99
- - lib/rspec/json_api/compare_array.rb
100
- - lib/rspec/json_api/compare_hash.rb
100
+ - lib/rspec/json_api/constraints.rb
101
101
  - lib/rspec/json_api/interfaces/example_interface.rb
102
102
  - lib/rspec/json_api/matchers.rb
103
103
  - lib/rspec/json_api/matchers/have_no_content.rb
104
104
  - lib/rspec/json_api/matchers/match_json_schema.rb
105
+ - lib/rspec/json_api/schema_match.rb
106
+ - lib/rspec/json_api/traversal.rb
105
107
  - lib/rspec/json_api/types/email.rb
106
108
  - lib/rspec/json_api/types/uri.rb
107
109
  - lib/rspec/json_api/types/uuid.rb
@@ -115,7 +117,6 @@ metadata:
115
117
  source_code_uri: https://github.com/nomtek/rspec-json_api
116
118
  changelog_uri: https://github.com/nomtek/rspec-json_api/blob/master/CHANGELOG.md
117
119
  rubygems_mfa_required: 'true'
118
- post_install_message:
119
120
  rdoc_options: []
120
121
  require_paths:
121
122
  - lib
@@ -130,8 +131,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
131
  - !ruby/object:Gem::Version
131
132
  version: '0'
132
133
  requirements: []
133
- rubygems_version: 3.4.10
134
- signing_key:
134
+ rubygems_version: 4.0.3
135
135
  specification_version: 4
136
136
  summary: RSpec extension to test JSON API response.
137
137
  test_files: []
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Array
4
- def deep_sort
5
- map { |element| element.is_a?(Array) ? element.deep_sort : element }
6
- .sort_by { |el| el.is_a?(Array) ? el.first.to_s : el.to_s }
7
- end
8
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Extension methods for hash class
4
- class Hash
5
- def deep_keys
6
- each_with_object([]) do |(k, v), keys|
7
- keys << k
8
- keys << v.deep_keys if v.respond_to?(:keys)
9
- end
10
- end
11
-
12
- def deep_key_paths
13
- stack = map { |k, v| [[k], v] }
14
- key_map = []
15
-
16
- until stack.empty?
17
- key, value = stack.pop
18
-
19
- key_map << key unless value.is_a? Hash
20
-
21
- next unless value.is_a? Hash
22
-
23
- value.map do |k, v|
24
- stack.push [key.dup << k, v]
25
- end
26
- end
27
-
28
- key_map.reverse
29
- end
30
-
31
- def sanitize!(keys)
32
- keep_if do |k, _v|
33
- keys.include?(k)
34
- end
35
- end
36
- end
@@ -1,9 +0,0 @@
1
- <%= module RSpec %>
2
- <%= module JsonApi %>
3
- <%= module Interfaces %>
4
- <%= const_set(file_name, {
5
-
6
- }.freeze) %>
7
- <%= end %>
8
- <%= end %>
9
- <%= end %>