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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b5fea26ee9dde153cf44881ba46fe548ebe63077029d787e9b8e82ac19295d27
|
|
4
|
+
data.tar.gz: 9314173d105633354963d996f0d8f4ceb70d5ec30c22684ced53052d694d9463
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b1fb3f7ea1140e86091161f740399c258540036c5dccd04e2c9c6c632d0ef8f5e9cd4ab3cf9ecfec7f2952f109b50716fd255bb22fd385efea4c2ea47df6999
|
|
7
|
+
data.tar.gz: e68a162dc87376dccfc8b5db1cc190aae4e1a47ff18029f21325b7dc2dc063aa522af594cedf0bec2a2f7d0c00d1df4f75151405a83e32821f7289ad4a14775b
|
data/README.md
CHANGED
|
@@ -224,6 +224,145 @@ This should start the application using WEBrick. Check
|
|
|
224
224
|
|
|
225
225
|
and more.
|
|
226
226
|
|
|
227
|
+
## Localization
|
|
228
|
+
|
|
229
|
+
HaveAPI can translate framework-owned response messages, validation errors and
|
|
230
|
+
validator descriptions using Ruby `i18n`. The JSON envelope shape does not
|
|
231
|
+
change; `message`, `errors` and self-description validator messages are still
|
|
232
|
+
plain strings. Existing application-supplied strings passed to `error!` or
|
|
233
|
+
custom validator `message:` options are returned unchanged.
|
|
234
|
+
|
|
235
|
+
English is the default locale. Czech translations are bundled and can be
|
|
236
|
+
selected with `Accept-Language: cs` or a regional tag such as
|
|
237
|
+
`Accept-Language: cs-CZ`.
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
api = HaveAPI::Server.new(MyAPI)
|
|
241
|
+
|
|
242
|
+
api.default_locale = :en
|
|
243
|
+
api.available_locales = %i[en cs]
|
|
244
|
+
api.locale_header = 'Accept-Language'
|
|
245
|
+
|
|
246
|
+
api.locale do |request:, current_user:, default_locale:|
|
|
247
|
+
current_user&.language&.code || default_locale
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The explicit request header has precedence over the resolver. The resolver is
|
|
252
|
+
called again after authentication, so applications can use authenticated user
|
|
253
|
+
preferences when the client did not request a locale. If `locale_header` is set
|
|
254
|
+
to a custom header name, HaveAPI also allows that header in CORS preflight
|
|
255
|
+
responses. The header value uses the same syntax as `Accept-Language`.
|
|
256
|
+
|
|
257
|
+
Actions can opt into application translations by passing lazy messages to
|
|
258
|
+
`error!` or validator options:
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
error!(api_message('my_api.errors.quota_exceeded', limit: max_limit))
|
|
262
|
+
|
|
263
|
+
input do
|
|
264
|
+
string :name, required: {
|
|
265
|
+
message: HaveAPI.message('my_api.validation.name_required')
|
|
266
|
+
}
|
|
267
|
+
end
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Use `api_t(key, **values)` when an immediate string translation is needed in an
|
|
271
|
+
action. Use `HaveAPI.message(key, **values)` in class-level DSL blocks such as
|
|
272
|
+
`input`.
|
|
273
|
+
|
|
274
|
+
Action parameter labels and descriptions can be translated from the
|
|
275
|
+
self-description context. Set `parameter_i18n_scope` to an application locale
|
|
276
|
+
namespace and keep the existing English labels/descriptions as fallbacks:
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
api.parameter_i18n_scope = 'my_api'
|
|
280
|
+
|
|
281
|
+
input do
|
|
282
|
+
string :hostname,
|
|
283
|
+
label: 'Hostname',
|
|
284
|
+
desc: 'VPS hostname'
|
|
285
|
+
end
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
HaveAPI first looks up the exact action parameter key:
|
|
289
|
+
|
|
290
|
+
```yaml
|
|
291
|
+
cs:
|
|
292
|
+
my_api:
|
|
293
|
+
resources:
|
|
294
|
+
vps:
|
|
295
|
+
actions:
|
|
296
|
+
create:
|
|
297
|
+
input:
|
|
298
|
+
hostname:
|
|
299
|
+
label: "Hostname"
|
|
300
|
+
description: "Nazev VPS"
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The generated path is
|
|
304
|
+
`resources.<resource_path>.actions.<action>.<input|output>.<name>`.
|
|
305
|
+
Metadata parameters include the metadata type and direction, for example
|
|
306
|
+
`resources.vps.actions.create.meta.global.output.action_state_id.label`.
|
|
307
|
+
|
|
308
|
+
If the exact key is missing, HaveAPI falls back to resource input/output keys,
|
|
309
|
+
resource attributes and then shared attributes:
|
|
310
|
+
|
|
311
|
+
```yaml
|
|
312
|
+
cs:
|
|
313
|
+
my_api:
|
|
314
|
+
resources:
|
|
315
|
+
vps:
|
|
316
|
+
input:
|
|
317
|
+
hostname:
|
|
318
|
+
label: "Hostname"
|
|
319
|
+
description: "Nazev VPS"
|
|
320
|
+
output:
|
|
321
|
+
hostname:
|
|
322
|
+
label: "Hostname"
|
|
323
|
+
attributes:
|
|
324
|
+
hostname:
|
|
325
|
+
label: "Hostname"
|
|
326
|
+
attributes:
|
|
327
|
+
hostname:
|
|
328
|
+
label: "Hostname"
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Metadata parameters fall back through resource and global metadata keys:
|
|
332
|
+
|
|
333
|
+
```yaml
|
|
334
|
+
cs:
|
|
335
|
+
my_api:
|
|
336
|
+
resources:
|
|
337
|
+
vps:
|
|
338
|
+
meta:
|
|
339
|
+
global:
|
|
340
|
+
output:
|
|
341
|
+
count:
|
|
342
|
+
label: "Return item count"
|
|
343
|
+
meta:
|
|
344
|
+
global:
|
|
345
|
+
output:
|
|
346
|
+
count:
|
|
347
|
+
label: "Return item count"
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Framework-owned HaveAPI metadata, such as pagination and built-in meta
|
|
351
|
+
parameters, is translated by HaveAPI itself and is not looked up in the
|
|
352
|
+
application parameter scope. Use `label_key` and `desc_key` when a parameter
|
|
353
|
+
needs an explicit key that is not derived from its action location:
|
|
354
|
+
|
|
355
|
+
```ruby
|
|
356
|
+
string :hostname,
|
|
357
|
+
label: 'Hostname',
|
|
358
|
+
desc: 'VPS hostname',
|
|
359
|
+
label_key: 'my_api.attributes.hostname.label',
|
|
360
|
+
desc_key: 'my_api.attributes.hostname.description'
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
HaveAPI also exposes `api.parameter_metadata_i18n_items` for maintenance tools
|
|
364
|
+
that generate application locale catalogs from declared parameters.
|
|
365
|
+
|
|
227
366
|
### Run with rackup
|
|
228
367
|
Use the same code as above, only the last line would be
|
|
229
368
|
|
data/Rakefile
CHANGED
data/haveapi.gemspec
CHANGED
|
@@ -15,7 +15,8 @@ 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.
|
|
18
|
+
s.add_dependency 'haveapi-client', '~> 0.29.0'
|
|
19
|
+
s.add_dependency 'i18n', '>= 1.6', '< 2'
|
|
19
20
|
s.add_dependency 'json'
|
|
20
21
|
s.add_dependency 'mail'
|
|
21
22
|
s.add_dependency 'nesty', '~> 1.0'
|
data/lib/haveapi/action.rb
CHANGED
|
@@ -109,9 +109,8 @@ module HaveAPI
|
|
|
109
109
|
meta(:global) do
|
|
110
110
|
output do
|
|
111
111
|
integer :action_state_id,
|
|
112
|
-
label: '
|
|
113
|
-
desc: '
|
|
114
|
-
'is not blocking for the current invocation.'
|
|
112
|
+
label: HaveAPI.message('haveapi.parameters.action_state_id.label'),
|
|
113
|
+
desc: HaveAPI.message('haveapi.parameters.action_state_id.description')
|
|
115
114
|
end
|
|
116
115
|
end
|
|
117
116
|
end
|
|
@@ -231,12 +230,12 @@ module HaveAPI
|
|
|
231
230
|
|
|
232
231
|
{
|
|
233
232
|
auth: @auth,
|
|
234
|
-
description: @desc,
|
|
233
|
+
description: HaveAPI.localize(@desc),
|
|
235
234
|
aliases: @aliases,
|
|
236
235
|
blocking: @blocking ? true : false,
|
|
237
236
|
input: @input ? @input.describe(context) : { parameters: {} },
|
|
238
237
|
output: @output ? @output.describe(context) : { parameters: {} },
|
|
239
|
-
meta: @meta ? @meta.
|
|
238
|
+
meta: @meta ? @meta.to_h { |type, v| [type, v && v.describe(context, type:)] } : nil,
|
|
240
239
|
examples: @examples ? @examples.describe(context) : [],
|
|
241
240
|
scope: context.action_scope,
|
|
242
241
|
path: context.resolved_path,
|
|
@@ -245,6 +244,16 @@ module HaveAPI
|
|
|
245
244
|
}
|
|
246
245
|
end
|
|
247
246
|
|
|
247
|
+
def parameter_metadata_i18n_items(context)
|
|
248
|
+
[
|
|
249
|
+
*@input&.parameter_metadata_i18n_items(context),
|
|
250
|
+
*@output&.parameter_metadata_i18n_items(context),
|
|
251
|
+
*(@meta&.flat_map do |type, metadata|
|
|
252
|
+
metadata&.parameter_metadata_i18n_items(context, type:)
|
|
253
|
+
end)
|
|
254
|
+
].compact
|
|
255
|
+
end
|
|
256
|
+
|
|
248
257
|
# Inherit attributes from resource action is defined in.
|
|
249
258
|
def inherit_attrs_from_resource(action, r, attrs)
|
|
250
259
|
begin
|
|
@@ -339,7 +348,7 @@ module HaveAPI
|
|
|
339
348
|
status = @context.server.validation_error_http_status
|
|
340
349
|
opts[:http_status] = status if status
|
|
341
350
|
|
|
342
|
-
error!(e.
|
|
351
|
+
error!(e.message_value, e.to_hash, opts)
|
|
343
352
|
end
|
|
344
353
|
|
|
345
354
|
def authorized?(user)
|
|
@@ -397,7 +406,7 @@ module HaveAPI
|
|
|
397
406
|
if tmp.empty?
|
|
398
407
|
p e.message
|
|
399
408
|
puts e.backtrace
|
|
400
|
-
error!('
|
|
409
|
+
error!(HaveAPI.message('haveapi.errors.server_error'), {}, http_status: 500)
|
|
401
410
|
end
|
|
402
411
|
|
|
403
412
|
unless tmp[:status]
|
|
@@ -415,7 +424,7 @@ module HaveAPI
|
|
|
415
424
|
|
|
416
425
|
return [
|
|
417
426
|
tmp[:status] || false,
|
|
418
|
-
tmp[:message] || '
|
|
427
|
+
tmp[:message] || HaveAPI.message('haveapi.errors.server_error'),
|
|
419
428
|
{},
|
|
420
429
|
tmp[:http_status] || 500
|
|
421
430
|
]
|
|
@@ -507,7 +516,7 @@ module HaveAPI
|
|
|
507
516
|
output {}
|
|
508
517
|
meta(:global) do
|
|
509
518
|
input do
|
|
510
|
-
bool :no, label: '
|
|
519
|
+
bool :no, label: HaveAPI.message('haveapi.parameters.metadata.no.label')
|
|
511
520
|
end
|
|
512
521
|
end
|
|
513
522
|
|
|
@@ -592,6 +601,14 @@ module HaveAPI
|
|
|
592
601
|
throw(:return, false)
|
|
593
602
|
end
|
|
594
603
|
|
|
604
|
+
def api_message(...)
|
|
605
|
+
HaveAPI.message(...)
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def api_t(...)
|
|
609
|
+
HaveAPI.t(...)
|
|
610
|
+
end
|
|
611
|
+
|
|
595
612
|
private
|
|
596
613
|
|
|
597
614
|
def validate
|
|
@@ -11,11 +11,14 @@ module HaveAPI
|
|
|
11
11
|
|
|
12
12
|
meta(:global) do
|
|
13
13
|
input do
|
|
14
|
-
bool :count,
|
|
14
|
+
bool :count,
|
|
15
|
+
label: HaveAPI.message('haveapi.parameters.default.count.label'),
|
|
16
|
+
default: false
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
output do
|
|
18
|
-
integer :total_count,
|
|
20
|
+
integer :total_count,
|
|
21
|
+
label: HaveAPI.message('haveapi.parameters.default.total_count.label')
|
|
19
22
|
end
|
|
20
23
|
end
|
|
21
24
|
|
|
@@ -4,10 +4,14 @@ module HaveAPI::Actions
|
|
|
4
4
|
|
|
5
5
|
def self.included(action)
|
|
6
6
|
action.input do
|
|
7
|
-
integer :from_id,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
integer :from_id,
|
|
8
|
+
label: HaveAPI.message('haveapi.parameters.paginable.from_id.label'),
|
|
9
|
+
desc: HaveAPI.message('haveapi.parameters.paginable.from_id.description'),
|
|
10
|
+
number: { min: 0 }
|
|
11
|
+
integer :limit,
|
|
12
|
+
label: HaveAPI.message('haveapi.parameters.paginable.limit.label'),
|
|
13
|
+
desc: HaveAPI.message('haveapi.parameters.paginable.limit.description'),
|
|
14
|
+
number: { min: 0, max: MAX_LIMIT }
|
|
11
15
|
end
|
|
12
16
|
end
|
|
13
17
|
end
|
|
@@ -127,7 +127,7 @@ module HaveAPI::Authentication
|
|
|
127
127
|
tokens.first
|
|
128
128
|
else
|
|
129
129
|
raise HaveAPI::Authentication::TokenConflict,
|
|
130
|
-
'
|
|
130
|
+
HaveAPI.t('haveapi.authentication.multiple_oauth2_tokens')
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
token && config.find_user_by_access_token(request, token)
|
|
@@ -75,9 +75,16 @@ module HaveAPI::Authentication
|
|
|
75
75
|
# Default request
|
|
76
76
|
subclass.request do
|
|
77
77
|
input do
|
|
78
|
-
string :user,
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
string :user,
|
|
79
|
+
label: HaveAPI.message('haveapi.parameters.authentication.token.user.label'),
|
|
80
|
+
required: true
|
|
81
|
+
password :password,
|
|
82
|
+
label: HaveAPI.message('haveapi.parameters.authentication.token.password.label'),
|
|
83
|
+
required: true
|
|
84
|
+
string :scope,
|
|
85
|
+
label: HaveAPI.message('haveapi.parameters.authentication.token.scope.label'),
|
|
86
|
+
default: 'all',
|
|
87
|
+
fill: true
|
|
81
88
|
end
|
|
82
89
|
|
|
83
90
|
handle do
|
|
@@ -177,7 +177,7 @@ module HaveAPI::Authentication
|
|
|
177
177
|
tokens.first
|
|
178
178
|
else
|
|
179
179
|
raise HaveAPI::Authentication::TokenConflict,
|
|
180
|
-
'
|
|
180
|
+
HaveAPI.t('haveapi.authentication.multiple_tokens')
|
|
181
181
|
end
|
|
182
182
|
end
|
|
183
183
|
|
|
@@ -217,18 +217,17 @@ module HaveAPI::Authentication
|
|
|
217
217
|
instance_exec(&block)
|
|
218
218
|
end
|
|
219
219
|
|
|
220
|
-
string :lifetime,
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
number: { min: 1, max: 86_400 }
|
|
220
|
+
string :lifetime,
|
|
221
|
+
label: HaveAPI.message('haveapi.parameters.authentication.token.lifetime.label'),
|
|
222
|
+
required: true,
|
|
223
|
+
choices: %i[fixed renewable_manual renewable_auto permanent],
|
|
224
|
+
desc: HaveAPI.message('haveapi.parameters.authentication.token.lifetime.description')
|
|
225
|
+
integer :interval,
|
|
226
|
+
label: HaveAPI.message('haveapi.parameters.authentication.token.interval.label'),
|
|
227
|
+
desc: HaveAPI.message('haveapi.parameters.authentication.token.interval.description'),
|
|
228
|
+
default: 60 * 5,
|
|
229
|
+
fill: true,
|
|
230
|
+
number: { min: 1, max: 86_400 }
|
|
232
231
|
end
|
|
233
232
|
|
|
234
233
|
output(:hash) do
|
|
@@ -245,7 +244,7 @@ module HaveAPI::Authentication
|
|
|
245
244
|
def validate!
|
|
246
245
|
validate
|
|
247
246
|
rescue HaveAPI::ValidationError => e
|
|
248
|
-
error!(e.
|
|
247
|
+
error!(e.message_value, e.to_hash, http_status: 400)
|
|
249
248
|
end
|
|
250
249
|
|
|
251
250
|
def exec
|
|
@@ -261,7 +260,9 @@ module HaveAPI::Authentication
|
|
|
261
260
|
end
|
|
262
261
|
|
|
263
262
|
unless result.ok?
|
|
264
|
-
error!(
|
|
263
|
+
error!(
|
|
264
|
+
result.error || HaveAPI.message('haveapi.authentication.invalid_credentials')
|
|
265
|
+
)
|
|
265
266
|
end
|
|
266
267
|
|
|
267
268
|
{
|
|
@@ -292,7 +293,7 @@ module HaveAPI::Authentication
|
|
|
292
293
|
|
|
293
294
|
unless user
|
|
294
295
|
error!(
|
|
295
|
-
'
|
|
296
|
+
HaveAPI.message('haveapi.authentication.token_required'),
|
|
296
297
|
{},
|
|
297
298
|
http_status: 401
|
|
298
299
|
)
|
|
@@ -311,7 +312,7 @@ module HaveAPI::Authentication
|
|
|
311
312
|
if result.ok?
|
|
312
313
|
ok!
|
|
313
314
|
else
|
|
314
|
-
error!(result.error || '
|
|
315
|
+
error!(result.error || HaveAPI.message('haveapi.authentication.revoke_failed'))
|
|
315
316
|
end
|
|
316
317
|
end
|
|
317
318
|
end
|
|
@@ -339,7 +340,7 @@ module HaveAPI::Authentication
|
|
|
339
340
|
|
|
340
341
|
unless user
|
|
341
342
|
error!(
|
|
342
|
-
'
|
|
343
|
+
HaveAPI.message('haveapi.authentication.token_required'),
|
|
343
344
|
{},
|
|
344
345
|
http_status: 401
|
|
345
346
|
)
|
|
@@ -358,7 +359,7 @@ module HaveAPI::Authentication
|
|
|
358
359
|
if result.ok?
|
|
359
360
|
{ valid_to: result.valid_to }
|
|
360
361
|
else
|
|
361
|
-
error!(result.error || '
|
|
362
|
+
error!(result.error || HaveAPI.message('haveapi.authentication.renew_failed'))
|
|
362
363
|
end
|
|
363
364
|
end
|
|
364
365
|
end
|
|
@@ -387,7 +388,7 @@ module HaveAPI::Authentication
|
|
|
387
388
|
def validate!
|
|
388
389
|
validate
|
|
389
390
|
rescue HaveAPI::ValidationError => e
|
|
390
|
-
error!(e.
|
|
391
|
+
error!(e.message_value, e.to_hash, http_status: 400)
|
|
391
392
|
end
|
|
392
393
|
|
|
393
394
|
define_method(:exec) do
|
|
@@ -402,7 +403,7 @@ module HaveAPI::Authentication
|
|
|
402
403
|
end
|
|
403
404
|
|
|
404
405
|
unless result.ok?
|
|
405
|
-
error!(result.error || 'authentication
|
|
406
|
+
error!(result.error || HaveAPI.message('haveapi.authentication.failed'))
|
|
406
407
|
end
|
|
407
408
|
|
|
408
409
|
{
|
data/lib/haveapi/context.rb
CHANGED
|
@@ -113,7 +113,10 @@ module HaveAPI
|
|
|
113
113
|
|
|
114
114
|
def resolve_arg!(path, arg)
|
|
115
115
|
value = arg.to_s
|
|
116
|
-
|
|
116
|
+
unless value.valid_encoding?
|
|
117
|
+
raise HaveAPI::ValidationError,
|
|
118
|
+
HaveAPI.message('haveapi.validation.invalid_path_parameter_encoding')
|
|
119
|
+
end
|
|
117
120
|
|
|
118
121
|
path.sub!(/\{[a-zA-Z0-9\-_]+\}/, value)
|
|
119
122
|
end
|
data/lib/haveapi/i18n.rb
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
require 'i18n'
|
|
2
|
+
|
|
3
|
+
module HaveAPI
|
|
4
|
+
class LocalizedMessage
|
|
5
|
+
attr_reader :key, :values, :default
|
|
6
|
+
|
|
7
|
+
def initialize(key, default: nil, scope: nil, **values)
|
|
8
|
+
@key = normalize_key(key, scope)
|
|
9
|
+
@default = default
|
|
10
|
+
@values = values
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def translate(extra_values = {})
|
|
14
|
+
values = localized_values(@values.merge(extra_values))
|
|
15
|
+
opts = values
|
|
16
|
+
opts[:default] = @default if @default
|
|
17
|
+
|
|
18
|
+
::I18n.t(@key, **opts)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_s
|
|
22
|
+
translate
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def normalize_key(key, scope)
|
|
28
|
+
return key.to_s unless scope
|
|
29
|
+
|
|
30
|
+
"#{scope}.#{key}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def localized_values(values)
|
|
34
|
+
values.transform_values do |value|
|
|
35
|
+
case value
|
|
36
|
+
when Array
|
|
37
|
+
value.map { |v| HaveAPI.localize(v) }.join('; ')
|
|
38
|
+
else
|
|
39
|
+
HaveAPI.localize(value)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
module I18n
|
|
46
|
+
class << self
|
|
47
|
+
def setup
|
|
48
|
+
locale_dir = File.expand_path('locales', __dir__)
|
|
49
|
+
::I18n.load_path |= Dir[File.join(locale_dir, '*.yml')]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def available_locales
|
|
53
|
+
locale_dir = File.expand_path('locales', __dir__)
|
|
54
|
+
|
|
55
|
+
Dir[File.join(locale_dir, '*.yml')].map do |path|
|
|
56
|
+
File.basename(path, '.yml').to_sym
|
|
57
|
+
end.sort
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def accept_language(header, available_locales)
|
|
61
|
+
header.to_s.split(',').each_with_index.filter_map do |raw, index|
|
|
62
|
+
tag, *params = raw.strip.split(';')
|
|
63
|
+
next if tag.nil? || tag.empty?
|
|
64
|
+
|
|
65
|
+
q = params.map(&:strip).grep(/\Aq=/).first
|
|
66
|
+
weight = q ? q.split('=', 2).last.to_f : 1.0
|
|
67
|
+
next if weight <= 0.0
|
|
68
|
+
|
|
69
|
+
[tag, weight, index]
|
|
70
|
+
end.sort_by { |(_, weight, index)| [-weight, index] }.each do |tag, _, _|
|
|
71
|
+
locale = normalize_locale(tag, available_locales)
|
|
72
|
+
return locale if locale
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
nil
|
|
76
|
+
rescue StandardError
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def normalize_locale(locale, available_locales)
|
|
81
|
+
return if locale.nil?
|
|
82
|
+
|
|
83
|
+
tag = locale.to_s.strip.tr('_', '-')
|
|
84
|
+
return if tag.empty? || tag == '*'
|
|
85
|
+
|
|
86
|
+
available = Array(available_locales).map(&:to_s)
|
|
87
|
+
candidates = [tag, tag.split('-').first].compact.map(&:downcase).uniq
|
|
88
|
+
|
|
89
|
+
candidates.each do |candidate|
|
|
90
|
+
match = available.detect { |v| v.downcase == candidate }
|
|
91
|
+
return match.to_sym if match
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class << self
|
|
100
|
+
def message(key, default: nil, scope: nil, **values)
|
|
101
|
+
LocalizedMessage.new(key, default:, scope:, **values)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def t(key, default: nil, scope: nil, **values)
|
|
105
|
+
message(key, default:, scope:, **values).translate
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def localize(value, **extra_values)
|
|
109
|
+
case value
|
|
110
|
+
when LocalizedMessage
|
|
111
|
+
value.translate(extra_values)
|
|
112
|
+
when Array
|
|
113
|
+
value.map { |v| localize(v, **extra_values) }
|
|
114
|
+
when Hash
|
|
115
|
+
value.transform_values { |v| localize(v, **extra_values) }
|
|
116
|
+
when String
|
|
117
|
+
extra_values.empty? ? value : format(value, extra_values)
|
|
118
|
+
else
|
|
119
|
+
value
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
HaveAPI::I18n.setup
|