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
data/lib/haveapi/server.rb
CHANGED
|
@@ -6,9 +6,10 @@ require 'haveapi/hooks'
|
|
|
6
6
|
|
|
7
7
|
module HaveAPI
|
|
8
8
|
class Server
|
|
9
|
-
attr_accessor :default_version, :action_state, :validation_error_http_status
|
|
9
|
+
attr_accessor :default_version, :action_state, :validation_error_http_status,
|
|
10
|
+
:default_locale, :parameter_i18n_scope
|
|
10
11
|
attr_reader :root, :routes, :module_name, :auth_chain, :versions, :extensions,
|
|
11
|
-
:action_state_auth
|
|
12
|
+
:action_state_auth, :locale_header, :available_locales
|
|
12
13
|
|
|
13
14
|
include Hookable
|
|
14
15
|
|
|
@@ -57,6 +58,50 @@ module HaveAPI
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
module ServerHelpers
|
|
61
|
+
def setup_request_locale
|
|
62
|
+
return if @haveapi_locale_setup
|
|
63
|
+
|
|
64
|
+
server = settings.api_server
|
|
65
|
+
@haveapi_previous_locale = ::I18n.locale
|
|
66
|
+
locale_header_value = server.locale_header_value(request)
|
|
67
|
+
@haveapi_locale_requested = !locale_header_value.nil?
|
|
68
|
+
@haveapi_explicit_locale = server.request_locale(request)
|
|
69
|
+
@haveapi_locale = if @haveapi_locale_requested
|
|
70
|
+
@haveapi_explicit_locale || server.default_locale
|
|
71
|
+
else
|
|
72
|
+
server.resolved_locale(
|
|
73
|
+
request:,
|
|
74
|
+
current_user: nil
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
server.activate_locale(@haveapi_locale)
|
|
78
|
+
add_vary_header(server.locale_header) if server.locale_header
|
|
79
|
+
@haveapi_locale_setup = true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def resolve_request_locale(user = current_user)
|
|
83
|
+
setup_request_locale
|
|
84
|
+
return @haveapi_locale if @haveapi_locale_requested
|
|
85
|
+
|
|
86
|
+
@haveapi_locale = settings.api_server.resolved_locale(
|
|
87
|
+
request:,
|
|
88
|
+
current_user: user
|
|
89
|
+
)
|
|
90
|
+
settings.api_server.activate_locale(@haveapi_locale)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def restore_request_locale
|
|
94
|
+
return unless @haveapi_locale_setup
|
|
95
|
+
|
|
96
|
+
settings.api_server.activate_locale(@haveapi_previous_locale)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def add_vary_header(name)
|
|
100
|
+
values = response['Vary'].to_s.split(/\s*,\s*/).reject(&:empty?)
|
|
101
|
+
values << name unless values.include?(name)
|
|
102
|
+
headers 'Vary' => values.join(', ')
|
|
103
|
+
end
|
|
104
|
+
|
|
60
105
|
def setup_formatter
|
|
61
106
|
return if @formatter
|
|
62
107
|
|
|
@@ -64,7 +109,7 @@ module HaveAPI
|
|
|
64
109
|
accept = request.accept
|
|
65
110
|
rescue ArgumentError, EncodingError
|
|
66
111
|
@formatter.supports?([])
|
|
67
|
-
report_error(400, {}, '
|
|
112
|
+
report_error(400, {}, HaveAPI.message('haveapi.errors.bad_accept_header'))
|
|
68
113
|
else
|
|
69
114
|
unless @formatter.supports?(accept)
|
|
70
115
|
@halted = true
|
|
@@ -79,7 +124,10 @@ module HaveAPI
|
|
|
79
124
|
end
|
|
80
125
|
|
|
81
126
|
def authenticated?(v)
|
|
82
|
-
|
|
127
|
+
if @current_user
|
|
128
|
+
resolve_request_locale
|
|
129
|
+
return @current_user
|
|
130
|
+
end
|
|
83
131
|
|
|
84
132
|
begin
|
|
85
133
|
@current_user = settings.api_server.send(:do_authenticate, v, request)
|
|
@@ -92,12 +140,13 @@ module HaveAPI
|
|
|
92
140
|
report_error(400, {}, e.message)
|
|
93
141
|
end
|
|
94
142
|
settings.api_server.call_hooks_for(:post_authenticated, args: [@current_user])
|
|
143
|
+
resolve_request_locale
|
|
95
144
|
@current_user
|
|
96
145
|
end
|
|
97
146
|
|
|
98
147
|
def authenticated_versions
|
|
99
|
-
settings.api_server.versions.each_with_object({}) do |v,
|
|
100
|
-
|
|
148
|
+
ret = settings.api_server.versions.each_with_object({}) do |v, users|
|
|
149
|
+
users[v] = settings.api_server.send(:do_authenticate, v, request)
|
|
101
150
|
rescue HaveAPI::Authentication::TokenConflict => e
|
|
102
151
|
unless @formatter
|
|
103
152
|
@formatter = OutputFormatter.new
|
|
@@ -106,6 +155,9 @@ module HaveAPI
|
|
|
106
155
|
|
|
107
156
|
report_error(400, {}, e.message)
|
|
108
157
|
end
|
|
158
|
+
|
|
159
|
+
resolve_request_locale(ret[settings.api_server.default_version] || ret.values.compact.first)
|
|
160
|
+
ret
|
|
109
161
|
end
|
|
110
162
|
|
|
111
163
|
def access_control
|
|
@@ -133,7 +185,7 @@ module HaveAPI
|
|
|
133
185
|
report_error(
|
|
134
186
|
401,
|
|
135
187
|
{ 'www-authenticate' => 'Basic realm="Restricted Area"' },
|
|
136
|
-
'
|
|
188
|
+
HaveAPI.message('haveapi.authentication.required')
|
|
137
189
|
)
|
|
138
190
|
end
|
|
139
191
|
|
|
@@ -170,7 +222,7 @@ module HaveAPI
|
|
|
170
222
|
report_error(
|
|
171
223
|
tmp[:http_status] || 500,
|
|
172
224
|
{},
|
|
173
|
-
tmp[:message] || '
|
|
225
|
+
tmp[:message] || HaveAPI.message('haveapi.errors.server_error')
|
|
174
226
|
)
|
|
175
227
|
end
|
|
176
228
|
|
|
@@ -260,6 +312,62 @@ module HaveAPI
|
|
|
260
312
|
@extensions = []
|
|
261
313
|
@action_state_auth = :backend
|
|
262
314
|
@validation_error_http_status = nil
|
|
315
|
+
@default_locale = :en
|
|
316
|
+
self.available_locales = HaveAPI::I18n.available_locales
|
|
317
|
+
self.locale_header = 'Accept-Language'
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def available_locales=(locales)
|
|
321
|
+
@available_locales = Array(locales)
|
|
322
|
+
allow_i18n_locales(@available_locales)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def locale_header=(header)
|
|
326
|
+
@locale_header = header
|
|
327
|
+
allow_header(header) if header
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def activate_locale(locale)
|
|
331
|
+
allow_i18n_locales([locale])
|
|
332
|
+
::I18n.locale = locale
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def allow_i18n_locales(locales)
|
|
336
|
+
requested = Array(locales).compact.map { |locale| locale.to_s.to_sym }
|
|
337
|
+
current = ::I18n.available_locales
|
|
338
|
+
missing = requested.reject do |locale|
|
|
339
|
+
current.any? { |available| available.to_s == locale.to_s }
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
::I18n.available_locales = current + missing unless missing.empty?
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def locale(&block)
|
|
346
|
+
@locale_resolver = block if block
|
|
347
|
+
@locale_resolver
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def request_locale(request)
|
|
351
|
+
HaveAPI::I18n.accept_language(
|
|
352
|
+
locale_header_value(request),
|
|
353
|
+
available_locales
|
|
354
|
+
)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def locale_header_value(request)
|
|
358
|
+
request.env["HTTP_#{locale_header.to_s.upcase.tr('-', '_')}"]
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def resolved_locale(request:, current_user:)
|
|
362
|
+
locale = if @locale_resolver
|
|
363
|
+
@locale_resolver.call(
|
|
364
|
+
request:,
|
|
365
|
+
current_user:,
|
|
366
|
+
default_locale:
|
|
367
|
+
)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
HaveAPI::I18n.normalize_locale(locale, available_locales) || default_locale
|
|
263
371
|
end
|
|
264
372
|
|
|
265
373
|
def action_state_auth=(mode)
|
|
@@ -321,6 +429,8 @@ module HaveAPI
|
|
|
321
429
|
helpers DocHelpers
|
|
322
430
|
|
|
323
431
|
before do
|
|
432
|
+
setup_request_locale
|
|
433
|
+
|
|
324
434
|
if request.env['HTTP_ORIGIN']
|
|
325
435
|
headers 'access-control-allow-origin' => '*',
|
|
326
436
|
'access-control-allow-credentials' => 'false'
|
|
@@ -329,7 +439,7 @@ module HaveAPI
|
|
|
329
439
|
|
|
330
440
|
not_found do
|
|
331
441
|
setup_formatter
|
|
332
|
-
report_error(404, {}, '
|
|
442
|
+
report_error(404, {}, HaveAPI.message('haveapi.errors.action_not_found')) unless @halted
|
|
333
443
|
end
|
|
334
444
|
|
|
335
445
|
error do
|
|
@@ -337,6 +447,8 @@ module HaveAPI
|
|
|
337
447
|
end
|
|
338
448
|
|
|
339
449
|
after do
|
|
450
|
+
restore_request_locale
|
|
451
|
+
|
|
340
452
|
if Object.const_defined?(:ActiveRecord)
|
|
341
453
|
ActiveRecord::Base.connection_handler.clear_active_connections!
|
|
342
454
|
end
|
|
@@ -534,6 +646,27 @@ module HaveAPI
|
|
|
534
646
|
end
|
|
535
647
|
end
|
|
536
648
|
|
|
649
|
+
def collect_parameter_metadata_i18n_items(resources, context)
|
|
650
|
+
resources.flat_map do |resource, children|
|
|
651
|
+
original_resource_path = context.resource_path
|
|
652
|
+
context.resource_path = context.resource_path + [resource.resource_name.underscore]
|
|
653
|
+
|
|
654
|
+
action_items = children[:actions].flat_map do |action, path|
|
|
655
|
+
context.action = action
|
|
656
|
+
context.path = path
|
|
657
|
+
|
|
658
|
+
action.parameter_metadata_i18n_items(context)
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
child_items = collect_parameter_metadata_i18n_items(children[:resources], context)
|
|
662
|
+
|
|
663
|
+
action_items + child_items
|
|
664
|
+
ensure
|
|
665
|
+
context.resource_path = original_resource_path
|
|
666
|
+
end
|
|
667
|
+
end
|
|
668
|
+
private :collect_parameter_metadata_i18n_items
|
|
669
|
+
|
|
537
670
|
def mount_resource(prefix, v, resource, hash)
|
|
538
671
|
hash[resource] = { resources: {}, actions: {} }
|
|
539
672
|
|
|
@@ -584,17 +717,17 @@ module HaveAPI
|
|
|
584
717
|
body_method = !%i[get head options].include?(route.http_method.to_sym)
|
|
585
718
|
|
|
586
719
|
if body_method && !raw_body.empty? && !settings.api_server.send(:json_content_type?, request)
|
|
587
|
-
report_error(415, {}, '
|
|
720
|
+
report_error(415, {}, HaveAPI.message('haveapi.errors.unsupported_content_type'))
|
|
588
721
|
end
|
|
589
722
|
|
|
590
723
|
begin
|
|
591
724
|
body = raw_body.empty? ? nil : JSON.parse(raw_body)
|
|
592
725
|
rescue JSON::ParserError
|
|
593
|
-
report_error(400, {}, '
|
|
726
|
+
report_error(400, {}, HaveAPI.message('haveapi.errors.bad_json_syntax'))
|
|
594
727
|
end
|
|
595
728
|
|
|
596
729
|
if !raw_body.empty? && !body.is_a?(Hash)
|
|
597
|
-
report_error(400, {}, '
|
|
730
|
+
report_error(400, {}, HaveAPI.message('haveapi.errors.json_body_object'))
|
|
598
731
|
end
|
|
599
732
|
|
|
600
733
|
action_params = settings.api_server.send(:path_params, route, params)
|
|
@@ -616,7 +749,7 @@ module HaveAPI
|
|
|
616
749
|
action = route.action.new(request, v, action_params, action_input, context)
|
|
617
750
|
|
|
618
751
|
unless action.authorized?(current_user)
|
|
619
|
-
report_error(403, {}, '
|
|
752
|
+
report_error(403, {}, HaveAPI.message('haveapi.authorization.insufficient_permissions'))
|
|
620
753
|
end
|
|
621
754
|
|
|
622
755
|
status, reply, errors, http_status = action.safe_exec
|
|
@@ -671,16 +804,16 @@ module HaveAPI
|
|
|
671
804
|
desc = route.action.describe(ctx)
|
|
672
805
|
|
|
673
806
|
unless desc
|
|
674
|
-
report_error(403, {}, '
|
|
807
|
+
report_error(403, {}, HaveAPI.message('haveapi.authorization.insufficient_permissions'))
|
|
675
808
|
end
|
|
676
809
|
rescue ValidationError => e
|
|
677
|
-
report_error(400, e.to_hash, e.
|
|
810
|
+
report_error(400, e.to_hash, e.message_value)
|
|
678
811
|
rescue StandardError => e
|
|
679
812
|
tmp = settings.api_server.call_hooks_for(:description_exception, args: [ctx, e])
|
|
680
813
|
report_error(
|
|
681
814
|
tmp[:http_status] || 500,
|
|
682
815
|
{},
|
|
683
|
-
tmp[:message] || '
|
|
816
|
+
tmp[:message] || HaveAPI.message('haveapi.errors.server_error')
|
|
684
817
|
)
|
|
685
818
|
end
|
|
686
819
|
|
|
@@ -742,6 +875,13 @@ module HaveAPI
|
|
|
742
875
|
r.describe(hash, context)
|
|
743
876
|
end
|
|
744
877
|
|
|
878
|
+
def parameter_metadata_i18n_items(version: @default_version)
|
|
879
|
+
routes = @routes.fetch(version)
|
|
880
|
+
context = Context.new(self, version:, doc: true)
|
|
881
|
+
|
|
882
|
+
collect_parameter_metadata_i18n_items(routes[:resources], context)
|
|
883
|
+
end
|
|
884
|
+
|
|
745
885
|
def path_for_action(version, action)
|
|
746
886
|
routes = @routes && @routes[version]
|
|
747
887
|
return unless routes
|
|
@@ -64,6 +64,31 @@ module HaveAPI::Spec
|
|
|
64
64
|
opt(:validation_error_http_status, status)
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
# Set default response locale.
|
|
68
|
+
def default_locale(locale)
|
|
69
|
+
opt(:default_locale, locale)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Set locales available to the API server.
|
|
73
|
+
def available_locales(locales)
|
|
74
|
+
opt(:available_locales, locales)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Set request header used to negotiate the response locale.
|
|
78
|
+
def locale_header(header)
|
|
79
|
+
opt(:locale_header, header)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Set custom locale resolver.
|
|
83
|
+
def locale(&block)
|
|
84
|
+
opt(:locale, block)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Set the translation scope for action parameter labels/descriptions.
|
|
88
|
+
def parameter_i18n_scope(scope)
|
|
89
|
+
opt(:parameter_i18n_scope, scope)
|
|
90
|
+
end
|
|
91
|
+
|
|
67
92
|
# Set a custom mount path.
|
|
68
93
|
def mount_to(path)
|
|
69
94
|
opt(:mount, path)
|
|
@@ -19,6 +19,16 @@ module HaveAPI::Spec
|
|
|
19
19
|
@api.action_state_auth = asa if asa
|
|
20
20
|
ves = get_opt(:validation_error_http_status)
|
|
21
21
|
@api.validation_error_http_status = ves if ves
|
|
22
|
+
dl = get_opt(:default_locale)
|
|
23
|
+
@api.default_locale = dl if dl
|
|
24
|
+
al = get_opt(:available_locales)
|
|
25
|
+
@api.available_locales = al if al
|
|
26
|
+
lh = get_opt(:locale_header)
|
|
27
|
+
@api.locale_header = lh if lh
|
|
28
|
+
locale = get_opt(:locale)
|
|
29
|
+
@api.locale(&locale) if locale
|
|
30
|
+
parameter_i18n_scope = get_opt(:parameter_i18n_scope)
|
|
31
|
+
@api.parameter_i18n_scope = parameter_i18n_scope if parameter_i18n_scope
|
|
22
32
|
@api.mount(get_opt(:mount) || '/')
|
|
23
33
|
@api.app
|
|
24
34
|
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
|
|
3
|
+
module HaveAPI
|
|
4
|
+
module Tasks
|
|
5
|
+
class I18nHealth
|
|
6
|
+
KEY_LITERAL_PATTERN = /['"](haveapi\.[a-z0-9_.]+)['"]/
|
|
7
|
+
|
|
8
|
+
RAW_MESSAGE_PATTERNS = [
|
|
9
|
+
/report_error\([^#\n]*,\s*['"]/,
|
|
10
|
+
/error!\(\s*['"]/,
|
|
11
|
+
/HaveAPI::ValidationError(?:\.new)?\(\s*['"]/,
|
|
12
|
+
/raise\s+HaveAPI::ValidationError,\s*['"]/,
|
|
13
|
+
/ret\[:message\]\s*=\s*['"]/,
|
|
14
|
+
/\|\|\s*['"][^'"]+(?:failed|error|denied|not found|invalid|requires|unsupported|missing)/,
|
|
15
|
+
/@message\s*=\s*take\(\s*:message\s*,\s*['"]/
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
PARAMETER_METHODS = %w[
|
|
19
|
+
bool custom datetime float id integer password resource string text
|
|
20
|
+
].freeze
|
|
21
|
+
PARAMETER_CALL_PATTERN = /\A\s*(?:#{PARAMETER_METHODS.join('|')})\b/
|
|
22
|
+
RAW_PARAMETER_METADATA_PATTERN = /\b(?:label|desc):\s*['"]/
|
|
23
|
+
|
|
24
|
+
def initialize(root:)
|
|
25
|
+
@root = root
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def check!
|
|
29
|
+
errors = []
|
|
30
|
+
errors.concat(missing_key_errors)
|
|
31
|
+
errors.concat(unused_key_errors)
|
|
32
|
+
errors.concat(raw_message_errors)
|
|
33
|
+
|
|
34
|
+
return true if errors.empty?
|
|
35
|
+
|
|
36
|
+
raise "i18n health check failed:\n#{errors.join("\n")}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def normalize!
|
|
40
|
+
locale_data.each do |locale, data|
|
|
41
|
+
file = File.join(locale_dir, "#{locale}.yml")
|
|
42
|
+
File.write(file, YAML.dump(locale.to_s => deep_sort(data)))
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def missing_key_errors
|
|
49
|
+
reference_locale = :en
|
|
50
|
+
reference_keys = locale_keys.fetch(reference_locale)
|
|
51
|
+
|
|
52
|
+
locale_keys.flat_map do |locale, keys|
|
|
53
|
+
next [] if locale == reference_locale
|
|
54
|
+
|
|
55
|
+
(reference_keys - keys).sort.map do |key|
|
|
56
|
+
"#{locale}: missing #{key}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def unused_key_errors
|
|
62
|
+
keys = locale_keys.fetch(:en)
|
|
63
|
+
used = used_keys
|
|
64
|
+
|
|
65
|
+
(keys - used).sort.map { |key| "unused translation key #{key}" }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def raw_message_errors
|
|
69
|
+
source_files.flat_map do |file|
|
|
70
|
+
rel = relative_path(file)
|
|
71
|
+
|
|
72
|
+
File.readlines(file, chomp: true).each_with_index.filter_map do |line, index|
|
|
73
|
+
stripped = line.strip
|
|
74
|
+
next if stripped.empty? || stripped.start_with?('#')
|
|
75
|
+
|
|
76
|
+
if RAW_MESSAGE_PATTERNS.any? { |pattern| pattern.match?(line) }
|
|
77
|
+
"#{rel}:#{index + 1}: raw user-facing framework message"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end + raw_parameter_metadata_errors
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def raw_parameter_metadata_errors
|
|
84
|
+
source_files.flat_map do |file|
|
|
85
|
+
rel = relative_path(file)
|
|
86
|
+
inside_parameter = false
|
|
87
|
+
remaining_lines = 0
|
|
88
|
+
|
|
89
|
+
File.readlines(file, chomp: true).each_with_index.filter_map do |line, index|
|
|
90
|
+
stripped = line.strip
|
|
91
|
+
next if stripped.empty? || stripped.start_with?('#')
|
|
92
|
+
|
|
93
|
+
if PARAMETER_CALL_PATTERN.match?(stripped)
|
|
94
|
+
inside_parameter = true
|
|
95
|
+
remaining_lines = 12
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if inside_parameter && RAW_PARAMETER_METADATA_PATTERN.match?(line)
|
|
99
|
+
"#{rel}:#{index + 1}: raw action parameter label/description"
|
|
100
|
+
end
|
|
101
|
+
ensure
|
|
102
|
+
if inside_parameter
|
|
103
|
+
remaining_lines -= 1
|
|
104
|
+
inside_parameter = false if remaining_lines <= 0 || parameter_call_end?(stripped)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def parameter_call_end?(line)
|
|
111
|
+
return false if line.nil?
|
|
112
|
+
|
|
113
|
+
stripped = line.strip
|
|
114
|
+
return false if stripped.empty?
|
|
115
|
+
return false if stripped.end_with?(',', '\\')
|
|
116
|
+
|
|
117
|
+
true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def used_keys
|
|
121
|
+
@used_keys ||= source_files.each_with_object(Set.new) do |file, ret|
|
|
122
|
+
File.read(file).scan(KEY_LITERAL_PATTERN) do |match|
|
|
123
|
+
ret << match.first
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def locale_keys
|
|
129
|
+
@locale_keys ||= locale_data.transform_values do |data|
|
|
130
|
+
flatten_keys(data)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def locale_data
|
|
135
|
+
@locale_data ||= Dir[File.join(locale_dir, '*.yml')].to_h do |file|
|
|
136
|
+
locale = File.basename(file, '.yml').to_sym
|
|
137
|
+
data = YAML.safe_load_file(file, aliases: true) || {}
|
|
138
|
+
|
|
139
|
+
[locale, data.fetch(locale.to_s)]
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def flatten_keys(hash, prefix = nil)
|
|
144
|
+
hash.each_with_object(Set.new) do |(key, value), ret|
|
|
145
|
+
name = [prefix, key].compact.join('.')
|
|
146
|
+
|
|
147
|
+
if value.is_a?(Hash)
|
|
148
|
+
ret.merge(flatten_keys(value, name))
|
|
149
|
+
else
|
|
150
|
+
ret << name
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def deep_sort(value)
|
|
156
|
+
case value
|
|
157
|
+
when Hash
|
|
158
|
+
value.keys.sort.to_h do |key|
|
|
159
|
+
[key, deep_sort(value[key])]
|
|
160
|
+
end
|
|
161
|
+
else
|
|
162
|
+
value
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def source_files
|
|
167
|
+
@source_files ||= Dir[File.join(@root, 'lib/haveapi/**/*.rb')].reject do |file|
|
|
168
|
+
rel = relative_path(file)
|
|
169
|
+
rel.start_with?('lib/haveapi/public/') ||
|
|
170
|
+
rel.start_with?('lib/haveapi/views/') ||
|
|
171
|
+
rel.start_with?('lib/haveapi/client_examples/') ||
|
|
172
|
+
rel.start_with?('lib/haveapi/spec/') ||
|
|
173
|
+
rel.start_with?('lib/haveapi/tasks/i18n.rb')
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def relative_path(file)
|
|
178
|
+
file.delete_prefix("#{@root}/")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def locale_dir
|
|
182
|
+
File.join(@root, 'lib/haveapi/locales')
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
namespace :i18n do
|
|
189
|
+
desc 'Check HaveAPI translation coverage and raw framework messages'
|
|
190
|
+
task :health do
|
|
191
|
+
HaveAPI::Tasks::I18nHealth.new(root: File.expand_path('../../..', __dir__)).check!
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
desc 'Normalize HaveAPI locale files'
|
|
195
|
+
task :normalize do
|
|
196
|
+
HaveAPI::Tasks::I18nHealth.new(root: File.expand_path('../../..', __dir__)).normalize!
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -23,13 +23,16 @@ module HaveAPI
|
|
|
23
23
|
take(:value)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
@message = take(
|
|
26
|
+
@message = take(
|
|
27
|
+
:message,
|
|
28
|
+
HaveAPI.message('haveapi.validators.acceptance.accepted', value: @value)
|
|
29
|
+
)
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
def describe
|
|
30
33
|
{
|
|
31
34
|
value: @value,
|
|
32
|
-
message: @message
|
|
35
|
+
message: HaveAPI.localize(@message)
|
|
33
36
|
}
|
|
34
37
|
end
|
|
35
38
|
|
|
@@ -24,7 +24,10 @@ module HaveAPI
|
|
|
24
24
|
@equal = take(:equal, true)
|
|
25
25
|
@message = take(
|
|
26
26
|
:message,
|
|
27
|
-
|
|
27
|
+
HaveAPI.message(
|
|
28
|
+
@equal ? 'haveapi.validators.confirmation.same' : 'haveapi.validators.confirmation.different',
|
|
29
|
+
parameter: @param
|
|
30
|
+
)
|
|
28
31
|
)
|
|
29
32
|
end
|
|
30
33
|
|
|
@@ -32,7 +35,7 @@ module HaveAPI
|
|
|
32
35
|
{
|
|
33
36
|
equal: @equal ? true : false,
|
|
34
37
|
parameter: @param,
|
|
35
|
-
message: @message
|
|
38
|
+
message: HaveAPI.localize(@message)
|
|
36
39
|
}
|
|
37
40
|
end
|
|
38
41
|
|
|
@@ -23,13 +23,13 @@ module HaveAPI
|
|
|
23
23
|
v.is_a?(::Symbol) ? v.to_s : v
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
@message = take(:message, '
|
|
26
|
+
@message = take(:message, HaveAPI.message('haveapi.validators.exclusion.excluded'))
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def describe
|
|
30
30
|
{
|
|
31
31
|
values: @values,
|
|
32
|
-
message: @message
|
|
32
|
+
message: HaveAPI.localize(@message)
|
|
33
33
|
}
|
|
34
34
|
end
|
|
35
35
|
|
|
@@ -20,7 +20,10 @@ module HaveAPI
|
|
|
20
20
|
@rx = simple? ? take : take(:rx)
|
|
21
21
|
@match = take(:match, true)
|
|
22
22
|
@desc = take(:desc)
|
|
23
|
-
@message = take(
|
|
23
|
+
@message = take(
|
|
24
|
+
:message,
|
|
25
|
+
@desc || HaveAPI.message('haveapi.validators.format.invalid')
|
|
26
|
+
)
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
def describe
|
|
@@ -28,7 +31,7 @@ module HaveAPI
|
|
|
28
31
|
rx: @rx.source,
|
|
29
32
|
match: @match,
|
|
30
33
|
description: @desc,
|
|
31
|
-
message: @message
|
|
34
|
+
message: HaveAPI.localize(@message)
|
|
32
35
|
}
|
|
33
36
|
end
|
|
34
37
|
|
|
@@ -31,13 +31,13 @@ module HaveAPI
|
|
|
31
31
|
@values = values.map { |v| v.is_a?(::Symbol) ? v.to_s : v }
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
@message = take(:message, '
|
|
34
|
+
@message = take(:message, HaveAPI.message('haveapi.validators.inclusion.included'))
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def describe
|
|
38
38
|
{
|
|
39
39
|
values: @values,
|
|
40
|
-
message: @message
|
|
40
|
+
message: HaveAPI.localize(@message)
|
|
41
41
|
}
|
|
42
42
|
end
|
|
43
43
|
|
|
@@ -30,16 +30,16 @@ module HaveAPI
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
msg = if @equals
|
|
33
|
-
|
|
33
|
+
HaveAPI.message('haveapi.validators.length.equals', equals: @equals)
|
|
34
34
|
|
|
35
35
|
elsif @min && !@max
|
|
36
|
-
|
|
36
|
+
HaveAPI.message('haveapi.validators.length.min', min: @min)
|
|
37
37
|
|
|
38
38
|
elsif !@min && @max
|
|
39
|
-
|
|
39
|
+
HaveAPI.message('haveapi.validators.length.max', max: @max)
|
|
40
40
|
|
|
41
41
|
else
|
|
42
|
-
|
|
42
|
+
HaveAPI.message('haveapi.validators.length.range', min: @min, max: @max)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
@message = take(:message, msg)
|
|
@@ -47,7 +47,7 @@ module HaveAPI
|
|
|
47
47
|
|
|
48
48
|
def describe
|
|
49
49
|
ret = {
|
|
50
|
-
message: @message
|
|
50
|
+
message: HaveAPI.localize(@message)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
if @equals
|