rspec-openapi 0.25.1 → 0.27.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.
- checksums.yaml +4 -4
- data/.github/workflows/create_release.yml +1 -1
- data/.github/workflows/publish.yml +1 -1
- data/.github/workflows/validate-openapi.yml +21 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +4 -0
- data/.rubocop_todo.yml +13 -13
- data/README.md +337 -36
- data/lib/rspec/openapi/extractors/hanami.rb +10 -29
- data/lib/rspec/openapi/extractors/rack.rb +7 -24
- data/lib/rspec/openapi/extractors/rails.rb +14 -27
- data/lib/rspec/openapi/extractors/shared_extractor.rb +102 -18
- data/lib/rspec/openapi/record.rb +6 -1
- data/lib/rspec/openapi/record_builder.rb +8 -22
- data/lib/rspec/openapi/schema_builder/build_context.rb +20 -0
- data/lib/rspec/openapi/schema_builder.rb +288 -286
- data/lib/rspec/openapi/schema_cleaner.rb +4 -1
- data/lib/rspec/openapi/schema_file.rb +4 -6
- data/lib/rspec/openapi/schema_merger.rb +81 -40
- data/lib/rspec/openapi/version.rb +1 -1
- data/lib/rspec/openapi.rb +2 -2
- data/redocly.yaml +31 -0
- metadata +6 -3
|
@@ -48,44 +48,25 @@ end)
|
|
|
48
48
|
class << RSpec::OpenAPI::Extractors::Hanami = Object.new
|
|
49
49
|
# @param [ActionDispatch::Request] request
|
|
50
50
|
# @param [RSpec::Core::Example] example
|
|
51
|
-
# @return
|
|
51
|
+
# @return [Hash]
|
|
52
52
|
def request_attributes(request, example)
|
|
53
53
|
route = Hanami.app.router.recognize(Rack::MockRequest.env_for(request.path, method: request.method))
|
|
54
54
|
|
|
55
55
|
return RSpec::OpenAPI::Extractors::Rack.request_attributes(request, example) unless route.routable?
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
example_key, example_name, response_enum, request_enum = SharedExtractor.attributes(example)
|
|
59
|
-
|
|
57
|
+
attrs = SharedExtractor.attributes(example)
|
|
60
58
|
path = request.path
|
|
59
|
+
result = InspectorAnalyzer.call(request.method, replace_path_params(path, route, '/:%<key>s'))
|
|
61
60
|
|
|
62
61
|
raw_path_params = route.params
|
|
63
|
-
|
|
64
|
-
result = InspectorAnalyzer.call(request.method, replace_path_params(path, route, '/:%{key}'))
|
|
65
|
-
|
|
66
|
-
summary ||= result[:summary]
|
|
67
|
-
tags ||= result[:tags]
|
|
68
|
-
path = replace_path_params(path, route, '/{%{key}}')
|
|
69
|
-
|
|
70
62
|
raw_path_params = raw_path_params.slice(*(raw_path_params.keys - RSpec::OpenAPI.ignored_path_params))
|
|
71
63
|
|
|
72
|
-
|
|
73
|
-
path,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
raw_path_params,
|
|
79
|
-
description,
|
|
80
|
-
security,
|
|
81
|
-
deprecated,
|
|
82
|
-
formats,
|
|
83
|
-
example_mode,
|
|
84
|
-
example_key,
|
|
85
|
-
example_name,
|
|
86
|
-
response_enum,
|
|
87
|
-
request_enum,
|
|
88
|
-
]
|
|
64
|
+
attrs.merge(
|
|
65
|
+
path: replace_path_params(path, route, '/{%<key>s}'),
|
|
66
|
+
path_params: raw_path_params,
|
|
67
|
+
summary: attrs[:summary] || result[:summary],
|
|
68
|
+
tags: attrs[:tags] || result[:tags],
|
|
69
|
+
)
|
|
89
70
|
end
|
|
90
71
|
|
|
91
72
|
# @param [RSpec::ExampleGroups::*] context
|
|
@@ -103,7 +84,7 @@ class << RSpec::OpenAPI::Extractors::Hanami = Object.new
|
|
|
103
84
|
return path if route.params.empty?
|
|
104
85
|
|
|
105
86
|
route.params.each_pair do |key, value|
|
|
106
|
-
path = path.sub("/#{value}", format
|
|
87
|
+
path = path.sub("/#{value}", format(format, key: key))
|
|
107
88
|
end
|
|
108
89
|
|
|
109
90
|
path
|
|
@@ -4,32 +4,15 @@
|
|
|
4
4
|
class << RSpec::OpenAPI::Extractors::Rack = Object.new
|
|
5
5
|
# @param [ActionDispatch::Request] request
|
|
6
6
|
# @param [RSpec::Core::Example] example
|
|
7
|
-
# @return
|
|
7
|
+
# @return [Hash]
|
|
8
8
|
def request_attributes(request, example)
|
|
9
|
-
summary, tags, formats, operation_id, required_request_params, security, description, deprecated, example_mode,
|
|
10
|
-
example_key, example_name, response_enum, request_enum = SharedExtractor.attributes(example)
|
|
11
|
-
|
|
12
|
-
raw_path_params = request.path_parameters
|
|
13
9
|
path = request.path
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
summary,
|
|
19
|
-
|
|
20
|
-
operation_id,
|
|
21
|
-
required_request_params,
|
|
22
|
-
raw_path_params,
|
|
23
|
-
description,
|
|
24
|
-
security,
|
|
25
|
-
deprecated,
|
|
26
|
-
formats,
|
|
27
|
-
example_mode,
|
|
28
|
-
example_key,
|
|
29
|
-
example_name,
|
|
30
|
-
response_enum,
|
|
31
|
-
request_enum,
|
|
32
|
-
]
|
|
10
|
+
attrs = SharedExtractor.attributes(example)
|
|
11
|
+
attrs.merge(
|
|
12
|
+
path: path,
|
|
13
|
+
path_params: request.path_parameters,
|
|
14
|
+
summary: attrs[:summary] || "#{request.method} #{path}",
|
|
15
|
+
)
|
|
33
16
|
end
|
|
34
17
|
|
|
35
18
|
# @param [RSpec::ExampleGroups::*] context
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
class << RSpec::OpenAPI::Extractors::Rails = Object.new
|
|
5
5
|
# @param [ActionDispatch::Request] request
|
|
6
6
|
# @param [RSpec::Core::Example] example
|
|
7
|
-
# @return
|
|
7
|
+
# @return [Hash]
|
|
8
8
|
def request_attributes(request, example)
|
|
9
9
|
# Reverse the destructive modification by Rails https://github.com/rails/rails/blob/v6.0.3.4/actionpack/lib/action_dispatch/journey/router.rb#L33-L41
|
|
10
10
|
fixed_request = request.dup
|
|
@@ -16,40 +16,27 @@ class << RSpec::OpenAPI::Extractors::Rails = Object.new
|
|
|
16
16
|
|
|
17
17
|
raise "No route matched for #{fixed_request.request_method} #{fixed_request.path_info}" if route.nil?
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
example_key, example_name, response_enum, request_enum = SharedExtractor.attributes(example)
|
|
21
|
-
|
|
22
|
-
raw_path_params = request.path_parameters
|
|
23
|
-
|
|
24
|
-
summary ||= route.requirements[:action]
|
|
25
|
-
tags ||= [route.requirements[:controller]&.classify].compact
|
|
19
|
+
attrs = SharedExtractor.attributes(example)
|
|
26
20
|
# :controller and :action always exist. :format is added when routes is configured as such.
|
|
27
21
|
# TODO: Use .except(:controller, :action, :format) when we drop support for Ruby 2.x
|
|
22
|
+
raw_path_params = request.path_parameters
|
|
28
23
|
raw_path_params = raw_path_params.slice(*(raw_path_params.keys - RSpec::OpenAPI.ignored_path_params))
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
path,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
operation_id,
|
|
37
|
-
required_request_params,
|
|
38
|
-
raw_path_params,
|
|
39
|
-
description,
|
|
40
|
-
security,
|
|
41
|
-
deprecated,
|
|
42
|
-
formats,
|
|
43
|
-
example_mode,
|
|
44
|
-
example_key,
|
|
45
|
-
example_name,
|
|
46
|
-
response_enum,
|
|
47
|
-
request_enum,
|
|
48
|
-
]
|
|
25
|
+
attrs.merge(
|
|
26
|
+
path: path,
|
|
27
|
+
path_params: raw_path_params,
|
|
28
|
+
summary: attrs[:summary] || route.requirements[:action] || "#{request.method} #{path}",
|
|
29
|
+
tags: attrs[:tags] || [route.requirements[:controller]&.classify].compact,
|
|
30
|
+
)
|
|
49
31
|
end
|
|
50
32
|
|
|
51
33
|
# @param [RSpec::ExampleGroups::*] context
|
|
52
34
|
def request_response(context)
|
|
35
|
+
# Read @integration_session directly so user-defined let(:request)/let(:response)
|
|
36
|
+
# don't shadow the real ActionDispatch objects we need for OpenAPI extraction.
|
|
37
|
+
session = context.instance_variable_get(:@integration_session)
|
|
38
|
+
return [session.request, session.response] if session
|
|
39
|
+
|
|
53
40
|
[context.request, context.response]
|
|
54
41
|
end
|
|
55
42
|
|
|
@@ -2,31 +2,67 @@
|
|
|
2
2
|
|
|
3
3
|
# Shared extractor for extracting OpenAPI metadata from RSpec examples
|
|
4
4
|
class SharedExtractor
|
|
5
|
-
VALID_EXAMPLE_MODES =
|
|
5
|
+
VALID_EXAMPLE_MODES = [:none, :single, :multiple].freeze
|
|
6
|
+
|
|
7
|
+
EXAMPLE_MODE_MULTIPLE_SHORTHAND_WARNING = <<~MSG.tr("\n", ' ').strip.freeze
|
|
8
|
+
[rspec-openapi] DEPRECATION: example_mode: :multiple currently means
|
|
9
|
+
{ request: :single, response: :multiple }. A future major version will
|
|
10
|
+
change it to { request: :multiple, response: :multiple } (both sides
|
|
11
|
+
multi). Specify the hash form explicitly to lock in current behavior or
|
|
12
|
+
opt in early.
|
|
13
|
+
MSG
|
|
6
14
|
|
|
7
15
|
def self.attributes(example)
|
|
8
16
|
metadata = merge_openapi_metadata(example.metadata)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
request_example_mode, response_example_mode = normalize_example_mode(metadata[:example_mode], example)
|
|
18
|
+
response_additional_properties, request_additional_properties = resolve_additional_properties(metadata)
|
|
19
|
+
response_hybrid_additional_properties, request_hybrid_additional_properties =
|
|
20
|
+
resolve_hybrid_additional_properties(metadata)
|
|
21
|
+
# Enum support: response_enum and request_enum can override the general enum
|
|
22
|
+
base_enum = normalize_enum(metadata[:enum])
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
summary: metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example),
|
|
26
|
+
tags: metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example),
|
|
27
|
+
formats: metadata[:formats] || RSpec::OpenAPI.formats_builder.curry.call(example),
|
|
28
|
+
operation_id: metadata[:operation_id],
|
|
29
|
+
required_request_params: metadata[:required_request_params] || [],
|
|
30
|
+
security: metadata[:security],
|
|
31
|
+
description: metadata[:description] || RSpec::OpenAPI.description_builder.call(example),
|
|
32
|
+
deprecated: metadata[:deprecated],
|
|
33
|
+
request_example_mode: request_example_mode,
|
|
34
|
+
response_example_mode: response_example_mode,
|
|
35
|
+
example_key: resolve_example_key(metadata, example),
|
|
36
|
+
example_name: metadata[:example_name] || RSpec::OpenAPI.example_name_builder.call(example),
|
|
37
|
+
response_enum: normalize_enum(metadata[:response_enum]) || base_enum,
|
|
38
|
+
request_enum: normalize_enum(metadata[:request_enum]) || base_enum,
|
|
39
|
+
response_additional_properties: response_additional_properties,
|
|
40
|
+
request_additional_properties: request_additional_properties,
|
|
41
|
+
response_hybrid_additional_properties: response_hybrid_additional_properties,
|
|
42
|
+
request_hybrid_additional_properties: request_hybrid_additional_properties,
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.resolve_example_key(metadata, example)
|
|
18
47
|
example_name = metadata[:example_name] || RSpec::OpenAPI.example_name_builder.call(example)
|
|
19
48
|
raw_example_key = metadata[:example_key] || example_name
|
|
20
49
|
example_key = RSpec::OpenAPI::ExampleKey.normalize(raw_example_key)
|
|
21
50
|
example_key = 'default' if example_key.nil? || example_key.empty?
|
|
51
|
+
example_key
|
|
52
|
+
end
|
|
22
53
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
54
|
+
def self.resolve_additional_properties(metadata)
|
|
55
|
+
base = normalize_additional_properties(metadata[:additional_properties])
|
|
56
|
+
response = normalize_additional_properties(metadata[:response_additional_properties]) || base
|
|
57
|
+
request = normalize_additional_properties(metadata[:request_additional_properties]) || base
|
|
58
|
+
[response, request]
|
|
59
|
+
end
|
|
27
60
|
|
|
28
|
-
|
|
29
|
-
|
|
61
|
+
def self.resolve_hybrid_additional_properties(metadata)
|
|
62
|
+
base = normalize_additional_properties(metadata[:hybrid_additional_properties])
|
|
63
|
+
response = normalize_additional_properties(metadata[:response_hybrid_additional_properties]) || base
|
|
64
|
+
request = normalize_additional_properties(metadata[:request_hybrid_additional_properties]) || base
|
|
65
|
+
[response, request]
|
|
30
66
|
end
|
|
31
67
|
|
|
32
68
|
def self.normalize_enum(enum_hash)
|
|
@@ -36,6 +72,14 @@ class SharedExtractor
|
|
|
36
72
|
enum_hash.transform_keys(&:to_s)
|
|
37
73
|
end
|
|
38
74
|
|
|
75
|
+
def self.normalize_additional_properties(hash)
|
|
76
|
+
return nil if hash.nil? || hash.empty?
|
|
77
|
+
|
|
78
|
+
hash.each_with_object({}) do |(path, schema), result|
|
|
79
|
+
result[path.to_s] = RSpec::OpenAPI::KeyTransformer.symbolize(schema)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
39
83
|
def self.merge_openapi_metadata(metadata)
|
|
40
84
|
collect_openapi_metadata(metadata).reduce({}, &:merge)
|
|
41
85
|
end
|
|
@@ -54,9 +98,33 @@ class SharedExtractor
|
|
|
54
98
|
end
|
|
55
99
|
end
|
|
56
100
|
|
|
101
|
+
# Returns [request_mode, response_mode]. Accepts either a bare Symbol/String
|
|
102
|
+
# (applied to both sides, except :multiple which is treated as a backward-compat
|
|
103
|
+
# shorthand for { request: :single, response: :multiple } and emits a one-time
|
|
104
|
+
# deprecation warning) or a Hash with :request / :response keys.
|
|
57
105
|
def self.normalize_example_mode(value, example = nil)
|
|
58
|
-
return :single if value.nil?
|
|
106
|
+
return [:single, :single] if value.nil?
|
|
107
|
+
|
|
108
|
+
case value
|
|
109
|
+
when Hash
|
|
110
|
+
[
|
|
111
|
+
normalize_example_mode_hash_value(value, :request, example),
|
|
112
|
+
normalize_example_mode_hash_value(value, :response, example),
|
|
113
|
+
]
|
|
114
|
+
when Symbol, String
|
|
115
|
+
mode = coerce_example_mode_value(value, example)
|
|
116
|
+
if mode == :multiple
|
|
117
|
+
warn_example_mode_multiple_shorthand
|
|
118
|
+
[:single, :multiple]
|
|
119
|
+
else
|
|
120
|
+
[mode, mode]
|
|
121
|
+
end
|
|
122
|
+
else
|
|
123
|
+
raise ArgumentError, example_mode_error(value, example)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
59
126
|
|
|
127
|
+
def self.coerce_example_mode_value(value, example)
|
|
60
128
|
raise ArgumentError, example_mode_error(value, example) unless value.is_a?(String) || value.is_a?(Symbol)
|
|
61
129
|
|
|
62
130
|
mode = value.to_s.strip.downcase.to_sym
|
|
@@ -65,9 +133,25 @@ class SharedExtractor
|
|
|
65
133
|
raise ArgumentError, example_mode_error(value, example)
|
|
66
134
|
end
|
|
67
135
|
|
|
136
|
+
def self.normalize_example_mode_hash_value(hash, key, example)
|
|
137
|
+
raw = hash[key]
|
|
138
|
+
raw = hash[key.to_s] if raw.nil?
|
|
139
|
+
return :single if raw.nil?
|
|
140
|
+
|
|
141
|
+
coerce_example_mode_value(raw, example)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.warn_example_mode_multiple_shorthand
|
|
145
|
+
return if @warned_example_mode_multiple_shorthand
|
|
146
|
+
|
|
147
|
+
@warned_example_mode_multiple_shorthand = true
|
|
148
|
+
Kernel.warn(EXAMPLE_MODE_MULTIPLE_SHORTHAND_WARNING)
|
|
149
|
+
end
|
|
150
|
+
|
|
68
151
|
def self.example_mode_error(value, example)
|
|
69
152
|
context = example&.full_description
|
|
70
153
|
context = " (example: #{context})" if context
|
|
71
|
-
"example_mode must be
|
|
154
|
+
"example_mode must be a Symbol/String in #{VALID_EXAMPLE_MODES.inspect} " \
|
|
155
|
+
"or a Hash with :request/:response keys, got #{value.inspect}#{context}"
|
|
72
156
|
end
|
|
73
157
|
end
|
data/lib/rspec/openapi/record.rb
CHANGED
|
@@ -20,7 +20,8 @@ RSpec::OpenAPI::Record = Struct.new(
|
|
|
20
20
|
:security, # @param [Array] - [{securityScheme1: []}]
|
|
21
21
|
:deprecated, # @param [Boolean] - true
|
|
22
22
|
:example_enabled, # @param [Boolean] - true
|
|
23
|
-
:
|
|
23
|
+
:request_example_mode, # @param [Symbol] - :none | :single | :multiple
|
|
24
|
+
:response_example_mode, # @param [Symbol] - :none | :single | :multiple
|
|
24
25
|
:status, # @param [Integer] - 200
|
|
25
26
|
:response_body, # @param [Object] - {"status" => "ok"}
|
|
26
27
|
:response_headers, # @param [Array] - [["header_key1", "header_value1"], ["header_key2", "header_value2"]]
|
|
@@ -28,5 +29,9 @@ RSpec::OpenAPI::Record = Struct.new(
|
|
|
28
29
|
:response_content_disposition, # @param [String] - "inline"
|
|
29
30
|
:response_enum, # @param [Hash] - {"status" => ["active", "inactive"], "user.role" => ["admin", "user"]}
|
|
30
31
|
:request_enum, # @param [Hash] - {"type" => ["create", "update"]}
|
|
32
|
+
:response_additional_properties, # @param [Hash] - {"data" => { type: "boolean" }}
|
|
33
|
+
:request_additional_properties, # @param [Hash] - {"meta" => { type: "string" }}
|
|
34
|
+
:response_hybrid_additional_properties, # @param [Hash] - {"data" => { type: "string" }}
|
|
35
|
+
:request_hybrid_additional_properties, # @param [Hash] - {"meta" => { type: "string" }}
|
|
31
36
|
keyword_init: true,
|
|
32
37
|
)
|
|
@@ -11,48 +11,34 @@ class << RSpec::OpenAPI::RecordBuilder = Object.new
|
|
|
11
11
|
request, response = extractor.request_response(context)
|
|
12
12
|
return if request.nil?
|
|
13
13
|
|
|
14
|
+
attributes = extractor.request_attributes(request, example)
|
|
15
|
+
return if RSpec::OpenAPI.ignored_paths.any? { |ignored_path| attributes[:path].match?(ignored_path) }
|
|
16
|
+
|
|
14
17
|
title = RSpec::OpenAPI.title.then { |t| t.is_a?(Proc) ? t.call(example) : t }
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
example_name, response_enum, request_enum = extractor.request_attributes(request, example)
|
|
18
|
+
build_record(title, request, response, attributes)
|
|
19
|
+
end
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
private
|
|
20
22
|
|
|
23
|
+
def build_record(title, request, response, attributes)
|
|
21
24
|
request_headers, response_headers = extract_headers(request, response)
|
|
22
|
-
|
|
23
25
|
RSpec::OpenAPI::Record.new(
|
|
24
26
|
title: title,
|
|
25
27
|
http_method: request.method,
|
|
26
|
-
path: path,
|
|
27
|
-
path_params: raw_path_params,
|
|
28
28
|
query_params: request.query_parameters,
|
|
29
29
|
request_params: raw_request_params(request),
|
|
30
|
-
required_request_params: required_request_params,
|
|
31
30
|
request_content_type: request.media_type,
|
|
32
31
|
request_headers: request_headers,
|
|
33
|
-
summary: summary,
|
|
34
|
-
tags: tags,
|
|
35
|
-
formats: formats,
|
|
36
|
-
operation_id: operation_id,
|
|
37
|
-
description: description,
|
|
38
|
-
security: security,
|
|
39
|
-
deprecated: deprecated,
|
|
40
32
|
status: response.status,
|
|
41
33
|
response_body: safe_parse_body(response, response.media_type),
|
|
42
34
|
response_headers: response_headers,
|
|
43
35
|
response_content_type: response.media_type,
|
|
44
36
|
response_content_disposition: response.header['Content-Disposition'],
|
|
45
37
|
example_enabled: RSpec::OpenAPI.enable_example,
|
|
46
|
-
|
|
47
|
-
example_key: example_key,
|
|
48
|
-
example_name: example_name,
|
|
49
|
-
response_enum: response_enum,
|
|
50
|
-
request_enum: request_enum,
|
|
38
|
+
**attributes,
|
|
51
39
|
).freeze
|
|
52
40
|
end
|
|
53
41
|
|
|
54
|
-
private
|
|
55
|
-
|
|
56
42
|
def safe_parse_body(response, media_type)
|
|
57
43
|
# Use raw body, because Nokogiri-parsed HTML are modified (new lines injection, meta injection, and so on) :(
|
|
58
44
|
return response.body if media_type == 'text/html'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class << RSpec::OpenAPI::SchemaBuilder
|
|
4
|
+
# Lookup context threaded through schema building. Bundles the metadata
|
|
5
|
+
# needed to resolve formats, enums, and additionalProperties overrides for
|
|
6
|
+
# a value at a given path under a record's request/response side.
|
|
7
|
+
BuildContext = Struct.new(:record, :context, :path, :key, keyword_init: true) do
|
|
8
|
+
def descend(child_key)
|
|
9
|
+
self.class.new(
|
|
10
|
+
record: record, context: context, key: child_key,
|
|
11
|
+
path: path ? "#{path}.#{child_key}" : child_key.to_s,
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def for_array_items
|
|
16
|
+
self.class.new(record: record, context: context, path: path, key: nil)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
private_constant :BuildContext
|
|
20
|
+
end
|