haveapi 0.29.5 → 0.29.7
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/haveapi.gemspec +1 -1
- data/lib/haveapi/action.rb +5 -1
- data/lib/haveapi/client_examples/http.rb +1 -1
- data/lib/haveapi/client_examples/js_client.rb +6 -4
- data/lib/haveapi/example.rb +175 -0
- data/lib/haveapi/example_list.rb +6 -0
- data/lib/haveapi/server.rb +3 -1
- data/lib/haveapi/version.rb +1 -1
- data/spec/client_examples/syntax_spec.rb +121 -0
- data/spec/documentation/examples_spec.rb +162 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f3555b00c36ae85444baaa5d2da5e69e70343a8d9528e6fee181ed947bf762d6
|
|
4
|
+
data.tar.gz: 19613608cf068c2ec9ed2ecb8eda34fbdfab5da9515708a9786f8648059bfd48
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8658ea7fb6d61350395fa2a3e9a7ac11467d732c27e4b460cb3105e53bbd2f3784f2426bcdc4760c672cce876af1ef111aaba61fa9a2b79be9bbda90fc7954a8
|
|
7
|
+
data.tar.gz: c03998dc485dca010fc82ab5156e463c3c673b2365e8caed997d08393203907addbb86ae8a01faf280be5b0453cd323c165ed6b6bf428a009a4412114a593b6d
|
data/haveapi.gemspec
CHANGED
|
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
|
|
|
15
15
|
s.required_ruby_version = ">= #{File.read('../../.ruby-version').strip}"
|
|
16
16
|
|
|
17
17
|
s.add_dependency 'activesupport', '>= 7.1'
|
|
18
|
-
s.add_dependency 'haveapi-client', '~> 0.29.
|
|
18
|
+
s.add_dependency 'haveapi-client', '~> 0.29.7'
|
|
19
19
|
s.add_dependency 'i18n', '>= 1.6', '< 2'
|
|
20
20
|
s.add_dependency 'json'
|
|
21
21
|
s.add_dependency 'mail'
|
data/lib/haveapi/action.rb
CHANGED
|
@@ -132,7 +132,7 @@ module HaveAPI
|
|
|
132
132
|
@initialized = true
|
|
133
133
|
end
|
|
134
134
|
|
|
135
|
-
def validate_build
|
|
135
|
+
def validate_build(path: nil)
|
|
136
136
|
check_build("#{self}.input") do
|
|
137
137
|
input.validate_build
|
|
138
138
|
end
|
|
@@ -140,6 +140,10 @@ module HaveAPI
|
|
|
140
140
|
check_build("#{self}.output") do
|
|
141
141
|
output.validate_build
|
|
142
142
|
end
|
|
143
|
+
|
|
144
|
+
check_build("#{self}.examples") do
|
|
145
|
+
@examples&.validate_build(self, path:)
|
|
146
|
+
end
|
|
143
147
|
end
|
|
144
148
|
|
|
145
149
|
def model_adapter(layout)
|
|
@@ -101,18 +101,20 @@ module HaveAPI::ClientExamples
|
|
|
101
101
|
|
|
102
102
|
case action[:output][:layout]
|
|
103
103
|
when :hash
|
|
104
|
-
out << "
|
|
105
|
-
out << "
|
|
104
|
+
out << " // reply is an instance of HaveAPI.Client.Response\n"
|
|
105
|
+
out << " // reply.response() returns an object with output parameters:\n"
|
|
106
106
|
out << JSON.pretty_generate(sample[:response] || {}).split("\n").map do |v|
|
|
107
107
|
" // #{v}"
|
|
108
108
|
end.join("\n")
|
|
109
|
+
out << "\n"
|
|
109
110
|
|
|
110
111
|
when :hash_list
|
|
111
|
-
out << "
|
|
112
|
-
out << "
|
|
112
|
+
out << " // reply is an instance of HaveAPI.Client.Response\n"
|
|
113
|
+
out << " // reply.response() returns an array of objects:\n"
|
|
113
114
|
out << JSON.pretty_generate(sample[:response] || []).split("\n").map do |v|
|
|
114
115
|
" // #{v}"
|
|
115
116
|
end.join("\n")
|
|
117
|
+
out << "\n"
|
|
116
118
|
|
|
117
119
|
when :object
|
|
118
120
|
out << " // reply is an instance of HaveAPI.Client.ResourceInstance\n"
|
data/lib/haveapi/example.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
require 'date'
|
|
2
|
+
|
|
1
3
|
module HaveAPI
|
|
2
4
|
class Example
|
|
5
|
+
JSON_SCALARS = [String, Numeric, TrueClass, FalseClass, NilClass].freeze
|
|
6
|
+
|
|
3
7
|
def initialize(title)
|
|
4
8
|
@title = title
|
|
5
9
|
end
|
|
@@ -74,8 +78,179 @@ module HaveAPI
|
|
|
74
78
|
end
|
|
75
79
|
end
|
|
76
80
|
|
|
81
|
+
def validate_build(action, path:, index:)
|
|
82
|
+
return unless provided?
|
|
83
|
+
return unless structured_data_provided?
|
|
84
|
+
|
|
85
|
+
@validation_context = "#{action} example #{example_name(index)}"
|
|
86
|
+
|
|
87
|
+
validate_path_params(action, path) if path
|
|
88
|
+
validate_payload('request', @request, action.input, require_required: true) unless @request.nil?
|
|
89
|
+
validate_payload('response', @response, action.output, require_required: false) unless @response.nil?
|
|
90
|
+
validate_errors(action.input) unless @errors.nil?
|
|
91
|
+
ensure
|
|
92
|
+
@validation_context = nil
|
|
93
|
+
end
|
|
94
|
+
|
|
77
95
|
protected
|
|
78
96
|
|
|
97
|
+
def structured_data_provided?
|
|
98
|
+
%i[
|
|
99
|
+
@path_params @request @response @status @message @errors @http_status
|
|
100
|
+
].any? { |name| !instance_variable_get(name).nil? }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def example_name(index)
|
|
104
|
+
@title.to_s.empty? ? "##{index + 1}" : @title.inspect
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def validate_path_params(action, path)
|
|
108
|
+
expected = action.path_param_names(path).length
|
|
109
|
+
actual = @path_params.nil? ? 0 : @path_params.length
|
|
110
|
+
return if actual == expected
|
|
111
|
+
|
|
112
|
+
invalid!("path_params has #{actual} values, but route #{path.inspect} requires #{expected}")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def validate_payload(name, value, params, require_required:)
|
|
116
|
+
unless params
|
|
117
|
+
invalid!("#{name} is provided, but the action has no #{name == 'request' ? 'input' : 'output'} parameters")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
objects =
|
|
121
|
+
case params.layout
|
|
122
|
+
when :object, :hash
|
|
123
|
+
invalid!("#{name} must be an object") unless value.is_a?(Hash)
|
|
124
|
+
[value]
|
|
125
|
+
when :object_list, :hash_list
|
|
126
|
+
unless value.is_a?(Array) && value.all?(Hash)
|
|
127
|
+
invalid!("#{name} must be a list of objects")
|
|
128
|
+
end
|
|
129
|
+
value
|
|
130
|
+
else
|
|
131
|
+
invalid!("#{name} uses unsupported layout #{params.layout.inspect}")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
objects.each_with_index do |object, index|
|
|
135
|
+
location = objects.length > 1 ? "#{name}[#{index}]" : name
|
|
136
|
+
validate_object(location, object, params, require_required:)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def validate_object(location, object, params, require_required:)
|
|
141
|
+
normalized = normalize_keys(location, object)
|
|
142
|
+
known = params.params.to_h { |param| [param.name.to_sym, param] }
|
|
143
|
+
unknown = normalized.keys - known.keys
|
|
144
|
+
invalid!("#{location} contains unknown parameters: #{unknown.join(', ')}") unless unknown.empty?
|
|
145
|
+
|
|
146
|
+
if require_required
|
|
147
|
+
missing = known.values.filter_map do |param|
|
|
148
|
+
param.name unless !param.required? || normalized.has_key?(param.name.to_sym)
|
|
149
|
+
end
|
|
150
|
+
invalid!("#{location} is missing required parameters: #{missing.join(', ')}") unless missing.empty?
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
normalized.each do |param_name, param_value|
|
|
154
|
+
validate_value("#{location}.#{param_name}", param_value, known.fetch(param_name))
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def normalize_keys(location, object)
|
|
159
|
+
object.each_with_object({}) do |(key, value), ret|
|
|
160
|
+
unless key.is_a?(String) || key.is_a?(Symbol)
|
|
161
|
+
invalid!("#{location} parameter name #{key.inspect} is not a string or symbol")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
normalized = key.to_sym
|
|
165
|
+
invalid!("#{location} repeats parameter #{normalized}") if ret.has_key?(normalized)
|
|
166
|
+
|
|
167
|
+
ret[normalized] = value
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def validate_value(location, value, param)
|
|
172
|
+
if value.nil?
|
|
173
|
+
invalid!("#{location} cannot be null") unless param.nullable?
|
|
174
|
+
return
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
if param.is_a?(HaveAPI::Parameters::Resource)
|
|
178
|
+
validate_resource_value(location, value, param)
|
|
179
|
+
return
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
valid =
|
|
183
|
+
case param.type.to_s
|
|
184
|
+
when 'Integer'
|
|
185
|
+
value.is_a?(Integer)
|
|
186
|
+
when 'Float'
|
|
187
|
+
value.is_a?(Numeric) && value.finite?
|
|
188
|
+
when 'Boolean'
|
|
189
|
+
[true, false].include?(value)
|
|
190
|
+
when 'Datetime'
|
|
191
|
+
valid_datetime?(value)
|
|
192
|
+
when 'String', 'Text'
|
|
193
|
+
value.is_a?(String)
|
|
194
|
+
else
|
|
195
|
+
json_value?(value)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
invalid!("#{location} has invalid #{param.type} value #{value.inspect}") unless valid
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def validate_resource_value(location, value, param)
|
|
202
|
+
return if value.is_a?(Integer) || value.is_a?(String)
|
|
203
|
+
|
|
204
|
+
unless value.is_a?(Hash)
|
|
205
|
+
invalid!("#{location} must be a resource ID or object")
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
normalized = normalize_keys(location, value)
|
|
209
|
+
id_name = param.value_id.to_sym
|
|
210
|
+
invalid!("#{location} resource object is missing #{id_name}") unless normalized.has_key?(id_name)
|
|
211
|
+
invalid!("#{location}.#{id_name} must be an integer or string") unless [
|
|
212
|
+
Integer,
|
|
213
|
+
String
|
|
214
|
+
].any? { |klass| normalized.fetch(id_name).is_a?(klass) }
|
|
215
|
+
invalid!("#{location} is not JSON-compatible") unless json_value?(value)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def valid_datetime?(value)
|
|
219
|
+
return true if value.is_a?(Time) || value.is_a?(DateTime)
|
|
220
|
+
return false unless value.is_a?(String)
|
|
221
|
+
|
|
222
|
+
DateTime.iso8601(value)
|
|
223
|
+
true
|
|
224
|
+
rescue ArgumentError
|
|
225
|
+
false
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def json_value?(value)
|
|
229
|
+
return value.finite? if value.is_a?(Float)
|
|
230
|
+
return true if JSON_SCALARS.any? { |klass| value.is_a?(klass) }
|
|
231
|
+
return value.all? { |item| json_value?(item) } if value.is_a?(Array)
|
|
232
|
+
|
|
233
|
+
if value.is_a?(Hash)
|
|
234
|
+
return value.all? do |key, item|
|
|
235
|
+
(key.is_a?(String) || key.is_a?(Symbol)) && json_value?(item)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
false
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def validate_errors(input)
|
|
243
|
+
invalid!('errors must be an object') unless @errors.is_a?(Hash)
|
|
244
|
+
|
|
245
|
+
known = input ? input.params.map { |param| param.name.to_sym } : []
|
|
246
|
+
unknown = @errors.keys.map(&:to_sym) - known
|
|
247
|
+
invalid!("errors refer to unknown input parameters: #{unknown.join(', ')}") unless unknown.empty?
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def invalid!(message)
|
|
251
|
+
raise "#{@validation_context}: #{message}"
|
|
252
|
+
end
|
|
253
|
+
|
|
79
254
|
def filter_input_params(context, input)
|
|
80
255
|
return nil if input.nil?
|
|
81
256
|
|
data/lib/haveapi/example_list.rb
CHANGED
data/lib/haveapi/server.rb
CHANGED
|
@@ -640,7 +640,9 @@ module HaveAPI
|
|
|
640
640
|
|
|
641
641
|
def validate_resources(resources)
|
|
642
642
|
resources.each_value do |r|
|
|
643
|
-
r[:actions].
|
|
643
|
+
r[:actions].each do |action, path|
|
|
644
|
+
action.validate_build(path:)
|
|
645
|
+
end
|
|
644
646
|
|
|
645
647
|
validate_resources(r[:resources])
|
|
646
648
|
end
|
data/lib/haveapi/version.rb
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'open3'
|
|
6
|
+
require 'tempfile'
|
|
7
|
+
require 'haveapi/client_examples/curl'
|
|
8
|
+
require 'haveapi/client_examples/http'
|
|
9
|
+
require 'haveapi/client_examples/js_client'
|
|
10
|
+
require 'haveapi/client_examples/php_client'
|
|
11
|
+
require 'haveapi/client_examples/ruby_client'
|
|
12
|
+
|
|
13
|
+
describe HaveAPI::ClientExamples do
|
|
14
|
+
let(:action) do
|
|
15
|
+
{
|
|
16
|
+
method: 'POST',
|
|
17
|
+
path: '/v1/widgets/{widget_id}/update',
|
|
18
|
+
input: {
|
|
19
|
+
layout: :hash,
|
|
20
|
+
namespace: :widget,
|
|
21
|
+
parameters: {
|
|
22
|
+
name: { type: 'String' },
|
|
23
|
+
enabled: { type: 'Boolean' }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
output: {
|
|
27
|
+
layout: :hash,
|
|
28
|
+
namespace: :widget,
|
|
29
|
+
parameters: {
|
|
30
|
+
id: { type: 'Integer' },
|
|
31
|
+
name: { type: 'String' }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
let(:sample) do
|
|
37
|
+
{
|
|
38
|
+
path_params: [101],
|
|
39
|
+
request: { name: 'documented widget', enabled: true },
|
|
40
|
+
response: { id: 101, name: 'documented widget' },
|
|
41
|
+
status: true,
|
|
42
|
+
message: nil,
|
|
43
|
+
errors: nil,
|
|
44
|
+
http_status: 200
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
let(:client_args) do
|
|
48
|
+
[
|
|
49
|
+
'api.example.test',
|
|
50
|
+
'https://api.example.test',
|
|
51
|
+
1,
|
|
52
|
+
%w[widget],
|
|
53
|
+
{},
|
|
54
|
+
'update',
|
|
55
|
+
action
|
|
56
|
+
]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def syntax_check(command, source, suffix)
|
|
60
|
+
Tempfile.create(['haveapi-example-', suffix]) do |file|
|
|
61
|
+
file.write(source)
|
|
62
|
+
file.flush
|
|
63
|
+
output, status = Open3.capture2e(*command, file.path)
|
|
64
|
+
[status, output]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'generates valid JavaScript' do
|
|
69
|
+
source = HaveAPI::ClientExamples::JsClient.new(*client_args).example(sample)
|
|
70
|
+
status, output = syntax_check(%w[node --check], source, '.mjs')
|
|
71
|
+
|
|
72
|
+
expect(status).to be_success, output
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'generates valid PHP' do
|
|
76
|
+
source = HaveAPI::ClientExamples::PhpClient.new(*client_args).example(sample)
|
|
77
|
+
status, output = syntax_check(%w[php -l], "<?php\n#{source}", '.php')
|
|
78
|
+
|
|
79
|
+
expect(status).to be_success, output
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'generates valid Ruby' do
|
|
83
|
+
source = HaveAPI::ClientExamples::RubyClient.new(*client_args).example(sample)
|
|
84
|
+
status, output = syntax_check(%w[ruby -c], source, '.rb')
|
|
85
|
+
|
|
86
|
+
expect(status).to be_success, output
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'generates shell-parseable curl requests' do
|
|
90
|
+
source = HaveAPI::ClientExamples::Curl.new(*client_args).request(sample)
|
|
91
|
+
status, output = syntax_check(%w[bash -n], source, '.sh')
|
|
92
|
+
|
|
93
|
+
expect(status).to be_success, output
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'generates structurally valid HTTP requests and responses' do
|
|
97
|
+
generator = HaveAPI::ClientExamples::Http.new(*client_args)
|
|
98
|
+
request_headers, request_body = generator.request(sample).split("\n\n", 2)
|
|
99
|
+
request_lines = request_headers.lines(chomp: true)
|
|
100
|
+
|
|
101
|
+
expect(request_lines.shift).to eq('POST /v1/widgets/101/update HTTP/1.1')
|
|
102
|
+
expect(request_lines).to include('Host: api.example.test', 'Content-Type: application/json')
|
|
103
|
+
expect(JSON.parse(request_body)).to eq(
|
|
104
|
+
'widget' => {
|
|
105
|
+
'name' => 'documented widget',
|
|
106
|
+
'enabled' => true
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
response_headers, response_body = generator.response(sample).split("\n\n", 2)
|
|
111
|
+
response_lines = response_headers.lines(chomp: true)
|
|
112
|
+
expect(response_lines.shift).to eq('HTTP/1.1 200 OK')
|
|
113
|
+
content_length = response_lines
|
|
114
|
+
.find { |line| line.start_with?('Content-Length: ') }
|
|
115
|
+
.split(': ', 2)
|
|
116
|
+
.last
|
|
117
|
+
.to_i
|
|
118
|
+
expect(content_length).to eq(response_body.bytesize)
|
|
119
|
+
expect(JSON.parse(response_body)).to include('status' => true)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -56,6 +56,43 @@ describe HaveAPI::Example do
|
|
|
56
56
|
{ name: 'alpha' }
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
|
+
|
|
60
|
+
define_action(:ValidatedExample) do
|
|
61
|
+
route 'validated/{widget_id}'
|
|
62
|
+
http_method :post
|
|
63
|
+
authorize { allow }
|
|
64
|
+
|
|
65
|
+
input(:hash) do
|
|
66
|
+
string :name, required: true
|
|
67
|
+
integer :count
|
|
68
|
+
bool :active
|
|
69
|
+
datetime :created_at
|
|
70
|
+
custom :metadata
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
output(:hash) do
|
|
74
|
+
integer :count
|
|
75
|
+
bool :accepted
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# rubocop:disable RSpec/NoExpectationExample
|
|
79
|
+
example 'valid structured data' do
|
|
80
|
+
path_params 101
|
|
81
|
+
request({
|
|
82
|
+
name: 'alpha',
|
|
83
|
+
count: 2,
|
|
84
|
+
active: true,
|
|
85
|
+
created_at: '2026-07-26T10:30:00Z',
|
|
86
|
+
metadata: { source: 'documentation' }
|
|
87
|
+
})
|
|
88
|
+
response({ count: 2 })
|
|
89
|
+
end
|
|
90
|
+
# rubocop:enable RSpec/NoExpectationExample
|
|
91
|
+
|
|
92
|
+
def exec
|
|
93
|
+
{ count: input[:count], accepted: true }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
59
96
|
end
|
|
60
97
|
end
|
|
61
98
|
|
|
@@ -94,4 +131,129 @@ describe HaveAPI::Example do
|
|
|
94
131
|
expect(api_response[:examples].first[:request]).to be_nil
|
|
95
132
|
expect(api_response[:examples].first[:response]).to be_nil
|
|
96
133
|
end
|
|
134
|
+
|
|
135
|
+
describe 'build validation' do
|
|
136
|
+
def example_for(action, path:, title: 'invalid example', &)
|
|
137
|
+
example = HaveAPI::Example.new(title)
|
|
138
|
+
example.instance_eval(&)
|
|
139
|
+
example.validate_build(action, path:, index: 0)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def validated_example_action
|
|
143
|
+
self.class.const_get(:ApiModule)::Widget::ValidatedExample
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'accepts valid structured and prose-only examples' do
|
|
147
|
+
expect do
|
|
148
|
+
example_for(
|
|
149
|
+
validated_example_action,
|
|
150
|
+
path: '/v1/widgets/validated/{widget_id}',
|
|
151
|
+
title: 'valid'
|
|
152
|
+
) do
|
|
153
|
+
path_params 101
|
|
154
|
+
request(name: 'alpha', count: 2, active: false)
|
|
155
|
+
response(count: 2)
|
|
156
|
+
end
|
|
157
|
+
end.not_to raise_error
|
|
158
|
+
|
|
159
|
+
expect do
|
|
160
|
+
example_for(validated_example_action, path: '/v1/widgets/validated/{widget_id}') do
|
|
161
|
+
comment 'The prose describes a request that is not represented as structured data.'
|
|
162
|
+
end
|
|
163
|
+
end.not_to raise_error
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'rejects path parameter count and request layout errors' do
|
|
167
|
+
expect do
|
|
168
|
+
example_for(validated_example_action, path: '/v1/widgets/validated/{widget_id}') do
|
|
169
|
+
request([{ name: 'alpha' }])
|
|
170
|
+
end
|
|
171
|
+
end.to raise_error(/path_params has 0 values.*requires 1/)
|
|
172
|
+
|
|
173
|
+
expect do
|
|
174
|
+
example_for(validated_example_action, path: '/v1/widgets/validated') do
|
|
175
|
+
request([{ name: 'alpha' }])
|
|
176
|
+
end
|
|
177
|
+
end.to raise_error(/request must be an object/)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it 'rejects unknown, missing, and incorrectly typed request parameters' do
|
|
181
|
+
expect do
|
|
182
|
+
example_for(validated_example_action, path: '/v1/widgets/validated/{widget_id}') do
|
|
183
|
+
path_params 101
|
|
184
|
+
request(name: 'alpha', mystery: true)
|
|
185
|
+
end
|
|
186
|
+
end.to raise_error(/unknown parameters: mystery/)
|
|
187
|
+
|
|
188
|
+
expect do
|
|
189
|
+
example_for(validated_example_action, path: '/v1/widgets/validated/{widget_id}') do
|
|
190
|
+
path_params 101
|
|
191
|
+
request(count: 2)
|
|
192
|
+
end
|
|
193
|
+
end.to raise_error(/missing required parameters: name/)
|
|
194
|
+
|
|
195
|
+
expect do
|
|
196
|
+
example_for(validated_example_action, path: '/v1/widgets/validated/{widget_id}') do
|
|
197
|
+
path_params 101
|
|
198
|
+
request(name: 'alpha', count: 'two')
|
|
199
|
+
end
|
|
200
|
+
end.to raise_error(/request.count has invalid Integer value/)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it 'allows abbreviated responses but validates present fields and error keys' do
|
|
204
|
+
expect do
|
|
205
|
+
example_for(validated_example_action, path: '/v1/widgets/validated/{widget_id}') do
|
|
206
|
+
path_params 101
|
|
207
|
+
request(name: 'alpha')
|
|
208
|
+
response(count: 1)
|
|
209
|
+
end
|
|
210
|
+
end.not_to raise_error
|
|
211
|
+
|
|
212
|
+
expect do
|
|
213
|
+
example_for(validated_example_action, path: '/v1/widgets/validated/{widget_id}') do
|
|
214
|
+
path_params 101
|
|
215
|
+
request(name: 'alpha')
|
|
216
|
+
response(accepted: 'yes')
|
|
217
|
+
end
|
|
218
|
+
end.to raise_error(/response.accepted has invalid Boolean value/)
|
|
219
|
+
|
|
220
|
+
expect do
|
|
221
|
+
example_for(validated_example_action, path: '/v1/widgets/validated/{widget_id}') do
|
|
222
|
+
path_params 101
|
|
223
|
+
request(name: 'alpha')
|
|
224
|
+
status false
|
|
225
|
+
errors missing: ['is invalid']
|
|
226
|
+
end
|
|
227
|
+
end.to raise_error(/errors refer to unknown input parameters: missing/)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
it 'accepts resource IDs and rejects malformed resource objects' do
|
|
231
|
+
output = HaveAPI::Params.new(:output, validated_example_action)
|
|
232
|
+
output.add_block(proc do
|
|
233
|
+
resource HaveAPI::Resources::ActionState, name: :state
|
|
234
|
+
end)
|
|
235
|
+
output.exec
|
|
236
|
+
action = Struct.new(:input, :output) do
|
|
237
|
+
def path_param_names(_path)
|
|
238
|
+
[]
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def to_s
|
|
242
|
+
'ResourceExampleAction'
|
|
243
|
+
end
|
|
244
|
+
end.new(nil, output)
|
|
245
|
+
|
|
246
|
+
expect do
|
|
247
|
+
example_for(action, path: '/v1/resource-example') do
|
|
248
|
+
response(state: 101)
|
|
249
|
+
end
|
|
250
|
+
end.not_to raise_error
|
|
251
|
+
|
|
252
|
+
expect do
|
|
253
|
+
example_for(action, path: '/v1/resource-example') do
|
|
254
|
+
response(state: { label: 'missing ID' })
|
|
255
|
+
end
|
|
256
|
+
end.to raise_error(/resource object is missing id/)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
97
259
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: haveapi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.29.
|
|
4
|
+
version: 0.29.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jakub Skokan
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.29.
|
|
32
|
+
version: 0.29.7
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.29.
|
|
39
|
+
version: 0.29.7
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: i18n
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -325,6 +325,7 @@ files:
|
|
|
325
325
|
- spec/authentication/token_spec.rb
|
|
326
326
|
- spec/authentication/token_version_routes_spec.rb
|
|
327
327
|
- spec/authorization_spec.rb
|
|
328
|
+
- spec/client_examples/syntax_spec.rb
|
|
328
329
|
- spec/common_spec.rb
|
|
329
330
|
- spec/documentation/auth_filtering_spec.rb
|
|
330
331
|
- spec/documentation/current_user_html_escaping_spec.rb
|