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
@@ -45,12 +45,34 @@ module HaveAPI
45
45
  end
46
46
  end
47
47
 
48
- def describe(context)
48
+ def describe(context, type:)
49
49
  {
50
- input: @input && @input.describe(context),
51
- output: @output && @output.describe(context, metadata: true)
50
+ input: @input && @input.describe(
51
+ context,
52
+ i18n_path: Params.metadata_i18n_path(context, type, :input)
53
+ ),
54
+ output: @output && @output.describe(
55
+ context,
56
+ metadata: true,
57
+ i18n_path: Params.metadata_i18n_path(context, type, :output)
58
+ )
52
59
  }
53
60
  end
61
+
62
+ def parameter_metadata_i18n_items(context, type:)
63
+ [
64
+ *@input&.parameter_metadata_i18n_items(
65
+ context,
66
+ i18n_path: Params.metadata_i18n_path(context, type, :input),
67
+ meta_type: type
68
+ ),
69
+ *@output&.parameter_metadata_i18n_items(
70
+ context,
71
+ i18n_path: Params.metadata_i18n_path(context, type, :output),
72
+ meta_type: type
73
+ )
74
+ ].compact
75
+ end
54
76
  end
55
77
  end
56
78
  end
@@ -81,7 +81,10 @@ module HaveAPI::ModelAdapters
81
81
 
82
82
  if limit && limit > HaveAPI::Actions::Paginable::MAX_LIMIT
83
83
  error!(
84
- "limit has to be maximally #{HaveAPI::Actions::Paginable::MAX_LIMIT}",
84
+ HaveAPI.message(
85
+ 'haveapi.pagination.limit_max',
86
+ max: HaveAPI::Actions::Paginable::MAX_LIMIT
87
+ ),
85
88
  {},
86
89
  http_status: 400
87
90
  )
@@ -174,18 +177,18 @@ module HaveAPI::ModelAdapters
174
177
  if raw.nil?
175
178
  return nil if allow_null
176
179
 
177
- raise HaveAPI::ValidationError, "not a valid id #{original.inspect}"
180
+ raise invalid_id(original)
178
181
  end
179
182
 
180
183
  if raw.is_a?(Array) || raw.is_a?(Hash) || [true, false].include?(raw)
181
- raise HaveAPI::ValidationError, "not a valid id #{original.inspect}"
184
+ raise invalid_id(original)
182
185
  elsif raw.is_a?(String)
183
186
  stripped = raw.strip
184
187
 
185
188
  if stripped.empty?
186
189
  return nil if allow_null
187
190
 
188
- raise HaveAPI::ValidationError, "not a valid id #{original.inspect}"
191
+ raise invalid_id(original)
189
192
  end
190
193
 
191
194
  raw = stripped
@@ -193,7 +196,7 @@ module HaveAPI::ModelAdapters
193
196
 
194
197
  value = if integer_pk?(model)
195
198
  id = coerce_integer_id(raw, original)
196
- raise HaveAPI::ValidationError, "not a valid id #{original.inspect}" if id < 0
199
+ raise invalid_id(original) if id < 0
197
200
 
198
201
  id
199
202
  else
@@ -207,14 +210,14 @@ module HaveAPI::ModelAdapters
207
210
  end
208
211
 
209
212
  if ret.nil? && !allow_null
210
- raise HaveAPI::ValidationError, 'resource not found'
213
+ raise resource_not_found
211
214
  end
212
215
 
213
216
  ret
214
217
  rescue ::ActiveRecord::RecordNotFound
215
- raise HaveAPI::ValidationError, 'resource not found'
218
+ raise resource_not_found
216
219
  rescue ArgumentError, TypeError
217
- raise HaveAPI::ValidationError, "not a valid id #{original.inspect}"
220
+ raise invalid_id(original)
218
221
  end
219
222
 
220
223
  def self.integer_pk?(model)
@@ -232,7 +235,7 @@ module HaveAPI::ModelAdapters
232
235
  raw
233
236
  when Float
234
237
  unless raw.finite? && (raw % 1) == 0
235
- raise HaveAPI::ValidationError, "not a valid id #{original.inspect}"
238
+ raise invalid_id(original)
236
239
  end
237
240
 
238
241
  raw.to_i
@@ -240,23 +243,38 @@ module HaveAPI::ModelAdapters
240
243
  s = raw.strip
241
244
 
242
245
  if s.empty? || !s.match?(/\A[+-]?\d+\z/)
243
- raise HaveAPI::ValidationError, "not a valid id #{original.inspect}"
246
+ raise invalid_id(original)
244
247
  end
245
248
 
246
249
  Integer(s, 10)
247
250
  else
248
- raise HaveAPI::ValidationError, "not a valid id #{original.inspect}"
251
+ raise invalid_id(original)
249
252
  end
250
253
  end
254
+
255
+ def self.invalid_id(original)
256
+ HaveAPI::ValidationError.new(
257
+ HaveAPI.message('haveapi.validation.invalid_id', value: original.inspect)
258
+ )
259
+ end
260
+
261
+ def self.resource_not_found
262
+ HaveAPI::ValidationError.new(
263
+ HaveAPI.message('haveapi.validation.resource_not_found')
264
+ )
265
+ end
251
266
  end
252
267
 
253
268
  class Output < ::HaveAPI::ModelAdapter::Output
254
269
  def self.used_by(action)
255
270
  action.meta(:object) do
256
271
  output do
257
- custom :path_params, label: 'URL parameters',
258
- desc: 'An array of parameters needed to resolve URL to this object'
259
- bool :resolved, label: 'Resolved', desc: 'True if the association is resolved'
272
+ custom :path_params,
273
+ label: HaveAPI.message('haveapi.parameters.active_record.path_params.label'),
274
+ desc: HaveAPI.message('haveapi.parameters.active_record.path_params.description')
275
+ bool :resolved,
276
+ label: HaveAPI.message('haveapi.parameters.active_record.resolved.label'),
277
+ desc: HaveAPI.message('haveapi.parameters.active_record.resolved.description')
260
278
  end
261
279
  end
262
280
 
@@ -268,30 +286,26 @@ module HaveAPI::ModelAdapters
268
286
  elsif raw.is_a?(Array)
269
287
  raw
270
288
  else
271
- raise HaveAPI::ValidationError, 'includes must be a string or array'
289
+ raise HaveAPI::ValidationError,
290
+ HaveAPI.message('haveapi.validation.includes_string_or_array')
272
291
  end
273
292
 
274
293
  values.map do |value|
275
294
  unless value.is_a?(String)
276
- raise HaveAPI::ValidationError, 'includes must contain only strings'
295
+ raise HaveAPI::ValidationError,
296
+ HaveAPI.message('haveapi.validation.includes_only_strings')
277
297
  end
278
298
 
279
299
  value.strip
280
300
  end
281
301
  end
282
302
 
283
- desc = <<~END
284
- A list of names of associated resources separated by a comma.
285
- Nested associations are declared with '__' between resource names.
286
- For example, 'user,node' will resolve the two associations.
287
- To resolve further associations of node, use e.g. 'user,node__location',
288
- to go even deeper, use e.g. 'user,node__location__environment'.
289
- END
290
-
291
303
  action.meta(:global) do
292
304
  input do
293
- custom :includes, label: 'Included associations',
294
- desc:, &clean
305
+ custom :includes,
306
+ label: HaveAPI.message('haveapi.parameters.active_record.includes.label'),
307
+ desc: HaveAPI.message('haveapi.parameters.active_record.includes.description'),
308
+ &clean
295
309
  end
296
310
  end
297
311
 
@@ -51,8 +51,8 @@ module HaveAPI
51
51
  ret.update({
52
52
  status:,
53
53
  response:,
54
- message:,
55
- errors:
54
+ message: HaveAPI.localize(message),
55
+ errors: HaveAPI.localize(errors)
56
56
  })
57
57
  ret
58
58
  end
@@ -0,0 +1,179 @@
1
+ module HaveAPI::Parameters
2
+ module MetadataI18n
3
+ def metadata_i18n_catalog_items(context, i18n_path)
4
+ %i[label description].filter_map do |kind|
5
+ fallback = metadata_i18n_fallback(kind)
6
+ keys = metadata_i18n_keys(context, i18n_path, kind, fallback)
7
+ value = HaveAPI.localize(fallback).to_s.strip
8
+
9
+ next if keys.empty? || value.empty?
10
+
11
+ {
12
+ param: HaveAPI::Params.i18n_segment(@name),
13
+ kind: metadata_i18n_key_suffix(kind),
14
+ keys:,
15
+ value:
16
+ }
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def localized_label(context, i18n_path)
23
+ localized_metadata(context, i18n_path, :label, @label)
24
+ end
25
+
26
+ def localized_description(context, i18n_path)
27
+ localized_metadata(context, i18n_path, :description, @desc)
28
+ end
29
+
30
+ def localized_metadata(context, i18n_path, kind, fallback)
31
+ keys = metadata_i18n_keys(context, i18n_path, kind, fallback)
32
+ default = HaveAPI.localize(fallback)
33
+
34
+ return default if keys.empty?
35
+
36
+ keys.each do |key|
37
+ value = ::I18n.t(key, default: nil)
38
+ return value unless value.nil?
39
+ end
40
+
41
+ default
42
+ end
43
+
44
+ def metadata_i18n_keys(context, i18n_path, kind, fallback)
45
+ explicit_key = explicit_metadata_i18n_key(kind)
46
+
47
+ return [normalize_metadata_i18n_key(context, explicit_key)] if explicit_key
48
+ return [] if fallback.is_a?(HaveAPI::LocalizedMessage)
49
+ return [] unless i18n_path
50
+
51
+ scope = parameter_i18n_scope(context)
52
+ return [] unless scope
53
+
54
+ exact_key = [
55
+ scope,
56
+ i18n_path,
57
+ HaveAPI::Params.i18n_segment(@name),
58
+ metadata_i18n_key_suffix(kind)
59
+ ].join('.')
60
+
61
+ [exact_key, *metadata_i18n_default_keys(context, i18n_path, kind, scope)].uniq
62
+ end
63
+
64
+ def metadata_i18n_fallback(kind)
65
+ case kind
66
+ when :label
67
+ @label
68
+ when :description
69
+ @desc
70
+ else
71
+ raise ArgumentError, "unsupported parameter metadata kind #{kind.inspect}"
72
+ end
73
+ end
74
+
75
+ def explicit_metadata_i18n_key(kind)
76
+ case kind
77
+ when :label
78
+ @label_key
79
+ when :description
80
+ @desc_key
81
+ else
82
+ raise ArgumentError, "unsupported parameter metadata kind #{kind.inspect}"
83
+ end
84
+ end
85
+
86
+ def normalize_metadata_i18n_key(context, key)
87
+ str = key.to_s
88
+ return str if str.include?('.')
89
+
90
+ scope = parameter_i18n_scope(context)
91
+ scope ? "#{scope}.#{str}" : str
92
+ end
93
+
94
+ def parameter_i18n_scope(context)
95
+ return unless context.respond_to?(:server)
96
+ return unless context.server.respond_to?(:parameter_i18n_scope)
97
+
98
+ scope = context.server.parameter_i18n_scope
99
+ return if scope.nil? || scope == false
100
+
101
+ Array(scope).flat_map { |segment| segment.to_s.split('.') }
102
+ .map { |segment| HaveAPI::Params.i18n_segment(segment) }
103
+ .join('.')
104
+ end
105
+
106
+ def metadata_i18n_key_suffix(kind)
107
+ kind == :description ? 'description' : kind.to_s
108
+ end
109
+
110
+ def metadata_i18n_default_keys(context, i18n_path, kind, scope)
111
+ param = HaveAPI::Params.i18n_segment(@name)
112
+ suffix = metadata_i18n_key_suffix(kind)
113
+ resource_prefix = metadata_i18n_resource_prefix(context)
114
+ path = metadata_i18n_path_parts(i18n_path)
115
+ keys = []
116
+
117
+ if path[:meta_type]
118
+ if resource_prefix
119
+ keys << [
120
+ scope,
121
+ resource_prefix,
122
+ 'meta',
123
+ path.fetch(:meta_type),
124
+ path.fetch(:direction),
125
+ param,
126
+ suffix
127
+ ].join('.')
128
+ end
129
+
130
+ keys << [
131
+ scope,
132
+ 'meta',
133
+ path.fetch(:meta_type),
134
+ path.fetch(:direction),
135
+ param,
136
+ suffix
137
+ ].join('.')
138
+ elsif resource_prefix
139
+ keys << [
140
+ scope,
141
+ resource_prefix,
142
+ path.fetch(:direction),
143
+ param,
144
+ suffix
145
+ ].join('.')
146
+ keys << [
147
+ scope,
148
+ resource_prefix,
149
+ 'attributes',
150
+ param,
151
+ suffix
152
+ ].join('.')
153
+ end
154
+
155
+ keys << [scope, 'attributes', param, suffix].join('.') unless path[:meta_type]
156
+ keys
157
+ end
158
+
159
+ def metadata_i18n_resource_prefix(context)
160
+ return unless context.respond_to?(:resource_path) && context.resource_path
161
+
162
+ [
163
+ 'resources',
164
+ *context.resource_path.map { |segment| HaveAPI::Params.i18n_segment(segment) }
165
+ ].join('.')
166
+ end
167
+
168
+ def metadata_i18n_path_parts(i18n_path)
169
+ parts = i18n_path.to_s.split('.')
170
+
171
+ return { direction: parts.last } unless parts[-3] == 'meta'
172
+
173
+ {
174
+ meta_type: parts[-2],
175
+ direction: parts[-1]
176
+ }
177
+ end
178
+ end
179
+ end
@@ -1,16 +1,23 @@
1
+ require_relative 'metadata_i18n'
2
+
1
3
  module HaveAPI::Parameters
2
4
  class Resource
5
+ include MetadataI18n
6
+
3
7
  attr_reader :name, :resource, :label, :desc, :type, :value_id, :value_label,
4
8
  :choices, :value_params
5
9
 
6
10
  def initialize(resource, name: nil, label: nil, desc: nil,
7
11
  choices: nil, value_id: :id, value_label: :label, required: nil,
8
- db_name: nil, fetch: nil, nullable: nil)
12
+ db_name: nil, fetch: nil, nullable: nil, label_key: nil,
13
+ desc_key: nil)
9
14
  @resource = resource
10
15
  @resource_path = build_resource_path(resource)
11
16
  @name = name || resource.resource_name.underscore.to_sym
12
17
  @label = label || (name && name.to_s.capitalize) || resource.resource_name
13
18
  @desc = desc
19
+ @label_key = label_key
20
+ @desc_key = desc_key
14
21
  @choices = choices || @resource::Index
15
22
  @value_id = value_id
16
23
  @value_label = value_label
@@ -46,7 +53,7 @@ module HaveAPI::Parameters
46
53
  @resource::Index
47
54
  end
48
55
 
49
- def describe(context)
56
+ def describe(context, i18n_path: nil)
50
57
  val_path = context.path_for(
51
58
  @resource::Show,
52
59
  context.endpoint && context.action_prepare && context.layout == :object && context.call_path_params(context.action, context.action_prepare)
@@ -62,8 +69,8 @@ module HaveAPI::Parameters
62
69
  {
63
70
  required: required?,
64
71
  nullable: nullable?,
65
- label: @label,
66
- description: @desc,
72
+ label: localized_label(context, i18n_path),
73
+ description: localized_description(context, i18n_path),
67
74
  type: 'Resource',
68
75
  resource: @resource_path,
69
76
  value_id: @value_id,
@@ -101,7 +108,7 @@ module HaveAPI::Parameters
101
108
  if raw.nil?
102
109
  return nil if nullable?
103
110
 
104
- raise HaveAPI::ValidationError, 'cannot be null'
111
+ raise validation_error('haveapi.validation.cannot_be_null')
105
112
  end
106
113
 
107
114
  if raw.is_a?(String)
@@ -133,7 +140,7 @@ module HaveAPI::Parameters
133
140
  def strip_string(value)
134
141
  value.strip
135
142
  rescue ArgumentError, Encoding::CompatibilityError
136
- raise HaveAPI::ValidationError, 'invalid string encoding'
143
+ raise validation_error('haveapi.validation.invalid_string_encoding')
137
144
  end
138
145
 
139
146
  def build_resource_path(r)
@@ -175,7 +182,11 @@ module HaveAPI::Parameters
175
182
  action = show_action.new(context.request, context.version, path_params, {}, child_context)
176
183
  return if action.authorized?(context.current_user)
177
184
 
178
- raise HaveAPI::ValidationError, 'resource not found'
185
+ raise validation_error('haveapi.validation.resource_not_found')
186
+ end
187
+
188
+ def validation_error(key, **values)
189
+ HaveAPI::ValidationError.new(HaveAPI.message(key, **values))
179
190
  end
180
191
  end
181
192
  end
@@ -1,11 +1,14 @@
1
1
  require 'date'
2
2
  require 'time'
3
+ require_relative 'metadata_i18n'
3
4
 
4
5
  module HaveAPI::Parameters
5
6
  class Typed
7
+ include MetadataI18n
8
+
6
9
  ATTRIBUTES = %i[
7
10
  label desc type db_name default fill clean protected load_validators
8
- nullable symbolize_keys
11
+ nullable symbolize_keys label_key desc_key
9
12
  ].freeze
10
13
 
11
14
  attr_reader :name, :label, :desc, :type, :default
@@ -52,12 +55,12 @@ module HaveAPI::Parameters
52
55
  @load_validators.nil? || @load_validators
53
56
  end
54
57
 
55
- def describe(context)
58
+ def describe(context, i18n_path: nil)
56
59
  {
57
60
  required: required?,
58
61
  nullable: nullable?,
59
- label: @label,
60
- description: @desc,
62
+ label: localized_label(context, i18n_path),
63
+ description: localized_description(context, i18n_path),
61
64
  type: @type ? @type.to_s : String.to_s,
62
65
  validators: @validators ? @validators.describe : {},
63
66
  default: @default,
@@ -88,7 +91,7 @@ module HaveAPI::Parameters
88
91
  if raw.nil?
89
92
  return nil if nullable?
90
93
 
91
- raise HaveAPI::ValidationError, 'cannot be null'
94
+ raise validation_error('haveapi.validation.cannot_be_null')
92
95
  end
93
96
 
94
97
  if raw.is_a?(String)
@@ -154,7 +157,7 @@ module HaveAPI::Parameters
154
157
 
155
158
  def validate_cleaned_value(value)
156
159
  if value.nil? && !nullable?
157
- raise HaveAPI::ValidationError, 'cannot be null'
160
+ raise validation_error('haveapi.validation.cannot_be_null')
158
161
  end
159
162
 
160
163
  value
@@ -188,7 +191,7 @@ module HaveAPI::Parameters
188
191
  def strip_string(value)
189
192
  value.strip
190
193
  rescue ArgumentError, Encoding::CompatibilityError
191
- raise HaveAPI::ValidationError, 'invalid string encoding'
194
+ raise validation_error('haveapi.validation.invalid_string_encoding')
192
195
  end
193
196
 
194
197
  def coerce_integer(raw)
@@ -197,7 +200,7 @@ module HaveAPI::Parameters
197
200
  raw
198
201
  when Float
199
202
  unless raw.finite? && (raw % 1) == 0
200
- raise HaveAPI::ValidationError, "not a valid integer #{raw.inspect}"
203
+ raise validation_error('haveapi.types.invalid_integer', value: raw.inspect)
201
204
  end
202
205
 
203
206
  raw.to_i
@@ -205,12 +208,12 @@ module HaveAPI::Parameters
205
208
  s = strip_string(raw)
206
209
 
207
210
  if s.empty? || !s.match?(/\A[+-]?\d+\z/)
208
- raise HaveAPI::ValidationError, "not a valid integer #{raw.inspect}"
211
+ raise validation_error('haveapi.types.invalid_integer', value: raw.inspect)
209
212
  end
210
213
 
211
214
  Integer(s, 10)
212
215
  else
213
- raise HaveAPI::ValidationError, "not a valid integer #{raw.inspect}"
216
+ raise validation_error('haveapi.types.invalid_integer', value: raw.inspect)
214
217
  end
215
218
  end
216
219
 
@@ -220,19 +223,19 @@ module HaveAPI::Parameters
220
223
 
221
224
  elsif raw.is_a?(String)
222
225
  s = strip_string(raw)
223
- raise HaveAPI::ValidationError, "not a valid float #{raw.inspect}" if s.empty?
226
+ raise validation_error('haveapi.types.invalid_float', value: raw.inspect) if s.empty?
224
227
 
225
228
  begin
226
229
  f = Float(s)
227
230
  rescue ArgumentError
228
- raise HaveAPI::ValidationError, "not a valid float #{raw.inspect}"
231
+ raise validation_error('haveapi.types.invalid_float', value: raw.inspect)
229
232
  end
230
233
 
231
234
  else
232
- raise HaveAPI::ValidationError, "not a valid float #{raw.inspect}"
235
+ raise validation_error('haveapi.types.invalid_float', value: raw.inspect)
233
236
  end
234
237
 
235
- raise HaveAPI::ValidationError, "not a valid float #{raw.inspect}" unless f.finite?
238
+ raise validation_error('haveapi.types.invalid_float', value: raw.inspect) unless f.finite?
236
239
 
237
240
  f
238
241
  end
@@ -247,35 +250,39 @@ module HaveAPI::Parameters
247
250
 
248
251
  elsif raw.is_a?(String)
249
252
  s = strip_string(raw)
250
- raise HaveAPI::ValidationError, "not a valid boolean #{raw.inspect}" if s.empty?
253
+ raise validation_error('haveapi.types.invalid_boolean', value: raw.inspect) if s.empty?
251
254
 
252
255
  return true if %w[true t yes y 1].include?(s.downcase)
253
256
  return false if %w[false f no n 0].include?(s.downcase)
254
257
  end
255
258
 
256
- raise HaveAPI::ValidationError, "not a valid boolean #{raw.inspect}"
259
+ raise validation_error('haveapi.types.invalid_boolean', value: raw.inspect)
257
260
  end
258
261
 
259
262
  def coerce_datetime(raw)
260
263
  unless raw.is_a?(String)
261
- raise HaveAPI::ValidationError, "not in ISO 8601 format '#{raw}'"
264
+ raise validation_error('haveapi.types.invalid_datetime', value: raw)
262
265
  end
263
266
 
264
267
  if strip_string(raw).empty?
265
- raise HaveAPI::ValidationError, "not in ISO 8601 format '#{raw}'"
268
+ raise validation_error('haveapi.types.invalid_datetime', value: raw)
266
269
  end
267
270
 
268
271
  DateTime.iso8601(raw).to_time
269
272
  rescue ArgumentError, TypeError
270
- raise HaveAPI::ValidationError, "not in ISO 8601 format '#{raw}'"
273
+ raise validation_error('haveapi.types.invalid_datetime', value: raw)
271
274
  end
272
275
 
273
276
  def coerce_string(raw)
274
277
  if raw.is_a?(Array) || raw.is_a?(Hash)
275
- raise HaveAPI::ValidationError, "not a valid string #{raw.inspect}"
278
+ raise validation_error('haveapi.types.invalid_string', value: raw.inspect)
276
279
  end
277
280
 
278
281
  raw.to_s
279
282
  end
283
+
284
+ def validation_error(key, **values)
285
+ HaveAPI::ValidationError.new(HaveAPI.message(key, **values))
286
+ end
280
287
  end
281
288
  end