explicit 0.2.0 → 0.2.2

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -68
  3. data/app/helpers/explicit/application_helper.rb +32 -0
  4. data/app/views/explicit/documentation/_attribute.html.erb +38 -0
  5. data/app/views/explicit/documentation/_page.html.erb +166 -0
  6. data/app/views/explicit/documentation/_request.html.erb +87 -0
  7. data/app/views/explicit/documentation/request/_examples.html.erb +50 -0
  8. data/app/views/explicit/documentation/type/_agreement.html.erb +7 -0
  9. data/app/views/explicit/documentation/type/_array.html.erb +3 -0
  10. data/app/views/explicit/documentation/type/_big_decimal.html.erb +4 -0
  11. data/app/views/explicit/documentation/type/_boolean.html.erb +7 -0
  12. data/app/views/explicit/documentation/type/_date_time_iso8601.html.erb +3 -0
  13. data/app/views/explicit/documentation/type/_date_time_posix.html.erb +3 -0
  14. data/app/views/explicit/documentation/type/_enum.html.erb +7 -0
  15. data/app/views/explicit/documentation/type/_file.html.erb +9 -0
  16. data/app/views/explicit/documentation/type/_hash.html.erb +4 -0
  17. data/app/views/explicit/documentation/type/_integer.html.erb +25 -0
  18. data/app/views/explicit/documentation/type/_one_of.html.erb +11 -0
  19. data/app/views/explicit/documentation/type/_record.html.erb +9 -0
  20. data/app/views/explicit/documentation/type/_string.html.erb +21 -0
  21. data/config/locales/en.yml +27 -11
  22. data/lib/explicit/configuration.rb +1 -1
  23. data/lib/explicit/documentation/builder.rb +80 -0
  24. data/lib/explicit/documentation/markdown.rb +2 -13
  25. data/lib/explicit/documentation/output/swagger.rb +176 -0
  26. data/lib/explicit/documentation/output/webpage.rb +31 -0
  27. data/lib/explicit/documentation/page/partial.rb +20 -0
  28. data/lib/explicit/documentation/page/request.rb +27 -0
  29. data/lib/explicit/documentation/section.rb +9 -0
  30. data/lib/explicit/documentation.rb +12 -145
  31. data/lib/explicit/request/example.rb +50 -1
  32. data/lib/explicit/request/invalid_params_error.rb +1 -3
  33. data/lib/explicit/request/invalid_response_error.rb +2 -15
  34. data/lib/explicit/request/route.rb +18 -0
  35. data/lib/explicit/request.rb +43 -24
  36. data/lib/explicit/test_helper/example_recorder.rb +7 -2
  37. data/lib/explicit/test_helper.rb +25 -7
  38. data/lib/explicit/type/agreement.rb +39 -0
  39. data/lib/explicit/type/array.rb +56 -0
  40. data/lib/explicit/type/big_decimal.rb +58 -0
  41. data/lib/explicit/type/boolean.rb +47 -0
  42. data/lib/explicit/type/date_time_iso8601.rb +41 -0
  43. data/lib/explicit/type/date_time_posix.rb +44 -0
  44. data/lib/explicit/type/enum.rb +41 -0
  45. data/lib/explicit/type/file.rb +60 -0
  46. data/lib/explicit/type/hash.rb +57 -0
  47. data/lib/explicit/type/integer.rb +79 -0
  48. data/lib/explicit/type/literal.rb +45 -0
  49. data/lib/explicit/type/modifiers/default.rb +24 -0
  50. data/lib/explicit/type/modifiers/description.rb +11 -0
  51. data/lib/explicit/type/modifiers/nilable.rb +19 -0
  52. data/lib/explicit/type/modifiers/param_location.rb +11 -0
  53. data/lib/explicit/type/one_of.rb +46 -0
  54. data/lib/explicit/type/record.rb +96 -0
  55. data/lib/explicit/type/string.rb +68 -0
  56. data/lib/explicit/type.rb +112 -0
  57. data/lib/explicit/version.rb +1 -1
  58. data/lib/explicit.rb +28 -18
  59. metadata +47 -25
  60. data/app/views/explicit/application/_documentation.html.erb +0 -136
  61. data/app/views/explicit/application/_request.html.erb +0 -37
  62. data/lib/explicit/documentation/property.rb +0 -19
  63. data/lib/explicit/spec/agreement.rb +0 -17
  64. data/lib/explicit/spec/array.rb +0 -28
  65. data/lib/explicit/spec/bigdecimal.rb +0 -27
  66. data/lib/explicit/spec/boolean.rb +0 -30
  67. data/lib/explicit/spec/date_time_iso8601.rb +0 -17
  68. data/lib/explicit/spec/date_time_posix.rb +0 -21
  69. data/lib/explicit/spec/default.rb +0 -20
  70. data/lib/explicit/spec/error.rb +0 -63
  71. data/lib/explicit/spec/hash.rb +0 -30
  72. data/lib/explicit/spec/inclusion.rb +0 -15
  73. data/lib/explicit/spec/integer.rb +0 -53
  74. data/lib/explicit/spec/literal.rb +0 -15
  75. data/lib/explicit/spec/nilable.rb +0 -15
  76. data/lib/explicit/spec/one_of.rb +0 -40
  77. data/lib/explicit/spec/record.rb +0 -33
  78. data/lib/explicit/spec/string.rb +0 -50
  79. data/lib/explicit/spec.rb +0 -72
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Documentation::Page
4
+ class Request
5
+ attr_reader :request
6
+
7
+ def initialize(request:)
8
+ @request = request
9
+ end
10
+
11
+ def request?
12
+ true
13
+ end
14
+
15
+ def title
16
+ @request.get_title
17
+ end
18
+
19
+ def anchor
20
+ title.gsub(" ", "-").downcase
21
+ end
22
+
23
+ def partial
24
+ "explicit/documentation/request"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Documentation
4
+ Section = ::Data.define(:name, :pages) do
5
+ def contains_request?
6
+ pages.any?(&:request?)
7
+ end
8
+ end
9
+ end
@@ -3,156 +3,23 @@
3
3
  require "commonmarker"
4
4
 
5
5
  module Explicit::Documentation
6
- Section = ::Data.define(:name, :pages)
6
+ extend self
7
7
 
8
- module Page
9
- class Partial
10
- attr_reader :title, :partial
8
+ def new(&block)
9
+ engine = ::Class.new(::Rails::Engine)
11
10
 
12
- def initialize(title:, partial:)
13
- @title = title
14
- @partial = partial
15
- end
16
-
17
- def request?
18
- false
19
- end
20
-
21
- def anchor
22
- title.dasherize
23
- end
24
- end
25
-
26
- class Request
27
- attr_reader :request
28
-
29
- def initialize(request:)
30
- @request = request
31
- end
32
-
33
- def request?
34
- true
35
- end
36
-
37
- def title
38
- @request.get_title
39
- end
40
-
41
- def description_html
42
- Explicit::Documentation::Markdown.render(@request.get_description).html_safe
43
- end
44
-
45
- def anchor
46
- title.dasherize
47
- end
48
-
49
- def params_properties
50
- @request.params.map do |name, spec|
51
- Property.new(name:, spec:)
52
- end
53
- end
54
-
55
- def headers_properties
56
- @request.headers.map do |name, spec|
57
- Property.new(name:, spec:)
58
- end
59
- end
60
-
61
- def partial
62
- "request"
63
- end
64
- end
65
- end
66
-
67
- class Builder
68
- attr_reader :sections
69
-
70
- def initialize
71
- @sections = []
72
- @current_section = nil
73
- end
74
-
75
- def page_title(page_title)
76
- @page_title = page_title
11
+ builder = Builder.new(engine).tap do |builder|
12
+ builder.instance_eval &block
13
+ builder.merge_request_examples_from_file!
77
14
  end
78
15
 
79
- def section(name, &block)
80
- @current_section = Section.new(name:, pages: [])
81
-
82
- block.call
83
-
84
- @sections << @current_section
85
-
86
- @current_section = nil
87
- end
88
-
89
- def add(*requests, **opts)
90
- raise ArgumentError(<<-MD) if @current_section.nil?
91
- You must define a section before adding a page. For example:
92
-
93
- section "Customers" do
94
- add CustomersController::CreateRequest
95
- end
96
- MD
97
-
98
- if requests.one?
99
- @current_section.pages << Page::Request.new(request: requests.first)
100
- elsif opts[:partial]
101
- @current_section.pages << Page::Partial.new(title: opts[:title], partial: opts[:partial])
102
- else
103
- raise ArgumentError("expected request or a partial")
104
- end
105
- end
106
-
107
- def call(request)
108
- @html ||= render_documentation_page
109
-
110
- [200, {}, [@html]]
16
+ engine.define_singleton_method(:documentation_builder) { builder }
17
+
18
+ engine.routes.draw do
19
+ get "/", to: builder.webpage, as: :explicit_documentation_webpage
20
+ get "/swagger", to: builder.swagger, as: :explicit_documentation_swagger
111
21
  end
112
22
 
113
- private
114
- def render_documentation_page
115
- merge_request_examples_from_file!
116
-
117
- Explicit::ApplicationController.render(
118
- partial: "documentation",
119
- locals: {
120
- page_title: @page_title,
121
- sections: @sections
122
- }
123
- )
124
- end
125
-
126
- def merge_request_examples_from_file!
127
- return if !Explicit.configuration.request_examples_file_path
128
-
129
- encoded = ::File.read(Explicit.configuration.request_examples_file_path)
130
- examples = ::JSON.parse(encoded)
131
-
132
- @sections.each do |section|
133
- section.pages.filter(&:request?).each do |page|
134
- if examples.key?(page.request.gid)
135
- examples[page.request.gid].each do |example|
136
- page.request.add_example(
137
- params: example["params"].with_indifferent_access,
138
- headers: example["headers"],
139
- response: {
140
- status: example.dig("response", "status"),
141
- data: example.dig("response", "data").with_indifferent_access
142
- }
143
- )
144
- end
145
- end
146
- end
147
- end
148
- end
149
- end
150
-
151
- def self.build(&block)
152
- builder = Builder.new.tap { _1.instance_eval &block }
153
-
154
- ::Class.new(::Rails::Engine).tap do |engine|
155
- engine.routes.draw { root to: builder }
156
- end
23
+ engine
157
24
  end
158
25
  end
@@ -1,5 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Explicit::Request
4
- Example = ::Data.define(:params, :headers, :response)
4
+ Example = ::Data.define(:request, :params, :headers, :response) do
5
+ def to_curl_lines
6
+ route = request.routes.first
7
+ method = route.method.to_s.upcase
8
+ path = route.replace_path_params(params)
9
+ url = "#{request.get_base_url}#{request.get_base_path}#{path}"
10
+ curl_request = "curl -X#{method} \"#{url}\""
11
+
12
+ body_params = params.except(*route.params)
13
+
14
+ curl_headers =
15
+ if body_params.empty?
16
+ []
17
+ elsif request.accepts_file_upload?
18
+ ['-H "Content-Type: multipart/form-data"']
19
+ else
20
+ ['-H "Content-Type: application/json"']
21
+ end
22
+
23
+ headers.each do |key, value|
24
+ curl_headers << "-H \"#{key}: #{value}\""
25
+ end
26
+
27
+ curl_body =
28
+ if body_params.empty?
29
+ []
30
+ elsif request.accepts_file_upload?
31
+ file_params = request.params_type.attributes.filter do |name, type|
32
+ type.is_a?(Explicit::Type::File)
33
+ end.to_h
34
+
35
+ non_file_params = body_params.except(*file_params.keys)
36
+
37
+ curl_non_file_params = non_file_params.to_query.split("&").map do |key_value|
38
+ "-F \"#{CGI.unescape(key_value).gsub('"', '\"')}\""
39
+ end
40
+
41
+ curl_file_params = file_params.map do |name, _|
42
+ "-F #{name}=\"#{body_params[name]}\""
43
+ end
44
+
45
+ curl_non_file_params.concat(curl_file_params)
46
+ else
47
+ # https://stackoverflow.com/questions/34847981/curl-with-multiline-of-json
48
+ ["-d @- << EOF\n#{JSON.pretty_generate(body_params)}\nEOF"]
49
+ end
50
+
51
+ [curl_request].concat(curl_headers).concat(curl_body)
52
+ end
53
+ end
5
54
  end
@@ -12,9 +12,7 @@ class Explicit::Request::InvalidParamsError < ::RuntimeError
12
12
 
13
13
  included do
14
14
  rescue_from Explicit::Request::InvalidParamsError do |err|
15
- params = Explicit::Spec::Error.translate(err.errors)
16
-
17
- render json: { error: "invalid_params", params: }, status: 422
15
+ render json: { error: "invalid_params", params: err.errors }, status: 422
18
16
  end
19
17
  end
20
18
  end
@@ -2,10 +2,6 @@
2
2
 
3
3
  class Explicit::Request::InvalidResponseError < ::RuntimeError
4
4
  def initialize(response, error)
5
- error => [:one_of, *errs]
6
- error = errs.map { translate_response_error(response, _1) }.join("\n")
7
-
8
-
9
5
  super <<-TXT
10
6
 
11
7
 
@@ -13,19 +9,10 @@ Got:
13
9
 
14
10
  HTTP #{response.status} #{JSON.pretty_generate(response.data)}
15
11
 
16
- This response doesn't match any spec. Here are the errors:
12
+ This response doesn't match any type. Here are the errors:
17
13
 
18
- #{error.presence || " ==> no response specs found for status #{response.status}"}
14
+ #{error.presence || " ==> no response types found for status #{response.status}"}
19
15
 
20
16
  TXT
21
17
  end
22
-
23
- private
24
- def translate_response_error(response, error)
25
- error = Explicit::Spec::Error.translate(error)
26
-
27
- <<-TXT
28
- HTTP #{response.status} #{JSON.pretty_generate(error)}
29
- TXT
30
- end
31
18
  end
@@ -5,5 +5,23 @@ class Explicit::Request
5
5
  def to_s
6
6
  "#{method.to_s.upcase} #{path}"
7
7
  end
8
+
9
+ def params
10
+ path.split("/").filter_map do |part|
11
+ part[1..-1].to_sym if part.start_with?(":")
12
+ end
13
+ end
14
+
15
+ def replace_path_params(values)
16
+ values.reduce(path) do |acc_path, (key, value)|
17
+ acc_path.gsub(":#{key}", value.to_s)
18
+ end
19
+ end
20
+
21
+ def path_with_curly_syntax
22
+ params.reduce(path) do |acc_path, param|
23
+ acc_path.gsub(":#{param}", "{#{param}}")
24
+ end
25
+ end
8
26
  end
9
27
  end
@@ -13,7 +13,11 @@ class Explicit::Request
13
13
  if Explicit.configuration.rescue_from_invalid_params?
14
14
  @responses[422] << {
15
15
  error: "invalid_params",
16
- params: [:hash, :string, :string]
16
+ params: [
17
+ :description,
18
+ "An object containing error messages for all invalid params",
19
+ [:hash, :string, :string]
20
+ ]
17
21
  }
18
22
  end
19
23
 
@@ -28,8 +32,14 @@ class Explicit::Request
28
32
  subrequest.instance_variable_set(:@routes, @routes.dup)
29
33
  subrequest.instance_variable_set(:@headers, @headers.dup)
30
34
  subrequest.instance_variable_set(:@params, @params.dup)
31
- subrequest.instance_variable_set(:@responses, @responses.dup)
32
- subrequest.instance_variable_set(:@examples, @examples.dup)
35
+
36
+ @responses.each do |status, types|
37
+ subrequest.responses[status] = types.dup
38
+ end
39
+
40
+ @examples.each do |status, examples|
41
+ subrequest.examples[status] = examples.dup
42
+ end
33
43
 
34
44
  subrequest.tap { _1.instance_eval(&block) }
35
45
  end
@@ -54,32 +64,36 @@ class Explicit::Request
54
64
  def description(markdown) = (@description = markdown)
55
65
  def get_description = @description
56
66
 
57
- def header(name, spec)
67
+ def header(name, type)
58
68
  raise ArgumentError("duplicated header #{name}") if @headers.key?(name)
59
69
 
60
- @headers[name] = spec
70
+ @headers[name] = type
61
71
  end
62
72
 
63
- def param(name, spec, **options)
73
+ def param(name, type, **options)
64
74
  raise ArgumentError("duplicated param #{name}") if @params.key?(name)
65
75
 
66
76
  if options[:optional]
67
- spec = [:nilable, spec]
77
+ type = [:nilable, type]
68
78
  end
69
79
 
70
80
  if (defaultval = options[:default])
71
- spec = [:default, defaultval, spec]
81
+ type = [:default, defaultval, type]
72
82
  end
73
83
 
74
84
  if (description = options[:description])
75
- spec = [:description, description, spec]
85
+ type = [:description, description, type]
76
86
  end
77
87
 
78
- @params[name] = spec
88
+ if @routes.first&.params&.include?(name)
89
+ type = [:_param_location, :path, type]
90
+ end
91
+
92
+ @params[name] = type
79
93
  end
80
94
 
81
- def response(status, spec)
82
- @responses[status] << spec
95
+ def response(status, typespec)
96
+ @responses[status] << typespec
83
97
  end
84
98
 
85
99
  def add_example(params:, response:, headers: {})
@@ -90,16 +104,16 @@ class Explicit::Request
90
104
 
91
105
  response = Response.new(status:, data:)
92
106
 
93
- case responses_validator(status:).call(data)
107
+ case responses_type(status:).validate(data)
94
108
  in [:ok, _] then nil
95
109
  in [:error, err] then raise InvalidResponseError.new(response, err)
96
110
  end
97
111
 
98
- @examples[response.status] << Example.new(params:, headers:, response:)
112
+ @examples[response.status] << Example.new(request: self, params:, headers:, response:)
99
113
  end
100
114
 
101
115
  def validate!(values)
102
- case params_validator.call(values)
116
+ case params_type.validate(values)
103
117
  in [:ok, validated_data] then validated_data
104
118
  in [:error, err] then raise InvalidParamsError.new(err)
105
119
  end
@@ -109,16 +123,21 @@ class Explicit::Request
109
123
  routes.first.to_s
110
124
  end
111
125
 
112
- private
113
- def params_validator
114
- @params_validator ||= Explicit::Spec.build(@params)
126
+ def accepts_file_upload?
127
+ params_type.attributes.any? do |name, type|
128
+ type.is_a?(Explicit::Type::File)
115
129
  end
130
+ end
116
131
 
117
- def headers_validator
118
- @headers_validator ||= Explicit::Spec.build(@headers)
119
- end
132
+ def headers_type
133
+ @headers_type ||= Explicit::Type.build(@headers)
134
+ end
120
135
 
121
- def responses_validator(status:)
122
- Explicit::Spec.build([:one_of, *@responses[status]])
123
- end
136
+ def params_type
137
+ @params_type ||= Explicit::Type.build(@params)
138
+ end
139
+
140
+ def responses_type(status:)
141
+ Explicit::Type.build([:one_of, *@responses[status]])
142
+ end
124
143
  end
@@ -25,6 +25,7 @@ class Explicit::TestHelper::ExampleRecorder
25
25
 
26
26
  def add(request_gid:, params:, headers:, response:)
27
27
  @examples[request_gid] << Explicit::Request::Example.new(
28
+ request: nil,
28
29
  params:,
29
30
  headers:,
30
31
  response:
@@ -32,7 +33,11 @@ class Explicit::TestHelper::ExampleRecorder
32
33
  end
33
34
 
34
35
  def save!
35
- total_examples_count = @examples.sum { |_, examples| examples.size }
36
+ examples_hash = @examples.keys.map do |key|
37
+ [key, @examples[key]]
38
+ end.to_h
39
+
40
+ total_examples_count = examples_hash.sum { _2.size }
36
41
  file_path = Explicit.configuration.request_examples_file_path
37
42
 
38
43
  puts "" if Explicit.configuration.test_runner == :rspec
@@ -43,6 +48,6 @@ class Explicit::TestHelper::ExampleRecorder
43
48
  puts " [Explicit] ========="
44
49
  puts "" if Explicit.configuration.test_runner == :minitest
45
50
 
46
- ::File.write(file_path, @examples.to_json, mode: "w")
51
+ ::File.write(file_path, examples_hash.to_json, mode: "w")
47
52
  end
48
53
  end
@@ -19,8 +19,9 @@ module Explicit::TestHelper
19
19
 
20
20
  included do
21
21
  if Explicit.configuration.test_runner == :minitest
22
- # TODO: improve this logic
23
- running_in_parallel = Minitest.parallel_executor.is_a?(ActiveSupport::Testing::ParallelizeExecutor)
22
+ running_in_parallel = Minitest.parallel_executor.is_a?(
23
+ ActiveSupport::Testing::ParallelizeExecutor
24
+ )
24
25
 
25
26
  if running_in_parallel
26
27
  uri = Explicit::TestHelper::ExampleRecorder.start_service
@@ -48,16 +49,21 @@ module Explicit::TestHelper
48
49
  DESC
49
50
  end
50
51
 
51
- process(route.method, route.path, params:, headers:)
52
+ method = route.method
53
+ path = (request.get_base_path || "") + route.path
54
+
55
+ process(method, path, params:, headers:)
52
56
 
53
57
  response = Explicit::Request::Response.new(
54
58
  status: @response.status,
55
59
  data: @response.parsed_body.deep_symbolize_keys
56
60
  )
57
61
 
58
- ensure_response_matches_spec!(request, response)
62
+ ensure_response_matches_documented_type!(request, response)
59
63
 
60
64
  if opts[:save_as_example]
65
+ params = replace_uploaded_files_with_filenames(params)
66
+
61
67
  ExampleRecorder.instance.add(
62
68
  request_gid: request.gid,
63
69
  params:,
@@ -69,12 +75,24 @@ module Explicit::TestHelper
69
75
  response
70
76
  end
71
77
 
72
- def ensure_response_matches_spec!(request, response)
73
- validator = request.send(:responses_validator, status: response.status)
78
+ def ensure_response_matches_documented_type!(request, response)
79
+ responses_type = request.responses_type(status: response.status)
74
80
 
75
- case validator.call(response.data.with_indifferent_access)
81
+ case responses_type.validate(response.data.with_indifferent_access)
76
82
  in [:ok, _] then :all_good
77
83
  in [:error, err] then raise Explicit::Request::InvalidResponseError.new(response, err)
78
84
  end
79
85
  end
86
+
87
+ def replace_uploaded_files_with_filenames(hash)
88
+ hash.to_h do |key, value|
89
+ if Explicit::Type::File::FILE_CLASSES.any? { value.is_a?(_1) }
90
+ [key, "@#{value.original_filename}"]
91
+ elsif value.is_a?(::Hash)
92
+ [key, replace_uploaded_files_with_filenames(value)]
93
+ else
94
+ [key, value]
95
+ end
96
+ end
97
+ end
80
98
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Explicit::Type::Agreement < Explicit::Type
4
+ VALUES = [true, "true", "on", "1", 1].freeze
5
+ OK = [:ok, true].freeze
6
+
7
+ def validate(value)
8
+ if VALUES.include?(value)
9
+ OK
10
+ else
11
+ [:error, error_i18n("agreement")]
12
+ end
13
+ end
14
+
15
+ concerning :Webpage do
16
+ def summary
17
+ "boolean"
18
+ end
19
+
20
+ def partial
21
+ "explicit/documentation/type/agreement"
22
+ end
23
+
24
+ def has_details?
25
+ true
26
+ end
27
+ end
28
+
29
+ concerning :Swagger do
30
+ def swagger_schema
31
+ {
32
+ type: "boolean",
33
+ description: swagger_description([
34
+ swagger_i18n("agreement")
35
+ ])
36
+ }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Explicit::Type::Array < Explicit::Type
4
+ attr_reader :itemtype, :empty
5
+
6
+ def initialize(itemtype:, empty: true)
7
+ @itemtype = Explicit::Type.build(itemtype)
8
+ @empty = empty
9
+ end
10
+
11
+ def validate(values)
12
+ return [:error, :array] if !values.is_a?(::Array)
13
+
14
+ if values.empty? && !empty
15
+ return [:error, :empty]
16
+ end
17
+
18
+ validated = []
19
+
20
+ values.each.with_index do |value, index|
21
+ case itemtype.validate(value)
22
+ in [:ok, value]
23
+ validated << value
24
+ in [:error, error]
25
+ return [:error, error_i18n("array", index:, error:)]
26
+ end
27
+ end
28
+
29
+ [:ok, validated]
30
+ end
31
+
32
+ concerning :Webpage do
33
+ def summary
34
+ "array of #{itemtype.summary}"
35
+ end
36
+
37
+ def partial
38
+ "explicit/documentation/type/array"
39
+ end
40
+
41
+ def has_details?
42
+ true
43
+ end
44
+ end
45
+
46
+ concerning :Swagger do
47
+ def swagger_schema
48
+ {
49
+ type: "array",
50
+ items: itemtype.swagger_schema,
51
+ minItems: empty ? 0 : 1,
52
+ description: swagger_description([])
53
+ }.compact_blank
54
+ end
55
+ end
56
+ end