haveapi 0.28.4 → 0.29.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/README.md +139 -0
- data/Rakefile +1 -0
- data/haveapi.gemspec +2 -1
- data/lib/haveapi/action.rb +26 -9
- data/lib/haveapi/actions/default.rb +5 -2
- data/lib/haveapi/actions/paginable.rb +8 -4
- data/lib/haveapi/authentication/oauth2/provider.rb +1 -1
- data/lib/haveapi/authentication/token/config.rb +10 -3
- data/lib/haveapi/authentication/token/provider.rb +22 -21
- data/lib/haveapi/context.rb +4 -1
- data/lib/haveapi/i18n.rb +125 -0
- data/lib/haveapi/locales/cs.yml +167 -0
- data/lib/haveapi/locales/en.yml +168 -0
- data/lib/haveapi/metadata.rb +25 -3
- data/lib/haveapi/model_adapters/active_record.rb +40 -26
- data/lib/haveapi/output_formatter.rb +2 -2
- data/lib/haveapi/parameters/metadata_i18n.rb +179 -0
- data/lib/haveapi/parameters/resource.rb +18 -7
- data/lib/haveapi/parameters/typed.rb +27 -20
- data/lib/haveapi/params.rb +76 -7
- data/lib/haveapi/resource.rb +1 -1
- data/lib/haveapi/resources/action_state.rb +47 -27
- data/lib/haveapi/server.rb +156 -16
- data/lib/haveapi/spec/api_builder.rb +25 -0
- data/lib/haveapi/spec/spec_methods.rb +10 -0
- data/lib/haveapi/tasks/i18n.rb +198 -0
- data/lib/haveapi/validator_chain.rb +1 -1
- data/lib/haveapi/validators/acceptance.rb +5 -2
- data/lib/haveapi/validators/confirmation.rb +5 -2
- data/lib/haveapi/validators/exclusion.rb +2 -2
- data/lib/haveapi/validators/format.rb +5 -2
- data/lib/haveapi/validators/inclusion.rb +2 -2
- data/lib/haveapi/validators/length.rb +5 -5
- data/lib/haveapi/validators/numericality.rb +20 -22
- data/lib/haveapi/validators/presence.rb +4 -2
- data/lib/haveapi/version.rb +1 -1
- data/lib/haveapi.rb +1 -0
- data/spec/authentication/oauth2_spec.rb +10 -0
- data/spec/i18n_spec.rb +520 -0
- data/spec/params_spec.rb +183 -0
- metadata +29 -3
|
@@ -32,49 +32,47 @@ module HaveAPI
|
|
|
32
32
|
@even = take(:even)
|
|
33
33
|
@odd = take(:odd)
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
end
|
|
35
|
+
requirements = []
|
|
36
|
+
|
|
37
|
+
requirements << if @min && !@max
|
|
38
|
+
HaveAPI.message('haveapi.validators.numericality.min', min: @min)
|
|
39
|
+
elsif !@min && @max
|
|
40
|
+
HaveAPI.message('haveapi.validators.numericality.max', max: @max)
|
|
41
|
+
elsif @min && @max
|
|
42
|
+
HaveAPI.message('haveapi.validators.numericality.range', min: @min, max: @max)
|
|
43
|
+
else
|
|
44
|
+
HaveAPI.message('haveapi.validators.numericality.number')
|
|
45
|
+
end
|
|
47
46
|
|
|
48
47
|
if @step
|
|
49
|
-
|
|
50
|
-
msg += "in steps of #{@step}"
|
|
48
|
+
requirements << HaveAPI.message('haveapi.validators.numericality.step', step: @step)
|
|
51
49
|
end
|
|
52
50
|
|
|
53
51
|
if @mod
|
|
54
|
-
|
|
55
|
-
msg += "mod #{@step} must equal zero"
|
|
52
|
+
requirements << HaveAPI.message('haveapi.validators.numericality.mod', mod: @mod)
|
|
56
53
|
end
|
|
57
54
|
|
|
58
55
|
if @odd
|
|
59
|
-
|
|
60
|
-
msg += 'odd'
|
|
56
|
+
requirements << HaveAPI.message('haveapi.validators.numericality.odd')
|
|
61
57
|
end
|
|
62
58
|
|
|
63
59
|
if @even
|
|
64
|
-
|
|
65
|
-
msg += 'even'
|
|
60
|
+
requirements << HaveAPI.message('haveapi.validators.numericality.even')
|
|
66
61
|
end
|
|
67
62
|
|
|
68
63
|
if @odd && @even
|
|
69
64
|
raise 'cannot be both odd and even at the same time'
|
|
70
65
|
end
|
|
71
66
|
|
|
72
|
-
@message = take(
|
|
67
|
+
@message = take(
|
|
68
|
+
:message,
|
|
69
|
+
HaveAPI.message('haveapi.validators.numericality.composite', requirements:)
|
|
70
|
+
)
|
|
73
71
|
end
|
|
74
72
|
|
|
75
73
|
def describe
|
|
76
74
|
ret = {
|
|
77
|
-
message: @message
|
|
75
|
+
message: HaveAPI.localize(@message)
|
|
78
76
|
}
|
|
79
77
|
|
|
80
78
|
ret[:min] = @min if @min
|
|
@@ -21,14 +21,16 @@ module HaveAPI
|
|
|
21
21
|
@empty = take(:empty, false)
|
|
22
22
|
@message = take(
|
|
23
23
|
:message,
|
|
24
|
-
|
|
24
|
+
HaveAPI.message(
|
|
25
|
+
@empty ? 'haveapi.validators.presence.present' : 'haveapi.validators.presence.non_empty'
|
|
26
|
+
)
|
|
25
27
|
)
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def describe
|
|
29
31
|
{
|
|
30
32
|
empty: @empty,
|
|
31
|
-
message: @message
|
|
33
|
+
message: HaveAPI.localize(@message)
|
|
32
34
|
}
|
|
33
35
|
end
|
|
34
36
|
|
data/lib/haveapi/version.rb
CHANGED
data/lib/haveapi.rb
CHANGED
|
@@ -218,6 +218,16 @@ describe HaveAPI::Authentication::OAuth2 do
|
|
|
218
218
|
expect(api_response.message).to match(/too many|multiple/i)
|
|
219
219
|
end
|
|
220
220
|
|
|
221
|
+
it 'localizes multiple token conflicts' do
|
|
222
|
+
header 'Accept-Language', 'cs'
|
|
223
|
+
header 'Authorization', 'Bearer abc'
|
|
224
|
+
call_api(:post, '/v1/secures/ping?access_token=abc', {})
|
|
225
|
+
|
|
226
|
+
expect(last_response.status).to eq(400)
|
|
227
|
+
expect(api_response).to be_failed
|
|
228
|
+
expect(api_response.message).to eq('Bylo poskytnuto více OAuth2 tokenů')
|
|
229
|
+
end
|
|
230
|
+
|
|
221
231
|
it 'ignores structured access_token query values before backend lookup' do
|
|
222
232
|
expect do
|
|
223
233
|
call_api(:post, '/v1/secures/ping?access_token[]=abc', {})
|
data/spec/i18n_spec.rb
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module I18nSpec
|
|
4
|
+
User = Struct.new(:language)
|
|
5
|
+
|
|
6
|
+
class Provider < HaveAPI::Authentication::Basic::Provider
|
|
7
|
+
protected
|
|
8
|
+
|
|
9
|
+
def find_user(_request, username, password)
|
|
10
|
+
User.new(:cs) if username == 'user' && password == 'pass'
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe HaveAPI::I18n do
|
|
16
|
+
context 'with translated API responses' do
|
|
17
|
+
api do
|
|
18
|
+
define_resource(:Thing) do
|
|
19
|
+
version 1
|
|
20
|
+
auth false
|
|
21
|
+
desc HaveAPI.message('i18n_spec.resources.thing.desc')
|
|
22
|
+
|
|
23
|
+
define_action(:Create) do
|
|
24
|
+
route ''
|
|
25
|
+
http_method :post
|
|
26
|
+
desc HaveAPI.message('i18n_spec.resources.thing.actions.create.desc')
|
|
27
|
+
|
|
28
|
+
input do
|
|
29
|
+
string :name,
|
|
30
|
+
label: HaveAPI.message('i18n_spec.resources.thing.params.name.label'),
|
|
31
|
+
desc: HaveAPI.message('i18n_spec.resources.thing.params.name.desc'),
|
|
32
|
+
required: true,
|
|
33
|
+
length: { min: 3, max: 5 }
|
|
34
|
+
string :auto_label,
|
|
35
|
+
label: 'Automatic label fallback',
|
|
36
|
+
desc: 'Automatic description fallback'
|
|
37
|
+
string :explicit_key,
|
|
38
|
+
label: 'Explicit label fallback',
|
|
39
|
+
desc: 'Explicit description fallback',
|
|
40
|
+
label_key: 'i18n_spec.shared.explicit_key.label',
|
|
41
|
+
desc_key: 'i18n_spec.shared.explicit_key.description'
|
|
42
|
+
string :shared_label,
|
|
43
|
+
label: 'Shared label fallback',
|
|
44
|
+
desc: 'Shared description fallback'
|
|
45
|
+
string :resource_attr_label,
|
|
46
|
+
label: 'Resource attribute fallback',
|
|
47
|
+
desc: 'Resource attribute description fallback'
|
|
48
|
+
string :global_attr_label,
|
|
49
|
+
label: 'Global attribute fallback',
|
|
50
|
+
desc: 'Global attribute description fallback'
|
|
51
|
+
string :code, length: {
|
|
52
|
+
min: 3,
|
|
53
|
+
message: HaveAPI.message('haveapi.validators.length.min', min: 3)
|
|
54
|
+
}
|
|
55
|
+
integer :count
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
output do
|
|
59
|
+
bool :ok,
|
|
60
|
+
label: 'OK fallback',
|
|
61
|
+
desc: 'Result description fallback'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
meta(:global) do
|
|
65
|
+
input do
|
|
66
|
+
bool :confirm,
|
|
67
|
+
label: 'Confirm fallback',
|
|
68
|
+
desc: 'Confirm description fallback'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
output do
|
|
72
|
+
bool :audited,
|
|
73
|
+
label: 'Audited fallback',
|
|
74
|
+
desc: 'Audited description fallback'
|
|
75
|
+
bool :global_audited,
|
|
76
|
+
label: 'Global audited fallback',
|
|
77
|
+
desc: 'Global audited description fallback'
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
authorize { allow }
|
|
82
|
+
|
|
83
|
+
def exec
|
|
84
|
+
{ ok: true }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
define_action(:CustomError) do
|
|
89
|
+
route 'custom_error'
|
|
90
|
+
http_method :get
|
|
91
|
+
|
|
92
|
+
authorize { allow }
|
|
93
|
+
|
|
94
|
+
def exec
|
|
95
|
+
error!('plain custom error')
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
parameter_i18n_scope 'i18n_spec'
|
|
102
|
+
default_version 1
|
|
103
|
+
|
|
104
|
+
it 'keeps default English responses unchanged' do
|
|
105
|
+
header 'Accept', 'application/json'
|
|
106
|
+
call_api(:post, '/v1/things', thing: {})
|
|
107
|
+
|
|
108
|
+
expect(api_response.message).to eq('input parameters not valid')
|
|
109
|
+
expect(api_response.errors[:name]).to include('required parameter missing')
|
|
110
|
+
expect(last_response.headers['Vary']).to include('Accept-Language')
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'localizes validation messages using Accept-Language' do
|
|
114
|
+
header 'Accept', 'application/json'
|
|
115
|
+
header 'Accept-Language', 'cs'
|
|
116
|
+
call_api(:post, '/v1/things', thing: { count: 'nope' })
|
|
117
|
+
|
|
118
|
+
expect(api_response.message).to eq('vstupní parametry nejsou platné')
|
|
119
|
+
expect(api_response.errors[:name]).to include('povinný parametr chybí')
|
|
120
|
+
expect(api_response.errors[:count].first).to include('neplatné celé číslo')
|
|
121
|
+
expect(last_response.headers['Vary']).to include('Accept-Language')
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'normalizes regional language tags' do
|
|
125
|
+
header 'Accept', 'application/json'
|
|
126
|
+
header 'Accept-Language', 'cs-CZ,cs;q=0.9,en;q=0.5'
|
|
127
|
+
get '/unknown_resource'
|
|
128
|
+
|
|
129
|
+
expect(api_response.message).to eq('Akce nebyla nalezena')
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'falls back to English for unsupported languages' do
|
|
133
|
+
header 'Accept', 'application/json'
|
|
134
|
+
header 'Accept-Language', 'de'
|
|
135
|
+
get '/unknown_resource'
|
|
136
|
+
|
|
137
|
+
expect(api_response.message).to eq('Action not found')
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'localizes validator descriptions in OPTIONS responses' do
|
|
141
|
+
header 'Accept', 'application/json'
|
|
142
|
+
header 'Accept-Language', 'cs'
|
|
143
|
+
options '/v1/things', method: 'POST'
|
|
144
|
+
|
|
145
|
+
length = api_response[:input][:parameters][:name][:validators][:length]
|
|
146
|
+
expect(length[:message]).to eq('délka musí být v rozsahu <3, 5>')
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'localizes API metadata in OPTIONS responses' do
|
|
150
|
+
previous_available = ::I18n.available_locales
|
|
151
|
+
::I18n.available_locales = (previous_available + %i[en cs]).uniq
|
|
152
|
+
::I18n.backend.store_translations(
|
|
153
|
+
:en,
|
|
154
|
+
i18n_spec: {
|
|
155
|
+
resources: {
|
|
156
|
+
thing: {
|
|
157
|
+
desc: 'Manage things',
|
|
158
|
+
actions: { create: { desc: 'Create a thing' } },
|
|
159
|
+
params: {
|
|
160
|
+
name: {
|
|
161
|
+
label: 'Name',
|
|
162
|
+
desc: 'Thing name'
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
::I18n.backend.store_translations(
|
|
170
|
+
:cs,
|
|
171
|
+
i18n_spec: {
|
|
172
|
+
resources: {
|
|
173
|
+
thing: {
|
|
174
|
+
desc: 'Spravovat věci',
|
|
175
|
+
actions: { create: { desc: 'Vytvořit věc' } },
|
|
176
|
+
params: {
|
|
177
|
+
name: {
|
|
178
|
+
label: 'Název',
|
|
179
|
+
desc: 'Název věci'
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
header 'Accept', 'application/json'
|
|
188
|
+
header 'Accept-Language', 'cs'
|
|
189
|
+
call_api(:options, '/?describe=default')
|
|
190
|
+
|
|
191
|
+
resource = api_response[:resources][:thing]
|
|
192
|
+
action = resource[:actions][:create]
|
|
193
|
+
param = action[:input][:parameters][:name]
|
|
194
|
+
|
|
195
|
+
expect(resource[:description]).to eq('Spravovat věci')
|
|
196
|
+
expect(action[:description]).to eq('Vytvořit věc')
|
|
197
|
+
expect(param[:label]).to eq('Název')
|
|
198
|
+
expect(param[:description]).to eq('Název věci')
|
|
199
|
+
ensure
|
|
200
|
+
::I18n.available_locales = previous_available
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it 'localizes action parameter metadata from the server parameter scope' do
|
|
204
|
+
previous_available = ::I18n.available_locales
|
|
205
|
+
::I18n.available_locales = (previous_available + %i[en cs]).uniq
|
|
206
|
+
::I18n.backend.store_translations(
|
|
207
|
+
:cs,
|
|
208
|
+
i18n_spec: {
|
|
209
|
+
resources: {
|
|
210
|
+
thing: {
|
|
211
|
+
actions: {
|
|
212
|
+
create: {
|
|
213
|
+
input: {
|
|
214
|
+
auto_label: {
|
|
215
|
+
label: 'Automatický popisek',
|
|
216
|
+
description: 'Automatický popis'
|
|
217
|
+
},
|
|
218
|
+
global_attr_label: {
|
|
219
|
+
label: 'Přesný globální popisek',
|
|
220
|
+
description: 'Přesný globální popis'
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
output: {
|
|
224
|
+
ok: {
|
|
225
|
+
label: 'V pořádku',
|
|
226
|
+
description: 'Popis výsledku'
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
meta: {
|
|
230
|
+
global: {
|
|
231
|
+
input: {
|
|
232
|
+
confirm: {
|
|
233
|
+
label: 'Potvrdit',
|
|
234
|
+
description: 'Popis potvrzení'
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
input: {
|
|
242
|
+
shared_label: {
|
|
243
|
+
label: 'Sdílený popisek',
|
|
244
|
+
description: 'Sdílený popis'
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
attributes: {
|
|
248
|
+
resource_attr_label: {
|
|
249
|
+
label: 'Atribut zdroje',
|
|
250
|
+
description: 'Popis atributu zdroje'
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
meta: {
|
|
254
|
+
global: {
|
|
255
|
+
output: {
|
|
256
|
+
audited: {
|
|
257
|
+
label: 'Auditováno',
|
|
258
|
+
description: 'Popis auditu'
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
attributes: {
|
|
266
|
+
auto_label: {
|
|
267
|
+
label: 'Sdílený automatický popisek',
|
|
268
|
+
description: 'Sdílený automatický popis'
|
|
269
|
+
},
|
|
270
|
+
global_attr_label: {
|
|
271
|
+
label: 'Globální atribut',
|
|
272
|
+
description: 'Popis globálního atributu'
|
|
273
|
+
},
|
|
274
|
+
shared_label: {
|
|
275
|
+
label: 'Sdílený popisek atributu',
|
|
276
|
+
description: 'Sdílený popis atributu'
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
meta: {
|
|
280
|
+
global: {
|
|
281
|
+
output: {
|
|
282
|
+
global_audited: {
|
|
283
|
+
label: 'Globálně auditováno',
|
|
284
|
+
description: 'Globální popis auditu'
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
shared: {
|
|
290
|
+
explicit_key: {
|
|
291
|
+
label: 'Explicitní popisek',
|
|
292
|
+
description: 'Explicitní popis'
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
header 'Accept', 'application/json'
|
|
299
|
+
header 'Accept-Language', 'cs'
|
|
300
|
+
call_api(:options, '/?describe=default')
|
|
301
|
+
|
|
302
|
+
create = api_response[:resources][:thing][:actions][:create]
|
|
303
|
+
input_params = create[:input][:parameters]
|
|
304
|
+
output_params = create[:output][:parameters]
|
|
305
|
+
meta_input_params = create[:meta][:global][:input][:parameters]
|
|
306
|
+
meta_output_params = create[:meta][:global][:output][:parameters]
|
|
307
|
+
|
|
308
|
+
expect(input_params[:auto_label]).to include(
|
|
309
|
+
label: 'Automatický popisek',
|
|
310
|
+
description: 'Automatický popis'
|
|
311
|
+
)
|
|
312
|
+
expect(input_params[:explicit_key]).to include(
|
|
313
|
+
label: 'Explicitní popisek',
|
|
314
|
+
description: 'Explicitní popis'
|
|
315
|
+
)
|
|
316
|
+
expect(input_params[:shared_label]).to include(
|
|
317
|
+
label: 'Sdílený popisek',
|
|
318
|
+
description: 'Sdílený popis'
|
|
319
|
+
)
|
|
320
|
+
expect(input_params[:resource_attr_label]).to include(
|
|
321
|
+
label: 'Atribut zdroje',
|
|
322
|
+
description: 'Popis atributu zdroje'
|
|
323
|
+
)
|
|
324
|
+
expect(input_params[:global_attr_label]).to include(
|
|
325
|
+
label: 'Přesný globální popisek',
|
|
326
|
+
description: 'Přesný globální popis'
|
|
327
|
+
)
|
|
328
|
+
expect(input_params[:count]).to include(
|
|
329
|
+
label: 'Count',
|
|
330
|
+
description: nil
|
|
331
|
+
)
|
|
332
|
+
expect(output_params[:ok]).to include(
|
|
333
|
+
label: 'V pořádku',
|
|
334
|
+
description: 'Popis výsledku'
|
|
335
|
+
)
|
|
336
|
+
expect(meta_input_params[:confirm]).to include(
|
|
337
|
+
label: 'Potvrdit',
|
|
338
|
+
description: 'Popis potvrzení'
|
|
339
|
+
)
|
|
340
|
+
expect(meta_output_params[:audited]).to include(
|
|
341
|
+
label: 'Auditováno',
|
|
342
|
+
description: 'Popis auditu'
|
|
343
|
+
)
|
|
344
|
+
expect(meta_output_params[:global_audited]).to include(
|
|
345
|
+
label: 'Globálně auditováno',
|
|
346
|
+
description: 'Globální popis auditu'
|
|
347
|
+
)
|
|
348
|
+
ensure
|
|
349
|
+
::I18n.available_locales = previous_available
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
it 'localizes framework action parameter metadata in OPTIONS responses' do
|
|
353
|
+
header 'Accept', 'application/json'
|
|
354
|
+
header 'Accept-Language', 'cs'
|
|
355
|
+
call_api(:options, '/?describe=default')
|
|
356
|
+
|
|
357
|
+
create = api_response[:resources][:thing][:actions][:create]
|
|
358
|
+
no_meta = create[:meta][:global][:input][:parameters][:no]
|
|
359
|
+
|
|
360
|
+
expect(no_meta[:label]).to eq('Zakázat metadata')
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
it 'localizes application-supplied lazy validator messages' do
|
|
364
|
+
header 'Accept', 'application/json'
|
|
365
|
+
header 'Accept-Language', 'cs'
|
|
366
|
+
call_api(:post, '/v1/things', thing: { name: 'abc', code: 'x' })
|
|
367
|
+
|
|
368
|
+
expect(api_response.errors[:code]).to include('délka musí být alespoň 3')
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
it 'leaves custom string errors unchanged' do
|
|
372
|
+
header 'Accept', 'application/json'
|
|
373
|
+
header 'Accept-Language', 'cs'
|
|
374
|
+
get '/v1/things/custom_error'
|
|
375
|
+
|
|
376
|
+
expect(api_response.message).to eq('plain custom error')
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
it 'works when host applications constrain global I18n locales' do
|
|
380
|
+
previous_available = ::I18n.available_locales
|
|
381
|
+
previous_locale = ::I18n.locale
|
|
382
|
+
::I18n.available_locales = [:en]
|
|
383
|
+
|
|
384
|
+
header 'Accept', 'application/json'
|
|
385
|
+
header 'Accept-Language', 'cs'
|
|
386
|
+
get '/unknown_resource'
|
|
387
|
+
|
|
388
|
+
expect(api_response.message).to eq('Akce nebyla nalezena')
|
|
389
|
+
ensure
|
|
390
|
+
::I18n.available_locales = (Array(previous_available) + [previous_locale]).uniq
|
|
391
|
+
::I18n.locale = previous_locale
|
|
392
|
+
::I18n.available_locales = previous_available
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
it 'restores the ambient I18n locale after each request' do
|
|
396
|
+
previous_available = ::I18n.available_locales
|
|
397
|
+
previous_locale = ::I18n.locale
|
|
398
|
+
::I18n.available_locales = (previous_available + [:cs]).uniq
|
|
399
|
+
::I18n.locale = :cs
|
|
400
|
+
|
|
401
|
+
header 'Accept', 'application/json'
|
|
402
|
+
get '/unknown_resource'
|
|
403
|
+
|
|
404
|
+
expect(api_response.message).to eq('Action not found')
|
|
405
|
+
expect(::I18n.locale).to eq(:cs)
|
|
406
|
+
ensure
|
|
407
|
+
::I18n.available_locales = (Array(previous_available) + [previous_locale]).uniq
|
|
408
|
+
::I18n.locale = previous_locale
|
|
409
|
+
::I18n.available_locales = previous_available
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
it 'localizes nested message values without changing surrounding structure' do
|
|
413
|
+
previous_locale = ::I18n.locale
|
|
414
|
+
::I18n.locale = :cs
|
|
415
|
+
|
|
416
|
+
data = {
|
|
417
|
+
message: HaveAPI.message('haveapi.errors.action_not_found'),
|
|
418
|
+
errors: {
|
|
419
|
+
name: [HaveAPI.message('haveapi.validation.required_parameter_missing')]
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
expect(HaveAPI.localize(data)).to eq({
|
|
424
|
+
message: 'Akce nebyla nalezena',
|
|
425
|
+
errors: {
|
|
426
|
+
name: ['povinný parametr chybí']
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
ensure
|
|
430
|
+
::I18n.locale = previous_locale
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
context 'with a locale resolver' do
|
|
435
|
+
empty_api
|
|
436
|
+
|
|
437
|
+
locale do |**_|
|
|
438
|
+
:cs
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
it 'uses the resolver when no language is requested explicitly' do
|
|
442
|
+
header 'Accept', 'application/json'
|
|
443
|
+
get '/unknown_resource'
|
|
444
|
+
|
|
445
|
+
expect(api_response.message).to eq('Akce nebyla nalezena')
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
it 'does not use the resolver when an unsupported language is requested explicitly' do
|
|
449
|
+
['de', '%%%'].each do |language|
|
|
450
|
+
header 'Accept', 'application/json'
|
|
451
|
+
header 'Accept-Language', language
|
|
452
|
+
get '/unknown_resource'
|
|
453
|
+
|
|
454
|
+
expect(api_response.message).to eq('Action not found')
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
context 'with an authenticated locale resolver' do
|
|
460
|
+
api do
|
|
461
|
+
define_resource(:Thing) do
|
|
462
|
+
version 1
|
|
463
|
+
auth false
|
|
464
|
+
|
|
465
|
+
define_action(:Create) do
|
|
466
|
+
route ''
|
|
467
|
+
http_method :post
|
|
468
|
+
|
|
469
|
+
input do
|
|
470
|
+
string :name, required: true, length: { min: 3, max: 5 }
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
authorize { allow }
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
default_version 1
|
|
479
|
+
auth_chain I18nSpec::Provider
|
|
480
|
+
|
|
481
|
+
locale do |current_user:, default_locale:, **_|
|
|
482
|
+
current_user&.language || default_locale
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
it 'uses the authenticated user for root self-description locale fallback' do
|
|
486
|
+
login('user', 'pass')
|
|
487
|
+
header 'Accept', 'application/json'
|
|
488
|
+
call_api(:options, '/?describe=default')
|
|
489
|
+
|
|
490
|
+
action = api_response[:resources][:thing][:actions][:create]
|
|
491
|
+
length = action.dig(:input, :parameters, :name, :validators, :length)
|
|
492
|
+
expect(length[:message]).to eq('délka musí být v rozsahu <3, 5>')
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
context 'with a custom locale header' do
|
|
497
|
+
empty_api
|
|
498
|
+
locale_header 'X-Accept-Language'
|
|
499
|
+
|
|
500
|
+
it 'uses the configured header for locale negotiation and Vary' do
|
|
501
|
+
header 'Accept', 'application/json'
|
|
502
|
+
header 'X-Accept-Language', 'cs'
|
|
503
|
+
get '/unknown_resource'
|
|
504
|
+
|
|
505
|
+
expect(api_response.message).to eq('Akce nebyla nalezena')
|
|
506
|
+
expect(last_response.headers['Vary']).to eq('X-Accept-Language')
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
it 'allows the configured header in CORS preflight responses' do
|
|
510
|
+
header 'Accept', 'application/json'
|
|
511
|
+
header 'Origin', 'https://example.com'
|
|
512
|
+
header 'Access-Control-Request-Method', 'GET'
|
|
513
|
+
header 'Access-Control-Request-Headers', 'X-Accept-Language'
|
|
514
|
+
options '/'
|
|
515
|
+
|
|
516
|
+
allowed_headers = last_response.headers['access-control-allow-headers'].split(',')
|
|
517
|
+
expect(allowed_headers).to include('X-Accept-Language')
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
end
|