haveapi 0.28.3 → 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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +139 -0
  3. data/Rakefile +1 -0
  4. data/haveapi.gemspec +2 -1
  5. data/lib/haveapi/action.rb +26 -9
  6. data/lib/haveapi/actions/default.rb +5 -2
  7. data/lib/haveapi/actions/paginable.rb +8 -4
  8. data/lib/haveapi/authentication/oauth2/provider.rb +1 -1
  9. data/lib/haveapi/authentication/token/config.rb +10 -3
  10. data/lib/haveapi/authentication/token/provider.rb +22 -21
  11. data/lib/haveapi/context.rb +4 -1
  12. data/lib/haveapi/extensions/exception_mailer.rb +136 -17
  13. data/lib/haveapi/i18n.rb +125 -0
  14. data/lib/haveapi/locales/cs.yml +167 -0
  15. data/lib/haveapi/locales/en.yml +168 -0
  16. data/lib/haveapi/metadata.rb +25 -3
  17. data/lib/haveapi/model_adapters/active_record.rb +40 -26
  18. data/lib/haveapi/output_formatter.rb +2 -2
  19. data/lib/haveapi/parameters/metadata_i18n.rb +179 -0
  20. data/lib/haveapi/parameters/resource.rb +18 -7
  21. data/lib/haveapi/parameters/typed.rb +27 -20
  22. data/lib/haveapi/params.rb +76 -7
  23. data/lib/haveapi/resource.rb +1 -1
  24. data/lib/haveapi/resources/action_state.rb +47 -27
  25. data/lib/haveapi/server.rb +287 -94
  26. data/lib/haveapi/spec/api_builder.rb +25 -0
  27. data/lib/haveapi/spec/spec_methods.rb +10 -0
  28. data/lib/haveapi/tasks/i18n.rb +198 -0
  29. data/lib/haveapi/validator_chain.rb +1 -1
  30. data/lib/haveapi/validators/acceptance.rb +5 -2
  31. data/lib/haveapi/validators/confirmation.rb +5 -2
  32. data/lib/haveapi/validators/exclusion.rb +2 -2
  33. data/lib/haveapi/validators/format.rb +5 -2
  34. data/lib/haveapi/validators/inclusion.rb +2 -2
  35. data/lib/haveapi/validators/length.rb +5 -5
  36. data/lib/haveapi/validators/numericality.rb +20 -22
  37. data/lib/haveapi/validators/presence.rb +4 -2
  38. data/lib/haveapi/version.rb +1 -1
  39. data/lib/haveapi.rb +1 -0
  40. data/spec/authentication/oauth2_spec.rb +10 -0
  41. data/spec/extensions/exception_mailer_spec.rb +195 -0
  42. data/spec/i18n_spec.rb +520 -0
  43. data/spec/params_spec.rb +183 -0
  44. data/spec/server/integration_spec.rb +34 -0
  45. metadata +30 -7
  46. data/doc/create-client.md +0 -107
  47. data/doc/json-schema.html +0 -1182
  48. data/doc/protocol.md +0 -535
  49. data/doc/protocol.png +0 -0
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
data/spec/params_spec.rb CHANGED
@@ -1,4 +1,28 @@
1
1
  module ParamsSpec
2
+ class Authorization
3
+ def filter_input(_definitions, params)
4
+ params
5
+ end
6
+ end
7
+
8
+ class DescriptionContext
9
+ attr_accessor :server, :resource_path, :action, :layout, :authorization,
10
+ :endpoint, :action_prepare
11
+
12
+ def initialize(server:, resource_path:, action:)
13
+ @server = server
14
+ @resource_path = resource_path
15
+ @action = action
16
+ @authorization = Authorization.new
17
+ @endpoint = false
18
+ @action_prepare = false
19
+ end
20
+
21
+ def path_for(*)
22
+ '/v1/my_resources'
23
+ end
24
+ end
25
+
2
26
  class MyResource < HaveAPI::Resource
3
27
  params(:all) do
4
28
  string :res_param1
@@ -137,6 +161,165 @@ describe HaveAPI::Params do
137
161
  expect(p.params.first).to be_an_instance_of(HaveAPI::Parameters::Resource)
138
162
  end
139
163
 
164
+ it 'localizes resource parameter metadata' do
165
+ previous_locale = ::I18n.locale
166
+ previous_available = ::I18n.available_locales
167
+ ::I18n.available_locales = (previous_available + %i[en cs]).uniq
168
+ ::I18n.backend.store_translations(
169
+ :cs,
170
+ params_spec: {
171
+ resource: {
172
+ label: 'Vlastnik',
173
+ description: 'Vyber vlastnika'
174
+ }
175
+ }
176
+ )
177
+
178
+ p = described_class.new(:input, ParamsSpec::MyResource::Index)
179
+ p.add_block(proc do
180
+ resource ParamsSpec::MyResource,
181
+ label: HaveAPI.message('params_spec.resource.label'),
182
+ desc: HaveAPI.message('params_spec.resource.description')
183
+ end)
184
+ p.exec
185
+
186
+ context = double(
187
+ path_for: '/v1/my_resources',
188
+ endpoint: false,
189
+ action_prepare: false
190
+ )
191
+
192
+ ::I18n.locale = :cs
193
+ expect(p.params.first.describe(context)).to include(
194
+ label: 'Vlastnik',
195
+ description: 'Vyber vlastnika'
196
+ )
197
+ ensure
198
+ ::I18n.locale = previous_locale
199
+ ::I18n.available_locales = previous_available
200
+ end
201
+
202
+ it 'localizes resource parameter metadata from a parameter scope' do
203
+ previous_locale = ::I18n.locale
204
+ previous_available = ::I18n.available_locales
205
+ ::I18n.available_locales = (previous_available + %i[en cs]).uniq
206
+ ::I18n.backend.store_translations(
207
+ :cs,
208
+ params_spec: {
209
+ resources: {
210
+ my_resource: {
211
+ actions: {
212
+ index: {
213
+ input: {
214
+ my_resource: {
215
+ label: 'Zdroj',
216
+ description: 'Vyber zdroj'
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
222
+ }
223
+ }
224
+ )
225
+
226
+ server = double(parameter_i18n_scope: 'params_spec')
227
+ context = ParamsSpec::DescriptionContext.new(
228
+ server:,
229
+ resource_path: %w[my_resource],
230
+ action: ParamsSpec::MyResource::Index
231
+ )
232
+
233
+ p = described_class.new(:input, ParamsSpec::MyResource::Index)
234
+ p.add_block(proc do
235
+ resource ParamsSpec::MyResource, desc: 'Choose a resource'
236
+ end)
237
+ p.exec
238
+
239
+ ::I18n.locale = :cs
240
+ param = p.describe(context)[:parameters][:my_resource]
241
+ expect(param).to include(
242
+ label: 'Zdroj',
243
+ description: 'Vyber zdroj'
244
+ )
245
+ ensure
246
+ ::I18n.locale = previous_locale
247
+ ::I18n.available_locales = previous_available
248
+ end
249
+
250
+ it 'returns app-owned parameter metadata i18n catalog candidates' do
251
+ server = double(parameter_i18n_scope: 'params_spec')
252
+ context = ParamsSpec::DescriptionContext.new(
253
+ server:,
254
+ resource_path: %w[my_resource],
255
+ action: ParamsSpec::MyResource::Index
256
+ )
257
+
258
+ p = described_class.new(:input, ParamsSpec::MyResource::Index)
259
+ p.add_block(proc do
260
+ string :hostname, label: 'Hostname', desc: 'VPS hostname'
261
+ string :framework,
262
+ label: HaveAPI.message('haveapi.parameters.metadata.no.label')
263
+ end)
264
+ p.exec
265
+
266
+ items = p.parameter_metadata_i18n_items(context)
267
+ label = items.detect { |item| item[:param] == 'hostname' && item[:kind] == 'label' }
268
+
269
+ expect(label[:value]).to eq('Hostname')
270
+ expect(label[:keys]).to eq(%w[
271
+ params_spec.resources.my_resource.actions.index.input.hostname.label
272
+ params_spec.resources.my_resource.input.hostname.label
273
+ params_spec.resources.my_resource.attributes.hostname.label
274
+ params_spec.attributes.hostname.label
275
+ ])
276
+ expect(items.none? { |item| item[:param] == 'framework' }).to be true
277
+ end
278
+
279
+ it 'does not parse meta resource path segments as metadata paths' do
280
+ server = double(parameter_i18n_scope: 'params_spec')
281
+ context = ParamsSpec::DescriptionContext.new(
282
+ server:,
283
+ resource_path: %w[meta],
284
+ action: ParamsSpec::MyResource::Index
285
+ )
286
+
287
+ p = described_class.new(:input, ParamsSpec::MyResource::Index)
288
+ p.add_block proc { string :hostname, label: 'Hostname' }
289
+ p.exec
290
+
291
+ item = p.parameter_metadata_i18n_items(context).first
292
+
293
+ expect(item[:keys]).to eq(%w[
294
+ params_spec.resources.meta.actions.index.input.hostname.label
295
+ params_spec.resources.meta.input.hostname.label
296
+ params_spec.resources.meta.attributes.hostname.label
297
+ params_spec.attributes.hostname.label
298
+ ])
299
+ end
300
+
301
+ it 'returns meta parameter metadata i18n catalog candidates' do
302
+ server = double(parameter_i18n_scope: 'params_spec')
303
+ context = ParamsSpec::DescriptionContext.new(
304
+ server:,
305
+ resource_path: %w[my_resource],
306
+ action: ParamsSpec::MyResource::Index
307
+ )
308
+ i18n_path = described_class.metadata_i18n_path(context, :global, :output)
309
+
310
+ p = described_class.new(:output, ParamsSpec::MyResource::Index)
311
+ p.add_block proc { integer :count, label: 'Count' }
312
+ p.exec
313
+
314
+ item = p.parameter_metadata_i18n_items(context, i18n_path:, meta_type: :global).first
315
+
316
+ expect(item[:keys]).to eq(%w[
317
+ params_spec.resources.my_resource.actions.index.meta.global.output.count.label
318
+ params_spec.resources.my_resource.meta.global.output.count.label
319
+ params_spec.meta.global.output.count.label
320
+ ])
321
+ end
322
+
140
323
  it 'patches params' do
141
324
  p = described_class.new(:input, ParamsSpec::MyResource::Index)
142
325
  p.add_block(proc do