apiwork 0.3.1 → 0.5.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/apiwork/adapter/serializer/resource/base.rb +15 -0
  3. data/lib/apiwork/adapter/serializer/resource/default/contract_builder.rb +4 -3
  4. data/lib/apiwork/adapter/standard/capability/writing/contract_builder.rb +13 -9
  5. data/lib/apiwork/api/base.rb +105 -17
  6. data/lib/apiwork/api/element.rb +35 -4
  7. data/lib/apiwork/api/object.rb +72 -7
  8. data/lib/apiwork/api/router.rb +16 -0
  9. data/lib/apiwork/configuration/validatable.rb +1 -0
  10. data/lib/apiwork/configuration.rb +2 -0
  11. data/lib/apiwork/contract/element.rb +19 -4
  12. data/lib/apiwork/contract/object/coercer.rb +31 -2
  13. data/lib/apiwork/contract/object/deserializer.rb +5 -1
  14. data/lib/apiwork/contract/object/transformer.rb +15 -2
  15. data/lib/apiwork/contract/object/validator.rb +49 -11
  16. data/lib/apiwork/contract/object.rb +79 -9
  17. data/lib/apiwork/element.rb +34 -1
  18. data/lib/apiwork/export/base.rb +1 -4
  19. data/lib/apiwork/export/builder_mapper.rb +184 -0
  20. data/lib/apiwork/export/open_api.rb +9 -2
  21. data/lib/apiwork/export/sorbus.rb +5 -1
  22. data/lib/apiwork/export/sorbus_mapper.rb +3 -7
  23. data/lib/apiwork/export/type_analysis.rb +20 -6
  24. data/lib/apiwork/export/type_script.rb +4 -1
  25. data/lib/apiwork/export/type_script_mapper.rb +25 -2
  26. data/lib/apiwork/export/zod.rb +9 -0
  27. data/lib/apiwork/export/zod_mapper.rb +22 -1
  28. data/lib/apiwork/introspection/api.rb +18 -0
  29. data/lib/apiwork/introspection/dump/action.rb +1 -1
  30. data/lib/apiwork/introspection/dump/api.rb +2 -0
  31. data/lib/apiwork/introspection/dump/param.rb +36 -20
  32. data/lib/apiwork/introspection/dump/resource.rb +7 -4
  33. data/lib/apiwork/introspection/dump/type.rb +31 -25
  34. data/lib/apiwork/introspection/param/array.rb +26 -0
  35. data/lib/apiwork/introspection/param/base.rb +15 -25
  36. data/lib/apiwork/introspection/param/binary.rb +36 -0
  37. data/lib/apiwork/introspection/param/boolean.rb +36 -0
  38. data/lib/apiwork/introspection/param/date.rb +36 -0
  39. data/lib/apiwork/introspection/param/date_time.rb +36 -0
  40. data/lib/apiwork/introspection/param/decimal.rb +26 -0
  41. data/lib/apiwork/introspection/param/integer.rb +26 -0
  42. data/lib/apiwork/introspection/param/number.rb +26 -0
  43. data/lib/apiwork/introspection/param/record.rb +71 -0
  44. data/lib/apiwork/introspection/param/string.rb +26 -0
  45. data/lib/apiwork/introspection/param/time.rb +36 -0
  46. data/lib/apiwork/introspection/param/uuid.rb +36 -0
  47. data/lib/apiwork/introspection/param.rb +1 -0
  48. data/lib/apiwork/introspection.rb +17 -4
  49. data/lib/apiwork/object.rb +246 -4
  50. data/lib/apiwork/representation/attribute.rb +2 -2
  51. data/lib/apiwork/representation/base.rb +107 -2
  52. data/lib/apiwork/representation/element.rb +15 -5
  53. data/lib/apiwork/version.rb +1 -1
  54. metadata +6 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61f7ef3c54d36ed6bced4d7e84233b2ab4879c697a29468202c09b811074e2f6
4
- data.tar.gz: 0b285cc8cd1b5e5ad64b85d8e1bbe17cb82d281b2b69bf7b51a5468db2a9e43a
3
+ metadata.gz: 02f6a539dd3708f867af6e298daec039dd03bc4ea38f22e21172940332ee800f
4
+ data.tar.gz: a5603ca5b09b0a8baa3195629b848ceb186e3b1e1d40b987539f02195a76d76f
5
5
  SHA512:
6
- metadata.gz: 31a41407721690942c50e1db93d5e602e67ee4dde083ff3e44b3a86e4935f874463856ee0217306156d4c7e29c994c46c9a21da9b185b9c6477a0b96b71aa6bd
7
- data.tar.gz: 4e197e9a7d1301ca771867b0ab85a777fdf0ab1e2951871ec156370f8118805cf0c502c65da8e6ed629d52eaab4ea8c7222639d2a6f0802077d401bb6c49588a
6
+ metadata.gz: 187ae2ea46e4df89ccca25bc2b88fd5a81b31f1887def0f7acfdb3dbc1fad3284f4b6f91c13956631aa20d6dd6793ac4ff1015ce36c6150c0b29dc669ca2aa18
7
+ data.tar.gz: 844cb5df21fe9803dabd1e450ce6a9c2f1d7f745fcb40d1d92dbfc2d95660d9ee1be719856d1b922ca5a0eeb11f8e711ce8be1eb423f20d90d348f7c55335a32
@@ -58,6 +58,8 @@ module Apiwork
58
58
  end
59
59
 
60
60
  def contract_types(contract_class)
61
+ register_representation_types(contract_class)
62
+
61
63
  builder_class = self.class.contract_builder
62
64
  return unless builder_class
63
65
 
@@ -77,6 +79,19 @@ module Apiwork
77
79
  def serialize(resource, context:, serialize_options:)
78
80
  raise NotImplementedError
79
81
  end
82
+
83
+ private
84
+
85
+ def register_representation_types(contract_class)
86
+ representation_class.type_definitions.each do |name, type_definition|
87
+ case type_definition[:kind]
88
+ when :object
89
+ contract_class.object(name, **type_definition[:options], &type_definition[:block])
90
+ when :union
91
+ contract_class.union(name, **type_definition[:options], &type_definition[:block])
92
+ end
93
+ end
94
+ end
80
95
  end
81
96
  end
82
97
  end
@@ -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
@@ -72,7 +72,7 @@ module Apiwork
72
72
 
73
73
  element = attribute.element
74
74
  if element
75
- if element.type == :array
75
+ if [:array, :record].include?(element.type)
76
76
  param_options[:of] = element.inner
77
77
  else
78
78
  param_options[:shape] = element.shape
@@ -95,7 +95,8 @@ module Apiwork
95
95
  }
96
96
 
97
97
  if association.singular?
98
- object.param(name, type: association_type || :object, **base_options)
98
+ resolved_type = association_type || :object
99
+ object.param(name, type: resolved_type, custom_type: association_type, **base_options)
99
100
  elsif association.collection?
100
101
  if association_type
101
102
  object.param(name, type: :array, **base_options) do |param|
@@ -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
@@ -86,7 +90,7 @@ module Apiwork
86
90
  writable = action_name != :delete
87
91
 
88
92
  object(type_name) do |object|
89
- object.literal(Constants::OP, optional: true, value: action_name.to_s)
93
+ object.literal(Constants::OP, value: action_name.to_s)
90
94
  object.param(:id, optional: action_name != :delete, type: primary_key_type) unless action_name == :create
91
95
 
92
96
  next unless writable
@@ -172,7 +176,7 @@ module Apiwork
172
176
  if attribute.element
173
177
  element = attribute.element
174
178
 
175
- if element.type == :array
179
+ if [:array, :record].include?(element.type)
176
180
  options[:of] = element.inner
177
181
  else
178
182
  options[:shape] = element.shape
@@ -27,6 +27,7 @@ module Apiwork
27
27
  # @return [String]
28
28
  attr_reader :base_path,
29
29
  :enum_registry,
30
+ :explorer_config,
30
31
  :export_configs,
31
32
  :representation_registry,
32
33
  :root_resource,
@@ -133,6 +134,44 @@ module Apiwork
133
134
  block.arity.positive? ? yield(@export_configs[name]) : @export_configs[name].instance_eval(&block)
134
135
  end
135
136
 
137
+ # @api public
138
+ # Configures the explorer for this API.
139
+ #
140
+ # The explorer is an interactive UI for browsing and testing API endpoints.
141
+ # Requires the `apiwork-explorer` gem.
142
+ #
143
+ # @yield Block evaluated in explorer context.
144
+ # @yieldparam explorer [Configuration]
145
+ # @return [void]
146
+ #
147
+ # @example Enable with defaults
148
+ # explorer
149
+ #
150
+ # @example Custom configuration
151
+ # explorer do
152
+ # mode :always
153
+ # path '/explorer'
154
+ # end
155
+ def explorer(&block)
156
+ unless defined?(Apiwork::Explorer::Engine)
157
+ raise ConfigurationError,
158
+ 'explorer requires the apiwork-explorer gem. ' \
159
+ "Add it to your Gemfile: gem 'apiwork-explorer'"
160
+ end
161
+
162
+ unless @explorer_config
163
+ options = Configurable.define do
164
+ option :mode, default: :auto, enum: %i[auto always never], type: :symbol
165
+ option :path, default: '/.explorer', type: :string
166
+ end
167
+ @explorer_config = Configuration.new(options)
168
+ end
169
+
170
+ return @explorer_config unless block
171
+
172
+ block.arity.positive? ? yield(@explorer_config) : @explorer_config.instance_eval(&block)
173
+ end
174
+
136
175
  # @api public
137
176
  # Sets or configures the adapter for this API.
138
177
  #
@@ -276,6 +315,30 @@ module Apiwork
276
315
  register_union(name, deprecated:, description:, discriminator:, example:, &block)
277
316
  end
278
317
 
318
+ # @api public
319
+ # Supported locales for this API.
320
+ #
321
+ # Declares which locales this API supports. Used by introspection
322
+ # to validate locale parameters and included in introspection output.
323
+ #
324
+ # @param locale_keys [Array<Symbol>]
325
+ # The locale identifiers.
326
+ # @return [Array<Symbol>]
327
+ # @raise [ConfigurationError] if any locale is not a Symbol
328
+ #
329
+ # @example
330
+ # locales :en, :sv, :it
331
+ # api_class.locales # => [:en, :sv, :it]
332
+ def locales(*locale_keys)
333
+ return @locales if locale_keys.empty?
334
+
335
+ locale_keys = locale_keys.flatten.uniq
336
+ locale_keys.each do |locale_key|
337
+ raise ConfigurationError, "locales must be symbols, got #{locale_key.class}: #{locale_key}" unless locale_key.is_a?(Symbol)
338
+ end
339
+ @locales = locale_keys
340
+ end
341
+
279
342
  # @api public
280
343
  # API-wide error codes.
281
344
  #
@@ -534,6 +597,18 @@ module Apiwork
534
597
  @root_resource.with_options(options, &block)
535
598
  end
536
599
 
600
+ # @api public
601
+ # The fingerprint for this API.
602
+ #
603
+ # A 16-character hex digest derived from the application name and {.base_path}.
604
+ #
605
+ # @return [String]
606
+ def fingerprint
607
+ @fingerprint ||= Digest::SHA256.hexdigest(
608
+ [Rails.application.class.module_parent_name, base_path].join(':'),
609
+ )[0, 16]
610
+ end
611
+
537
612
  def register_object(name, deprecated: false, description: nil, example: nil, scope: nil, &block)
538
613
  type_registry.register(
539
614
  name,
@@ -602,10 +677,13 @@ module Apiwork
602
677
 
603
678
  def mount(base_path)
604
679
  @base_path = base_path
680
+ @fingerprint = nil
605
681
  @locale_key = nil
606
682
  @namespaces = nil
607
683
  @info = nil
684
+ @locales = []
608
685
  @raises = []
686
+ @explorer_config = nil
609
687
  @export_configs = {}
610
688
  @adapter_config = nil
611
689
  @root_resource = Resource.new(self)
@@ -644,16 +722,33 @@ module Apiwork
644
722
  end.join('/')
645
723
  end
646
724
 
725
+ def transform_key(key)
726
+ key_string = key.to_s
727
+
728
+ return key_string if key_string.match?(/\A[A-Z]+\z/)
729
+
730
+ case key_format
731
+ when :camel then key_string.camelize(:lower)
732
+ when :pascal then key_string.camelize
733
+ when :kebab then key_string.dasherize
734
+ when :underscore then key_string.underscore
735
+ else key_string
736
+ end
737
+ end
738
+
739
+ def normalize_key(key)
740
+ key_string = key.to_s
741
+
742
+ return key_string if key_string.match?(/\A[A-Z]+\z/)
743
+
744
+ key_string.underscore
745
+ end
746
+
647
747
  def normalize_request(request)
648
- return request if %i[camel pascal kebab].exclude?(key_format)
748
+ return request if key_format == :keep
649
749
 
650
750
  request.transform do |hash|
651
- hash.deep_transform_keys do |key|
652
- key_string = key.to_s
653
- next key if key_string.match?(/\A[A-Z]+\z/)
654
-
655
- key_string.underscore.to_sym
656
- end
751
+ hash.deep_transform_keys { |key| normalize_key(key).to_sym }
657
752
  end
658
753
  end
659
754
 
@@ -663,16 +758,9 @@ module Apiwork
663
758
 
664
759
  def prepare_response(response)
665
760
  result = adapter.apply_response_transformers(response)
666
- case key_format
667
- when :camel
668
- result.transform { |hash| hash.deep_transform_keys { |key| key.to_s.camelize(:lower).to_sym } }
669
- when :pascal
670
- result.transform { |hash| hash.deep_transform_keys { |key| key.to_s.camelize.to_sym } }
671
- when :kebab
672
- result.transform { |hash| hash.deep_transform_keys { |key| key.to_s.dasherize.to_sym } }
673
- else
674
- result
675
- end
761
+ return result if key_format == :keep
762
+
763
+ result.transform { |hash| hash.deep_transform_keys { |key| transform_key(key).to_sym } }
676
764
  end
677
765
 
678
766
  def type?(name, scope: nil)
@@ -32,16 +32,16 @@ module Apiwork
32
32
  # This is the verbose form. Prefer sugar methods (string, integer, etc.)
33
33
  # for static definitions. Use `of` for dynamic element generation.
34
34
  #
35
- # @param type [Symbol] [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :string, :time, :union, :uuid]
35
+ # @param type [Symbol] [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :string, :time, :union, :unknown, :uuid]
36
36
  # The element type. Custom type references are also allowed.
37
37
  # @param discriminator [Symbol, nil] (nil)
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)
@@ -64,9 +64,25 @@ module Apiwork
64
64
  # string :name
65
65
  # end
66
66
  # end
67
+ #
68
+ # @example Discriminated union
69
+ # array :payments do
70
+ # of :union, discriminator: :type do
71
+ # variant tag: 'card' do
72
+ # object do
73
+ # string :last_four
74
+ # end
75
+ # end
76
+ # variant tag: 'bank' do
77
+ # object do
78
+ # string :account_number
79
+ # end
80
+ # end
81
+ # end
82
+ # end
67
83
  def of(type, discriminator: nil, enum: nil, format: nil, max: nil, min: nil, value: nil, &block)
68
84
  case type
69
- when :string, :integer, :decimal, :boolean, :number, :datetime, :date, :uuid, :time, :binary
85
+ when :string, :integer, :decimal, :boolean, :number, :datetime, :date, :uuid, :time, :binary, :unknown
70
86
  set_type(type, enum:, format:, max:, min:)
71
87
  when :literal
72
88
  @type = :literal
@@ -90,6 +106,15 @@ module Apiwork
90
106
  @inner = inner
91
107
  @shape = inner.shape
92
108
  @defined = true
109
+ when :record
110
+ raise ConfigurationError, 'record requires a block' unless block
111
+
112
+ inner = Element.new
113
+ block.arity.positive? ? yield(inner) : inner.instance_eval(&block)
114
+ inner.validate!
115
+ @type = :record
116
+ @inner = inner
117
+ @defined = true
93
118
  when :union
94
119
  raise ConfigurationError, 'union requires a block' unless block
95
120
 
@@ -105,6 +130,12 @@ module Apiwork
105
130
  @defined = true
106
131
  end
107
132
  end
133
+
134
+ def reference(type_name)
135
+ @type = type_name
136
+ @custom_type = type_name
137
+ @defined = true
138
+ end
108
139
  end
109
140
  end
110
141
  end
@@ -7,7 +7,7 @@ module Apiwork
7
7
  #
8
8
  # Accessed via `object :name do` in API or contract definitions.
9
9
  # Use type methods to define params: {#string}, {#integer}, {#decimal},
10
- # {#boolean}, {#array}, {#object}, {#union}, {#reference}.
10
+ # {#boolean}, {#array}, {#record}, {#object}, {#union}, {#reference}.
11
11
  #
12
12
  # @see API::Element Block context for array/variant elements
13
13
  # @see Contract::Object Block context for inline objects
@@ -32,7 +32,7 @@ module Apiwork
32
32
  #
33
33
  # @param name [Symbol]
34
34
  # The param name.
35
- # @param type [Symbol, nil] (nil) [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :string, :time, :union, :uuid]
35
+ # @param type [Symbol, nil] (nil) [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :record, :string, :time, :union, :unknown, :uuid]
36
36
  # The param type.
37
37
  # @param as [Symbol, nil] (nil)
38
38
  # The target attribute name.
@@ -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)
@@ -59,7 +59,7 @@ module Apiwork
59
59
  # @param nullable [Boolean] (false)
60
60
  # Whether the value can be `null`.
61
61
  # @param of [Symbol, Hash, nil] (nil)
62
- # The element type. Arrays only.
62
+ # The element or value type. Arrays and records only.
63
63
  # @param optional [Boolean] (false)
64
64
  # Whether the param is optional.
65
65
  # @param required [Boolean] (false)
@@ -85,6 +85,7 @@ module Apiwork
85
85
  name,
86
86
  type: nil,
87
87
  as: nil,
88
+ custom_type: nil,
88
89
  default: nil,
89
90
  deprecated: false,
90
91
  description: nil,
@@ -103,11 +104,12 @@ module Apiwork
103
104
  &block
104
105
  )
105
106
  resolved_of = resolve_of(of, type, &block)
106
- resolved_shape = type == :array ? nil : (shape || build_shape(type, discriminator, &block))
107
+ resolved_shape = [:array, :record].include?(type) ? nil : (shape || build_shape(type, discriminator, &block))
107
108
  discriminator = resolved_of&.discriminator if type == :array
108
109
 
109
110
  param_hash = {
110
111
  as:,
112
+ custom_type:,
111
113
  default:,
112
114
  deprecated:,
113
115
  description:,
@@ -193,10 +195,73 @@ module Apiwork
193
195
  )
194
196
  end
195
197
 
198
+ # @api public
199
+ # Defines a record param with value type.
200
+ #
201
+ # @param name [Symbol]
202
+ # The param name.
203
+ # @param as [Symbol, nil] (nil)
204
+ # The target attribute name.
205
+ # @param default [Object, nil] (nil)
206
+ # The default value.
207
+ # @param deprecated [Boolean] (false)
208
+ # Whether deprecated. Metadata included in exports.
209
+ # @param description [String, nil] (nil)
210
+ # The description. Metadata included in exports.
211
+ # @param nullable [Boolean] (false)
212
+ # Whether the value can be `null`.
213
+ # @param optional [Boolean] (false)
214
+ # Whether the param is optional.
215
+ # @param required [Boolean] (false)
216
+ # Whether the param is required.
217
+ # @yield block for defining value type
218
+ # @yieldparam element [API::Element]
219
+ # @return [void]
220
+ #
221
+ # @example instance_eval style
222
+ # record :scores do
223
+ # integer
224
+ # end
225
+ #
226
+ # @example yield style
227
+ # record :scores do |element|
228
+ # element.integer
229
+ # end
230
+ def record(
231
+ name,
232
+ as: nil,
233
+ default: nil,
234
+ deprecated: false,
235
+ description: nil,
236
+ nullable: false,
237
+ optional: false,
238
+ required: false,
239
+ &block
240
+ )
241
+ raise ArgumentError, 'record requires a block' unless block
242
+
243
+ element = Element.new
244
+ block.arity.positive? ? yield(element) : element.instance_eval(&block)
245
+ element.validate!
246
+
247
+ param(
248
+ name,
249
+ as:,
250
+ default:,
251
+ deprecated:,
252
+ description:,
253
+ nullable:,
254
+ optional:,
255
+ required:,
256
+ of: element,
257
+ type: :record,
258
+ )
259
+ end
260
+
196
261
  private
197
262
 
198
263
  def resolve_of(of, type, &block)
199
- return nil unless type == :array
264
+ return nil unless [:array, :record].include?(type)
200
265
 
201
266
  if block
202
267
  element = Element.new
@@ -35,6 +35,22 @@ module Apiwork
35
35
  end
36
36
  end
37
37
 
38
+ if api_class.explorer_config && defined?(Apiwork::Explorer::Engine)
39
+ mount_explorer = case api_class.explorer_config.mode
40
+ when :always then true
41
+ when :never then false
42
+ when :auto then Rails.env.development?
43
+ end
44
+
45
+ if mount_explorer
46
+ scope path: api_class.transform_path(api_class.base_path) do
47
+ mount Apiwork::Explorer::Engine,
48
+ at: api_class.explorer_config.path,
49
+ defaults: { api_base_path: api_class.base_path }
50
+ end
51
+ end
52
+ end
53
+
38
54
  scope module: api_class.namespaces.map(&:to_s).join('/').underscore,
39
55
  path: api_class.transform_path(api_class.base_path) do
40
56
  router.draw_resources(self, api_class.root_resource.resources, api_class)
@@ -7,6 +7,7 @@ module Apiwork
7
7
 
8
8
  def validate_type!(value)
9
9
  valid = case type
10
+ when :boolean then value.is_a?(TrueClass) || value.is_a?(FalseClass)
10
11
  when :symbol then value.is_a?(Symbol)
11
12
  when :string then value.is_a?(String)
12
13
  when :integer then value.is_a?(Integer)
@@ -24,6 +24,8 @@ module Apiwork
24
24
  raise ConfigurationError, "Unknown option: #{name}" unless option
25
25
 
26
26
  if args.empty? && !block
27
+ return @storage[name] = true if option.type == :boolean
28
+
27
29
  stored = @storage[name]
28
30
 
29
31
  if option.nested?
@@ -37,16 +37,16 @@ module Apiwork
37
37
  # This is the verbose form. Prefer sugar methods (string, integer, etc.)
38
38
  # for static definitions. Use `of` for dynamic element generation.
39
39
  #
40
- # @param type [Symbol] [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :string, :time, :union, :uuid]
40
+ # @param type [Symbol] [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :string, :time, :union, :unknown, :uuid]
41
41
  # The element type. Custom type references are also allowed.
42
42
  # @param discriminator [Symbol, nil] (nil)
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)
@@ -75,7 +75,7 @@ module Apiwork
75
75
  resolved_enum = enum.is_a?(Symbol) ? resolve_enum(enum) : enum
76
76
 
77
77
  case type
78
- when :string, :integer, :decimal, :boolean, :number, :datetime, :date, :uuid, :time, :binary
78
+ when :string, :integer, :decimal, :boolean, :number, :datetime, :date, :uuid, :time, :binary, :unknown
79
79
  set_type(type, format:, max:, min:, enum: resolved_enum)
80
80
  when :literal
81
81
  @type = :literal
@@ -99,6 +99,15 @@ module Apiwork
99
99
  @inner = inner
100
100
  @shape = inner.shape
101
101
  @defined = true
102
+ when :record
103
+ raise ConfigurationError, 'record requires a block' unless block
104
+
105
+ inner = Element.new(@contract_class)
106
+ block.arity.positive? ? yield(inner) : inner.instance_eval(&block)
107
+ inner.validate!
108
+ @type = :record
109
+ @inner = inner
110
+ @defined = true
102
111
  when :union
103
112
  raise ConfigurationError, 'union requires a block' unless block
104
113
 
@@ -115,6 +124,12 @@ module Apiwork
115
124
  end
116
125
  end
117
126
 
127
+ def reference(type_name)
128
+ @type = type_name
129
+ @custom_type = type_name
130
+ @defined = true
131
+ end
132
+
118
133
  private
119
134
 
120
135
  def resolve_enum(enum)