apiwork 0.3.1 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61f7ef3c54d36ed6bced4d7e84233b2ab4879c697a29468202c09b811074e2f6
4
- data.tar.gz: 0b285cc8cd1b5e5ad64b85d8e1bbe17cb82d281b2b69bf7b51a5468db2a9e43a
3
+ metadata.gz: 5716ac8fad77cf2a8acaaba2580b0ce732ba68ebeefb4cf33253a436eaa6578d
4
+ data.tar.gz: 68225f4d7f8d5dc9826073e1d83febc94dc10b500492530a98791abc77476621
5
5
  SHA512:
6
- metadata.gz: 31a41407721690942c50e1db93d5e602e67ee4dde083ff3e44b3a86e4935f874463856ee0217306156d4c7e29c994c46c9a21da9b185b9c6477a0b96b71aa6bd
7
- data.tar.gz: 4e197e9a7d1301ca771867b0ab85a777fdf0ab1e2951871ec156370f8118805cf0c502c65da8e6ed629d52eaab4ea8c7222639d2a6f0802077d401bb6c49588a
6
+ metadata.gz: 94be026e4bd4260f8a69a6d8655ff4f8329aed9f4a2c1a72275e2e895c3759e873d178ce8bf3c1b0c79c5e7f1e99d1cdad8f873ca6461ed31e962abb0afe2f88
7
+ data.tar.gz: 6982da2ffa9df35994e22b1fcb6f238f4ab0b7a8745ad85175839a446c2842de574de9423c51bcc3e33836f898e9f549b704536893d4944ac12dd1b1710ce3a1
@@ -15,7 +15,7 @@ module Apiwork
15
15
  if sti_base_representation?
16
16
  build_sti_response_union_type
17
17
  else
18
- register_type(representation_class.root_key.singular.to_sym) unless type?(scoped_type_name(nil))
18
+ register_type(representation_class.root_key.singular.to_sym)
19
19
 
20
20
  scoped_type_name(nil)
21
21
  end
@@ -8,12 +8,13 @@ module Apiwork
8
8
  class ContractBuilder < Adapter::Capability::Contract::Base
9
9
  def build
10
10
  build_enums
11
- build_payload_types
12
11
  build_nested_payload_union if api_class.representation_registry.nested_writable?(representation_class)
13
12
 
14
13
  %i[create update].each do |action_name|
15
14
  next unless scope.action?(action_name)
16
15
 
16
+ build_payload_type(action_name)
17
+
17
18
  payload_type_name = [action_name, 'payload'].join('_').to_sym
18
19
  next unless type?(payload_type_name)
19
20
 
@@ -26,6 +27,11 @@ module Apiwork
26
27
  end
27
28
  end
28
29
  end
30
+
31
+ return unless representation_class.subclass?
32
+
33
+ build_payload_type(:create)
34
+ build_payload_type(:update)
29
35
  end
30
36
 
31
37
  private
@@ -38,11 +44,6 @@ module Apiwork
38
44
  end
39
45
  end
40
46
 
41
- def build_payload_types
42
- build_payload_type(:create)
43
- build_payload_type(:update)
44
- end
45
-
46
47
  def build_payload_type(action_name)
47
48
  if sti_base_representation?
48
49
  build_sti_payload_union(action_name)
@@ -55,6 +56,9 @@ module Apiwork
55
56
  type_name = [action_name, 'payload'].join('_').to_sym
56
57
  return if type?(type_name)
57
58
 
59
+ writable_params = collect_writable_params(action_name)
60
+ return if writable_params.empty? && !representation_class.subclass?
61
+
58
62
  object(type_name, description: representation_class.description) do |object|
59
63
  if representation_class.subclass?
60
64
  parent_inheritance = representation_class.superclass.inheritance
@@ -66,7 +70,7 @@ module Apiwork
66
70
  )
67
71
  end
68
72
 
69
- collect_writable_params(action_name).each do |param_config|
73
+ writable_params.each do |param_config|
70
74
  object.param(param_config[:name], **param_config[:options])
71
75
  end
72
76
  end
@@ -276,6 +276,30 @@ module Apiwork
276
276
  register_union(name, deprecated:, description:, discriminator:, example:, &block)
277
277
  end
278
278
 
279
+ # @api public
280
+ # Supported locales for this API.
281
+ #
282
+ # Declares which locales this API supports. Used by introspection
283
+ # to validate locale parameters and included in introspection output.
284
+ #
285
+ # @param locale_keys [Array<Symbol>]
286
+ # The locale identifiers.
287
+ # @return [Array<Symbol>]
288
+ # @raise [ConfigurationError] if any locale is not a Symbol
289
+ #
290
+ # @example
291
+ # locales :en, :sv, :it
292
+ # api_class.locales # => [:en, :sv, :it]
293
+ def locales(*locale_keys)
294
+ return @locales if locale_keys.empty?
295
+
296
+ locale_keys = locale_keys.flatten.uniq
297
+ locale_keys.each do |locale_key|
298
+ raise ConfigurationError, "locales must be symbols, got #{locale_key.class}: #{locale_key}" unless locale_key.is_a?(Symbol)
299
+ end
300
+ @locales = locale_keys
301
+ end
302
+
279
303
  # @api public
280
304
  # API-wide error codes.
281
305
  #
@@ -534,6 +558,18 @@ module Apiwork
534
558
  @root_resource.with_options(options, &block)
535
559
  end
536
560
 
561
+ # @api public
562
+ # The fingerprint for this API.
563
+ #
564
+ # A 16-character hex digest derived from the application name and {.base_path}.
565
+ #
566
+ # @return [String]
567
+ def fingerprint
568
+ @fingerprint ||= Digest::SHA256.hexdigest(
569
+ [Rails.application.class.module_parent_name, base_path].join(':'),
570
+ )[0, 16]
571
+ end
572
+
537
573
  def register_object(name, deprecated: false, description: nil, example: nil, scope: nil, &block)
538
574
  type_registry.register(
539
575
  name,
@@ -602,9 +638,11 @@ module Apiwork
602
638
 
603
639
  def mount(base_path)
604
640
  @base_path = base_path
641
+ @fingerprint = nil
605
642
  @locale_key = nil
606
643
  @namespaces = nil
607
644
  @info = nil
645
+ @locales = []
608
646
  @raises = []
609
647
  @export_configs = {}
610
648
  @adapter_config = nil
@@ -38,10 +38,10 @@ module Apiwork
38
38
  # The discriminator field name. Unions only.
39
39
  # @param enum [Array, Symbol, nil] (nil)
40
40
  # The allowed values. Strings and integers only.
41
- # @param format [Symbol, nil] (nil) [:date, :datetime, :double, :email, :float, :hostname, :int32, :int64, :ipv4, :ipv6, :password, :url, :uuid]
41
+ # @param format [Symbol, nil] (nil) [:date, :datetime, :double, :email, :float, :hostname, :int32, :int64, :ipv4, :ipv6, :password, :text, :url, :uuid]
42
42
  # Format hint for exports. Does not change the type, but exports may add validation or documentation based on it.
43
43
  # Valid formats by type: `:decimal`/`:number` (`:double`, `:float`), `:integer` (`:int32`, `:int64`),
44
- # `:string` (`:date`, `:datetime`, `:email`, `:hostname`, `:ipv4`, `:ipv6`, `:password`, `:url`, `:uuid`).
44
+ # `:string` (`:date`, `:datetime`, `:email`, `:hostname`, `:ipv4`, `:ipv6`, `:password`, `:text`, `:url`, `:uuid`).
45
45
  # @param max [Integer, nil] (nil)
46
46
  # The maximum. For `:decimal`, `:integer`, `:number`: value. For `:string`: length.
47
47
  # @param min [Integer, nil] (nil)
@@ -48,10 +48,10 @@ module Apiwork
48
48
  # The allowed values.
49
49
  # @param example [Object, nil] (nil)
50
50
  # The example value. Metadata included in exports.
51
- # @param format [Symbol, nil] (nil) [:date, :datetime, :double, :email, :float, :hostname, :int32, :int64, :ipv4, :ipv6, :password, :url, :uuid]
51
+ # @param format [Symbol, nil] (nil) [:date, :datetime, :double, :email, :float, :hostname, :int32, :int64, :ipv4, :ipv6, :password, :text, :url, :uuid]
52
52
  # Format hint for exports. Does not change the type, but exports may add validation or documentation based on it.
53
53
  # Valid formats by type: `:decimal`/`:number` (`:double`, `:float`), `:integer` (`:int32`, `:int64`),
54
- # `:string` (`:date`, `:datetime`, `:email`, `:hostname`, `:ipv4`, `:ipv6`, `:password`, `:url`, `:uuid`).
54
+ # `:string` (`:date`, `:datetime`, `:email`, `:hostname`, `:ipv4`, `:ipv6`, `:password`, `:text`, `:url`, `:uuid`).
55
55
  # @param max [Integer, nil] (nil)
56
56
  # The maximum. For `:array`: size. For `:decimal`, `:integer`, `:number`: value. For `:string`: length.
57
57
  # @param min [Integer, nil] (nil)
@@ -43,10 +43,10 @@ module Apiwork
43
43
  # The discriminator field name. Unions only.
44
44
  # @param enum [Array, Symbol, nil] (nil)
45
45
  # The allowed values or enum reference. Strings and integers only.
46
- # @param format [Symbol, nil] (nil) [:date, :datetime, :double, :email, :float, :hostname, :int32, :int64, :ipv4, :ipv6, :password, :url, :uuid]
46
+ # @param format [Symbol, nil] (nil) [:date, :datetime, :double, :email, :float, :hostname, :int32, :int64, :ipv4, :ipv6, :password, :text, :url, :uuid]
47
47
  # Format hint for exports. Does not change the type, but exports may add validation or documentation based on it.
48
48
  # Valid formats by type: `:decimal`/`:number` (`:double`, `:float`), `:integer` (`:int32`, `:int64`),
49
- # `:string` (`:date`, `:datetime`, `:email`, `:hostname`, `:ipv4`, `:ipv6`, `:password`, `:url`, `:uuid`).
49
+ # `:string` (`:date`, `:datetime`, `:email`, `:hostname`, `:ipv4`, `:ipv6`, `:password`, `:text`, `:url`, `:uuid`).
50
50
  # @param max [Integer, nil] (nil)
51
51
  # The maximum. For `:decimal`, `:integer`, `:number`: value. For `:string`: length.
52
52
  # @param min [Integer, nil] (nil)
@@ -156,6 +156,13 @@ module Apiwork
156
156
 
157
157
  next if discriminator
158
158
 
159
+ if PRIMITIVES.key?(variant_type) && !value.is_a?(Hash) && !value.is_a?(Array)
160
+ coerced = coerce_primitive(value, variant_type)
161
+ return coerced unless coerced.nil?
162
+
163
+ next
164
+ end
165
+
159
166
  custom_shape = resolve_custom_shape(variant_type)
160
167
  next unless custom_shape
161
168
 
@@ -129,14 +129,7 @@ module Apiwork
129
129
  return nil if param_options[:optional]
130
130
  return nil if param_options[:nullable] && data.key?(name) && value.nil?
131
131
 
132
- missing = case param_options[:type]
133
- when :boolean, :string
134
- value.nil?
135
- else
136
- value.blank?
137
- end
138
-
139
- return nil unless missing
132
+ return nil unless value.nil?
140
133
 
141
134
  if param_options[:enum].present?
142
135
  Issue.new(
@@ -437,6 +430,8 @@ module Apiwork
437
430
  most_specific_error = error
438
431
  elsif error.code == :value_invalid && (most_specific_error.nil? || most_specific_error.code != :field_unknown)
439
432
  most_specific_error = error
433
+ elsif error.path.length > path.length && most_specific_error.nil?
434
+ most_specific_error = error
440
435
  end
441
436
  end
442
437
 
@@ -448,7 +443,7 @@ module Apiwork
448
443
  translate_detail(:type_invalid),
449
444
  path:,
450
445
  meta: {
451
- actual: value.is_a?(Hash) ? :hash : value.class.name.underscore.to_sym,
446
+ actual: value.is_a?(Hash) ? :object : value.class.name.underscore.to_sym,
452
447
  expected: expected_types.join(' | '),
453
448
  field: name,
454
449
  },
@@ -72,10 +72,10 @@ module Apiwork
72
72
  # The allowed values or enum reference.
73
73
  # @param example [Object, nil] (nil)
74
74
  # The example value. Metadata included in exports.
75
- # @param format [Symbol, nil] (nil) [:date, :datetime, :double, :email, :float, :hostname, :int32, :int64, :ipv4, :ipv6, :password, :url, :uuid]
75
+ # @param format [Symbol, nil] (nil) [:date, :datetime, :double, :email, :float, :hostname, :int32, :int64, :ipv4, :ipv6, :password, :text, :url, :uuid]
76
76
  # Format hint for exports. Does not change the type, but exports may add validation or documentation based on it.
77
77
  # Valid formats by type: `:decimal`/`:number` (`:double`, `:float`), `:integer` (`:int32`, `:int64`),
78
- # `:string` (`:date`, `:datetime`, `:email`, `:hostname`, `:ipv4`, `:ipv6`, `:password`, `:url`, `:uuid`).
78
+ # `:string` (`:date`, `:datetime`, `:email`, `:hostname`, `:ipv4`, `:ipv6`, `:password`, `:text`, `:url`, `:uuid`).
79
79
  # @param max [Integer, nil] (nil)
80
80
  # The maximum. For `:array`: size. For `:decimal`, `:integer`, `:number`: value. For `:string`: length.
81
81
  # @param min [Integer, nil] (nil)
@@ -82,7 +82,7 @@ module Apiwork
82
82
  #
83
83
  # @param enum [Array, Symbol, nil] (nil)
84
84
  # The allowed values.
85
- # @param format [Symbol, nil] (nil) [:date, :datetime, :email, :hostname, :ipv4, :ipv6, :password, :url, :uuid]
85
+ # @param format [Symbol, nil] (nil) [:date, :datetime, :email, :hostname, :ipv4, :ipv6, :password, :text, :url, :uuid]
86
86
  # Format hint for exports. Does not change the type, but exports may add validation or documentation based on it.
87
87
  # Valid formats by type: `:string`.
88
88
  # @param max [Integer, nil] (nil)
@@ -33,6 +33,14 @@ module Apiwork
33
33
  @dump[:base_path]
34
34
  end
35
35
 
36
+ # @api public
37
+ # The fingerprint for this API.
38
+ #
39
+ # @return [String]
40
+ def fingerprint
41
+ @dump[:fingerprint]
42
+ end
43
+
36
44
  # @api public
37
45
  # The info for this API.
38
46
  #
@@ -65,6 +73,14 @@ module Apiwork
65
73
  @enums ||= @dump[:enums].transform_values { |dump| Enum.new(dump) }
66
74
  end
67
75
 
76
+ # @api public
77
+ # The supported locales for this API.
78
+ #
79
+ # @return [Array<Symbol>]
80
+ def locales
81
+ @dump[:locales]
82
+ end
83
+
68
84
  # @api public
69
85
  # The error codes for this API.
70
86
  #
@@ -80,6 +96,8 @@ module Apiwork
80
96
  def to_h
81
97
  {
82
98
  base_path:,
99
+ fingerprint:,
100
+ locales:,
83
101
  enums: enums.transform_values(&:to_h),
84
102
  error_codes: error_codes.transform_values(&:to_h),
85
103
  info: info&.to_h,
@@ -18,7 +18,9 @@ module Apiwork
18
18
  base_path: @api_class.transform_path(@api_class.base_path),
19
19
  enums: type_dump_hash[:enums],
20
20
  error_codes: build_error_codes(collect_all_error_code_keys(resources)),
21
+ fingerprint: @api_class.fingerprint,
21
22
  info: build_info,
23
+ locales: @api_class.locales,
22
24
  types: type_dump_hash[:types],
23
25
  }
24
26
  end
@@ -4,11 +4,12 @@ module Apiwork
4
4
  module Introspection
5
5
  module Dump
6
6
  class Resource
7
- def initialize(resource, api_class, parent_identifiers: [], parent_param: nil, parent_singular: false)
7
+ def initialize(resource, api_class, parent_identifiers: [], parent_param: nil, parent_path: nil, parent_singular: false)
8
8
  @resource = resource
9
9
  @api_class = api_class
10
10
  @parent_identifiers = parent_identifiers
11
11
  @parent_param = parent_param
12
+ @parent_path = parent_path
12
13
  @parent_singular = parent_singular
13
14
  end
14
15
 
@@ -57,10 +58,11 @@ module Apiwork
57
58
  end
58
59
 
59
60
  def build_resource_path(formatted_segment)
60
- return formatted_segment if @parent_identifiers.empty?
61
- return formatted_segment if @parent_singular
61
+ return formatted_segment if @parent_path.nil?
62
+ return "#{@parent_path}/#{formatted_segment}" if @parent_singular
62
63
 
63
- ":#{@parent_param || "#{@parent_identifiers.last.singularize}_id"}/#{formatted_segment}"
64
+ param = @parent_param || "#{@parent_identifiers.last.singularize}_id"
65
+ "#{@parent_path}/:#{param}/#{formatted_segment}"
64
66
  end
65
67
 
66
68
  def build_nested_resources(resource_path)
@@ -70,6 +72,7 @@ module Apiwork
70
72
 
71
73
  nested_options = {
72
74
  parent_identifiers: child_parent_identifiers,
75
+ parent_path: resource_path,
73
76
  parent_singular: @resource.singular,
74
77
  }
75
78
  nested_options[:parent_param] = @resource.param&.to_s || "#{@resource.name.to_s.singularize}_id" unless @resource.singular
@@ -64,14 +64,6 @@ module Apiwork
64
64
  @dump[:default]
65
65
  end
66
66
 
67
- # @api public
68
- # Whether this param has a default.
69
- #
70
- # @return [Boolean]
71
- def default?
72
- @dump.key?(:default)
73
- end
74
-
75
67
  # @api public
76
68
  # The tag for this param.
77
69
  #
@@ -4,23 +4,36 @@ module Apiwork
4
4
  module Introspection
5
5
  class << self
6
6
  def api(api_class, locale: nil)
7
+ validate_locale(api_class, locale)
7
8
  with_locale(locale) { API.new(Dump.api(api_class)) }
8
9
  end
9
10
 
10
11
  def contract(contract_class, expand: false, locale: nil)
12
+ validate_locale(contract_class.api_class, locale)
11
13
  with_locale(locale) { Contract.new(Dump.contract(contract_class, expand:)) }
12
14
  end
13
15
 
14
16
  private
15
17
 
16
- def with_locale(locale, &block)
17
- return yield unless locale
18
+ def validate_locale(api_class, locale)
19
+ return unless locale
20
+ return unless api_class
18
21
 
19
- unless I18n.available_locales.include?(locale)
22
+ if api_class.locales.empty?
20
23
  raise ConfigurationError,
21
- "locale must be one of #{I18n.available_locales.inspect}, got #{locale.inspect}"
24
+ "locale :#{locale} was requested but no locales are defined. " \
25
+ "Add `locales #{locale.inspect}` to your API definition."
22
26
  end
23
27
 
28
+ return if api_class.locales.include?(locale)
29
+
30
+ raise ConfigurationError,
31
+ "locale must be one of #{api_class.locales.inspect}, got #{locale.inspect}"
32
+ end
33
+
34
+ def with_locale(locale, &block)
35
+ return yield unless locale
36
+
24
37
  I18n.with_locale(locale, &block)
25
38
  end
26
39
  end
@@ -71,7 +71,7 @@ module Apiwork
71
71
  # The allowed values.
72
72
  # @param example [String, nil] (nil)
73
73
  # The example value. Metadata included in exports.
74
- # @param format [Symbol, nil] (nil) [:date, :datetime, :email, :hostname, :ipv4, :ipv6, :password, :url, :uuid]
74
+ # @param format [Symbol, nil] (nil) [:date, :datetime, :email, :hostname, :ipv4, :ipv6, :password, :text, :url, :uuid]
75
75
  # Format hint for exports. Does not change the type, but exports may add validation or documentation based on it.
76
76
  # Valid formats by type: `:string`.
77
77
  # @param max [Integer, nil] (nil)
@@ -144,7 +144,7 @@ module Apiwork
144
144
  # The allowed values.
145
145
  # @param example [String, nil] (nil)
146
146
  # The example value. Metadata included in exports.
147
- # @param format [Symbol, nil] (nil) [:date, :datetime, :email, :hostname, :ipv4, :ipv6, :password, :url, :uuid]
147
+ # @param format [Symbol, nil] (nil) [:date, :datetime, :email, :hostname, :ipv4, :ipv6, :password, :text, :url, :uuid]
148
148
  # Format hint for exports. Does not change the type, but exports may add validation or documentation based on it.
149
149
  # Valid formats by type: `:string`.
150
150
  # @param max [Integer, nil] (nil)
@@ -18,7 +18,7 @@ module Apiwork
18
18
  decimal: %i[float double],
19
19
  integer: %i[int32 int64],
20
20
  number: %i[float double],
21
- string: %i[email uuid url date datetime ipv4 ipv6 password hostname],
21
+ string: %i[date datetime email hostname ipv4 ipv6 password text url uuid],
22
22
  }.freeze
23
23
 
24
24
  # @!attribute [r] description
@@ -197,11 +197,11 @@ module Apiwork
197
197
  # The example. Metadata included in exports.
198
198
  # @param filterable [Boolean] (false)
199
199
  # Whether the attribute is filterable.
200
- # @param format [Symbol, nil] (nil) [:date, :datetime, :double, :email, :float, :hostname, :int32, :int64, :ipv4, :ipv6, :password, :url, :uuid]
200
+ # @param format [Symbol, nil] (nil) [:date, :datetime, :double, :email, :float, :hostname, :int32, :int64, :ipv4, :ipv6, :password, :text, :url, :uuid]
201
201
  # Format hint for exports. Does not change the type, but exports may add validation or
202
202
  # documentation based on it. Valid formats by type: `:decimal`/`:number` (`:double`, `:float`),
203
203
  # `:integer` (`:int32`, `:int64`), `:string` (`:date`, `:datetime`, `:email`, `:hostname`,
204
- # `:ipv4`, `:ipv6`, `:password`, `:url`, `:uuid`).
204
+ # `:ipv4`, `:ipv6`, `:password`, `:text`, `:url`, `:uuid`).
205
205
  # @param max [Integer, nil] (nil)
206
206
  # The maximum. For `:array`: size. For `:decimal`, `:integer`, `:number`: value. For `:string`: length.
207
207
  # @param min [Integer, nil] (nil)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Apiwork
4
- VERSION = '0.3.1'
4
+ VERSION = '0.4.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apiwork
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - skiftle
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-04 00:00:00.000000000 Z
11
+ date: 2026-03-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '1.0'
47
+ version: '2.1'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '1.0'
54
+ version: '2.1'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rake
57
57
  requirement: !ruby/object:Gem::Requirement