apiwork 0.0.0.pre → 0.1.1

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 (202) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +2 -2
  3. data/README.md +117 -1
  4. data/Rakefile +5 -3
  5. data/app/controllers/apiwork/errors_controller.rb +13 -0
  6. data/app/controllers/apiwork/exports_controller.rb +22 -0
  7. data/lib/apiwork/abstractable.rb +26 -0
  8. data/lib/apiwork/adapter/base.rb +369 -0
  9. data/lib/apiwork/adapter/builder/api/base.rb +66 -0
  10. data/lib/apiwork/adapter/builder/contract/base.rb +86 -0
  11. data/lib/apiwork/adapter/capability/api/base.rb +51 -0
  12. data/lib/apiwork/adapter/capability/api/scope.rb +64 -0
  13. data/lib/apiwork/adapter/capability/base.rb +291 -0
  14. data/lib/apiwork/adapter/capability/contract/base.rb +37 -0
  15. data/lib/apiwork/adapter/capability/contract/scope.rb +110 -0
  16. data/lib/apiwork/adapter/capability/operation/base.rb +172 -0
  17. data/lib/apiwork/adapter/capability/operation/metadata_shape.rb +165 -0
  18. data/lib/apiwork/adapter/capability/result.rb +21 -0
  19. data/lib/apiwork/adapter/capability/runner.rb +56 -0
  20. data/lib/apiwork/adapter/capability/transformer/request/base.rb +72 -0
  21. data/lib/apiwork/adapter/capability/transformer/response/base.rb +45 -0
  22. data/lib/apiwork/adapter/registry.rb +16 -0
  23. data/lib/apiwork/adapter/serializer/error/base.rb +72 -0
  24. data/lib/apiwork/adapter/serializer/error/default/api_builder.rb +32 -0
  25. data/lib/apiwork/adapter/serializer/error/default.rb +37 -0
  26. data/lib/apiwork/adapter/serializer/resource/base.rb +84 -0
  27. data/lib/apiwork/adapter/serializer/resource/default/contract_builder.rb +209 -0
  28. data/lib/apiwork/adapter/serializer/resource/default.rb +39 -0
  29. data/lib/apiwork/adapter/standard/capability/filtering/api_builder.rb +75 -0
  30. data/lib/apiwork/adapter/standard/capability/filtering/constants.rb +37 -0
  31. data/lib/apiwork/adapter/standard/capability/filtering/contract_builder.rb +193 -0
  32. data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/builder.rb +47 -0
  33. data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/operator_builder.rb +36 -0
  34. data/lib/apiwork/adapter/standard/capability/filtering/operation/filter.rb +462 -0
  35. data/lib/apiwork/adapter/standard/capability/filtering/operation.rb +22 -0
  36. data/lib/apiwork/adapter/standard/capability/filtering/request_transformer.rb +47 -0
  37. data/lib/apiwork/adapter/standard/capability/filtering.rb +18 -0
  38. data/lib/apiwork/adapter/standard/capability/including/contract_builder.rb +169 -0
  39. data/lib/apiwork/adapter/standard/capability/including/operation.rb +20 -0
  40. data/lib/apiwork/adapter/standard/capability/including.rb +16 -0
  41. data/lib/apiwork/adapter/standard/capability/pagination/api_builder.rb +34 -0
  42. data/lib/apiwork/adapter/standard/capability/pagination/contract_builder.rb +35 -0
  43. data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/cursor.rb +84 -0
  44. data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/offset.rb +66 -0
  45. data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate.rb +24 -0
  46. data/lib/apiwork/adapter/standard/capability/pagination/operation.rb +24 -0
  47. data/lib/apiwork/adapter/standard/capability/pagination.rb +21 -0
  48. data/lib/apiwork/adapter/standard/capability/sorting/api_builder.rb +19 -0
  49. data/lib/apiwork/adapter/standard/capability/sorting/contract_builder.rb +84 -0
  50. data/lib/apiwork/adapter/standard/capability/sorting/operation/sort.rb +83 -0
  51. data/lib/apiwork/adapter/standard/capability/sorting/operation.rb +22 -0
  52. data/lib/apiwork/adapter/standard/capability/sorting.rb +17 -0
  53. data/lib/apiwork/adapter/standard/capability/writing/constants.rb +15 -0
  54. data/lib/apiwork/adapter/standard/capability/writing/contract_builder.rb +253 -0
  55. data/lib/apiwork/adapter/standard/capability/writing/operation/issue_mapper.rb +210 -0
  56. data/lib/apiwork/adapter/standard/capability/writing/operation.rb +32 -0
  57. data/lib/apiwork/adapter/standard/capability/writing/request_transformer.rb +37 -0
  58. data/lib/apiwork/adapter/standard/capability/writing.rb +17 -0
  59. data/lib/apiwork/adapter/standard/includes_resolver.rb +106 -0
  60. data/lib/apiwork/adapter/standard.rb +22 -0
  61. data/lib/apiwork/adapter/wrapper/base.rb +70 -0
  62. data/lib/apiwork/adapter/wrapper/collection/base.rb +60 -0
  63. data/lib/apiwork/adapter/wrapper/collection/default.rb +47 -0
  64. data/lib/apiwork/adapter/wrapper/error/base.rb +30 -0
  65. data/lib/apiwork/adapter/wrapper/error/default.rb +34 -0
  66. data/lib/apiwork/adapter/wrapper/member/base.rb +58 -0
  67. data/lib/apiwork/adapter/wrapper/member/default.rb +40 -0
  68. data/lib/apiwork/adapter/wrapper/shape.rb +203 -0
  69. data/lib/apiwork/adapter.rb +50 -0
  70. data/lib/apiwork/api/base.rb +802 -0
  71. data/lib/apiwork/api/element.rb +110 -0
  72. data/lib/apiwork/api/enum_registry/definition.rb +51 -0
  73. data/lib/apiwork/api/enum_registry.rb +98 -0
  74. data/lib/apiwork/api/info/contact.rb +67 -0
  75. data/lib/apiwork/api/info/license.rb +50 -0
  76. data/lib/apiwork/api/info/server.rb +50 -0
  77. data/lib/apiwork/api/info.rb +221 -0
  78. data/lib/apiwork/api/object.rb +235 -0
  79. data/lib/apiwork/api/registry.rb +33 -0
  80. data/lib/apiwork/api/representation_registry.rb +76 -0
  81. data/lib/apiwork/api/resource/action.rb +41 -0
  82. data/lib/apiwork/api/resource.rb +648 -0
  83. data/lib/apiwork/api/router.rb +104 -0
  84. data/lib/apiwork/api/type_registry/definition.rb +117 -0
  85. data/lib/apiwork/api/type_registry.rb +99 -0
  86. data/lib/apiwork/api/union.rb +49 -0
  87. data/lib/apiwork/api.rb +85 -0
  88. data/lib/apiwork/configurable.rb +71 -0
  89. data/lib/apiwork/configuration/option.rb +125 -0
  90. data/lib/apiwork/configuration/validatable.rb +25 -0
  91. data/lib/apiwork/configuration.rb +95 -0
  92. data/lib/apiwork/configuration_error.rb +6 -0
  93. data/lib/apiwork/constraint_error.rb +20 -0
  94. data/lib/apiwork/contract/action/request.rb +79 -0
  95. data/lib/apiwork/contract/action/response.rb +87 -0
  96. data/lib/apiwork/contract/action.rb +258 -0
  97. data/lib/apiwork/contract/base.rb +714 -0
  98. data/lib/apiwork/contract/element.rb +130 -0
  99. data/lib/apiwork/contract/object/coercer.rb +194 -0
  100. data/lib/apiwork/contract/object/deserializer.rb +101 -0
  101. data/lib/apiwork/contract/object/transformer.rb +95 -0
  102. data/lib/apiwork/contract/object/validator/result.rb +27 -0
  103. data/lib/apiwork/contract/object/validator.rb +734 -0
  104. data/lib/apiwork/contract/object.rb +566 -0
  105. data/lib/apiwork/contract/request_parser/result.rb +25 -0
  106. data/lib/apiwork/contract/request_parser.rb +72 -0
  107. data/lib/apiwork/contract/response_parser/result.rb +25 -0
  108. data/lib/apiwork/contract/response_parser.rb +35 -0
  109. data/lib/apiwork/contract/union.rb +56 -0
  110. data/lib/apiwork/contract_error.rb +9 -0
  111. data/lib/apiwork/controller.rb +300 -0
  112. data/lib/apiwork/domain_error.rb +13 -0
  113. data/lib/apiwork/element.rb +386 -0
  114. data/lib/apiwork/engine.rb +20 -0
  115. data/lib/apiwork/error.rb +6 -0
  116. data/lib/apiwork/error_code/definition.rb +63 -0
  117. data/lib/apiwork/error_code/registry.rb +18 -0
  118. data/lib/apiwork/error_code.rb +132 -0
  119. data/lib/apiwork/export/base.rb +291 -0
  120. data/lib/apiwork/export/open_api.rb +600 -0
  121. data/lib/apiwork/export/pipeline/writer.rb +66 -0
  122. data/lib/apiwork/export/pipeline.rb +84 -0
  123. data/lib/apiwork/export/registry.rb +16 -0
  124. data/lib/apiwork/export/surface_resolver.rb +189 -0
  125. data/lib/apiwork/export/type_analysis.rb +170 -0
  126. data/lib/apiwork/export/type_script.rb +23 -0
  127. data/lib/apiwork/export/type_script_mapper.rb +349 -0
  128. data/lib/apiwork/export/zod.rb +39 -0
  129. data/lib/apiwork/export/zod_mapper.rb +421 -0
  130. data/lib/apiwork/export.rb +80 -0
  131. data/lib/apiwork/http_error.rb +16 -0
  132. data/lib/apiwork/introspection/action/request.rb +66 -0
  133. data/lib/apiwork/introspection/action/response.rb +57 -0
  134. data/lib/apiwork/introspection/action.rb +124 -0
  135. data/lib/apiwork/introspection/api/info/contact.rb +59 -0
  136. data/lib/apiwork/introspection/api/info/license.rb +49 -0
  137. data/lib/apiwork/introspection/api/info/server.rb +50 -0
  138. data/lib/apiwork/introspection/api/info.rb +107 -0
  139. data/lib/apiwork/introspection/api/resource.rb +83 -0
  140. data/lib/apiwork/introspection/api.rb +92 -0
  141. data/lib/apiwork/introspection/contract.rb +63 -0
  142. data/lib/apiwork/introspection/dump/action.rb +101 -0
  143. data/lib/apiwork/introspection/dump/api.rb +119 -0
  144. data/lib/apiwork/introspection/dump/contract.rb +129 -0
  145. data/lib/apiwork/introspection/dump/param.rb +486 -0
  146. data/lib/apiwork/introspection/dump/resource.rb +112 -0
  147. data/lib/apiwork/introspection/dump/type.rb +339 -0
  148. data/lib/apiwork/introspection/dump.rb +17 -0
  149. data/lib/apiwork/introspection/enum.rb +63 -0
  150. data/lib/apiwork/introspection/error_code.rb +44 -0
  151. data/lib/apiwork/introspection/param/array.rb +88 -0
  152. data/lib/apiwork/introspection/param/base.rb +285 -0
  153. data/lib/apiwork/introspection/param/binary.rb +73 -0
  154. data/lib/apiwork/introspection/param/boolean.rb +73 -0
  155. data/lib/apiwork/introspection/param/date.rb +73 -0
  156. data/lib/apiwork/introspection/param/date_time.rb +73 -0
  157. data/lib/apiwork/introspection/param/decimal.rb +121 -0
  158. data/lib/apiwork/introspection/param/integer.rb +131 -0
  159. data/lib/apiwork/introspection/param/literal.rb +45 -0
  160. data/lib/apiwork/introspection/param/number.rb +121 -0
  161. data/lib/apiwork/introspection/param/object.rb +59 -0
  162. data/lib/apiwork/introspection/param/reference.rb +45 -0
  163. data/lib/apiwork/introspection/param/string.rb +122 -0
  164. data/lib/apiwork/introspection/param/time.rb +73 -0
  165. data/lib/apiwork/introspection/param/union.rb +57 -0
  166. data/lib/apiwork/introspection/param/unknown.rb +26 -0
  167. data/lib/apiwork/introspection/param/uuid.rb +73 -0
  168. data/lib/apiwork/introspection/param.rb +31 -0
  169. data/lib/apiwork/introspection/type.rb +129 -0
  170. data/lib/apiwork/introspection.rb +28 -0
  171. data/lib/apiwork/issue.rb +80 -0
  172. data/lib/apiwork/json_pointer.rb +21 -0
  173. data/lib/apiwork/object.rb +1618 -0
  174. data/lib/apiwork/reference_generator.rb +622 -0
  175. data/lib/apiwork/registry.rb +56 -0
  176. data/lib/apiwork/representation/association.rb +391 -0
  177. data/lib/apiwork/representation/attribute.rb +335 -0
  178. data/lib/apiwork/representation/base.rb +819 -0
  179. data/lib/apiwork/representation/deserializer.rb +95 -0
  180. data/lib/apiwork/representation/element.rb +128 -0
  181. data/lib/apiwork/representation/inheritance.rb +78 -0
  182. data/lib/apiwork/representation/model_detector.rb +75 -0
  183. data/lib/apiwork/representation/root_key.rb +35 -0
  184. data/lib/apiwork/representation/serializer.rb +127 -0
  185. data/lib/apiwork/request.rb +79 -0
  186. data/lib/apiwork/response.rb +56 -0
  187. data/lib/apiwork/union.rb +102 -0
  188. data/lib/apiwork/version.rb +2 -2
  189. data/lib/apiwork.rb +61 -3
  190. data/lib/generators/apiwork/api_generator.rb +38 -0
  191. data/lib/generators/apiwork/contract_generator.rb +25 -0
  192. data/lib/generators/apiwork/install_generator.rb +27 -0
  193. data/lib/generators/apiwork/representation_generator.rb +25 -0
  194. data/lib/generators/apiwork/templates/api/api.rb.tt +4 -0
  195. data/lib/generators/apiwork/templates/contract/contract.rb.tt +6 -0
  196. data/lib/generators/apiwork/templates/install/application_contract.rb.tt +5 -0
  197. data/lib/generators/apiwork/templates/install/application_representation.rb.tt +5 -0
  198. data/lib/generators/apiwork/templates/representation/representation.rb.tt +6 -0
  199. data/lib/tasks/apiwork.rake +102 -0
  200. metadata +319 -19
  201. data/.rubocop.yml +0 -8
  202. data/sig/apiwork.rbs +0 -4
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module API
5
+ class Router
6
+ class << self
7
+ def draw
8
+ new.draw
9
+ end
10
+ end
11
+
12
+ def draw
13
+ api_classes = Registry.values
14
+ router = self
15
+
16
+ set = ActionDispatch::Routing::RouteSet.new
17
+
18
+ set.draw do
19
+ api_classes.each do |api_class|
20
+ next if api_class.base_path.blank? || api_class.root_resource.blank?
21
+
22
+ if api_class.export_configs.any?
23
+ scope path: api_class.transform_path(api_class.base_path) do
24
+ api_class.export_configs.each do |export_name, export_config|
25
+ next unless case export_config.endpoint.mode
26
+ when :always then true
27
+ when :never then false
28
+ when :auto then Rails.env.development?
29
+ end
30
+
31
+ get export_config.endpoint.path || "/.#{export_name}",
32
+ defaults: { export_name:, api_base_path: api_class.base_path },
33
+ to: 'apiwork/exports#show'
34
+ end
35
+ end
36
+ end
37
+
38
+ scope module: api_class.namespaces.map(&:to_s).join('/').underscore,
39
+ path: api_class.transform_path(api_class.base_path) do
40
+ router.draw_resources(self, api_class.root_resource.resources, api_class)
41
+ end
42
+
43
+ scope path: api_class.transform_path(api_class.base_path) do
44
+ match '*unmatched', to: 'apiwork/errors#not_found', via: :all
45
+ end
46
+ end
47
+ end
48
+
49
+ set
50
+ end
51
+
52
+ def draw_resources(context, resources_hash, api_class)
53
+ resources_hash.each_value do |resource|
54
+ resource_method = resource.singular ? :resource : :resources
55
+ options = {
56
+ constraints: resource.constraints,
57
+ controller: resource.controller,
58
+ defaults: resource.defaults,
59
+ except: resource.except,
60
+ only: resource.only,
61
+ param: resource.param,
62
+ }.compact
63
+
64
+ path_option = api_class.transform_path(resource.path || resource.name)
65
+ options[:path] = path_option unless path_option == resource.name.to_s
66
+
67
+ router = self
68
+
69
+ context.instance_eval do
70
+ send(resource_method, resource.name, **options) do
71
+ if resource.member_actions.any?
72
+ member do
73
+ resource.member_actions.each_value do |action|
74
+ action_path = api_class.transform_path(action.name)
75
+ if action_path == action.name.to_s
76
+ send(action.method, action.name)
77
+ else
78
+ send(action.method, action.name, path: action_path)
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ if resource.collection_actions.any?
85
+ collection do
86
+ resource.collection_actions.each_value do |action|
87
+ action_path = api_class.transform_path(action.name)
88
+ if action_path == action.name.to_s
89
+ send(action.method, action.name)
90
+ else
91
+ send(action.method, action.name, path: action_path)
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ router.draw_resources(self, resource.resources, api_class)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module API
5
+ class TypeRegistry
6
+ class Definition
7
+ attr_reader :deprecated,
8
+ :description,
9
+ :discriminator,
10
+ :example,
11
+ :kind,
12
+ :name,
13
+ :scope
14
+
15
+ def initialize(
16
+ name,
17
+ kind:,
18
+ scope: nil,
19
+ block: nil,
20
+ deprecated: false,
21
+ description: nil,
22
+ discriminator: nil,
23
+ example: nil,
24
+ fragment: false
25
+ )
26
+ @name = name
27
+ @kind = kind
28
+ @scope = scope
29
+ @block = block
30
+ @deprecated = deprecated
31
+ @description = description
32
+ @discriminator = discriminator
33
+ @example = example
34
+ @fragment = fragment
35
+ @shape = nil
36
+ end
37
+
38
+ def deprecated?
39
+ @deprecated == true
40
+ end
41
+
42
+ def fragment?
43
+ @fragment == true
44
+ end
45
+
46
+ def object?
47
+ @kind == :object
48
+ end
49
+
50
+ def union?
51
+ @kind == :union
52
+ end
53
+
54
+ def shape
55
+ ensure_shape_built!
56
+ @shape
57
+ end
58
+
59
+ def params
60
+ shape.params if object?
61
+ end
62
+
63
+ def variants
64
+ shape.variants if union?
65
+ end
66
+
67
+ def merge(block:, deprecated:, description:, example:)
68
+ Definition.new(
69
+ @name,
70
+ block: merge_blocks(@block, block),
71
+ deprecated: deprecated || @deprecated,
72
+ description: description || @description,
73
+ discriminator: @discriminator,
74
+ example: example || @example,
75
+ fragment: @fragment,
76
+ kind: @kind,
77
+ scope: @scope,
78
+ )
79
+ end
80
+
81
+ private
82
+
83
+ def merge_blocks(existing_block, new_block)
84
+ return existing_block unless new_block
85
+ return new_block unless existing_block
86
+
87
+ proc do |shape|
88
+ if existing_block.arity.positive?
89
+ existing_block.call(shape)
90
+ else
91
+ shape.instance_eval(&existing_block)
92
+ end
93
+ if new_block.arity.positive?
94
+ new_block.call(shape)
95
+ else
96
+ shape.instance_eval(&new_block)
97
+ end
98
+ end
99
+ end
100
+
101
+ def ensure_shape_built!
102
+ return if @shape
103
+
104
+ @shape = if object?
105
+ Object.new
106
+ else
107
+ Union.new(discriminator: @discriminator)
108
+ end
109
+
110
+ return unless @block
111
+
112
+ @block.arity.positive? ? @block.call(@shape) : @shape.instance_eval(&@block)
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module API
5
+ class TypeRegistry
6
+ def initialize
7
+ @store = {}
8
+ end
9
+
10
+ def register(
11
+ name,
12
+ kind:,
13
+ scope: nil,
14
+ deprecated: false,
15
+ description: nil,
16
+ discriminator: nil,
17
+ example: nil,
18
+ fragment: false,
19
+ &block
20
+ )
21
+ key = scoped_name(scope, name)
22
+
23
+ if @store.key?(key)
24
+ validate_kind_consistency!(key, kind)
25
+ merge(key, block:, deprecated:, description:, example:)
26
+ else
27
+ @store[key] = Definition.new(
28
+ key,
29
+ block:,
30
+ deprecated:,
31
+ description:,
32
+ discriminator:,
33
+ example:,
34
+ fragment:,
35
+ kind:,
36
+ scope:,
37
+ )
38
+ end
39
+ end
40
+
41
+ def [](name)
42
+ @store[name]
43
+ end
44
+
45
+ def key?(name)
46
+ @store.key?(name)
47
+ end
48
+
49
+ def each_pair(&block)
50
+ @store.each_pair(&block)
51
+ end
52
+
53
+ def exists?(name, scope: nil)
54
+ find(name, scope:).present?
55
+ end
56
+
57
+ def find(name, scope: nil)
58
+ definition = scope ? @store[scoped_name(scope, name)] : nil
59
+ definition || @store[name]
60
+ end
61
+
62
+ def scoped_name(scope, name)
63
+ return name unless scope
64
+
65
+ prefix = scope.respond_to?(:scope_prefix) ? scope.scope_prefix : nil
66
+ return name unless prefix
67
+ return prefix.to_sym if name.nil?
68
+ return prefix.to_sym if name.to_s.empty?
69
+ return name.to_sym if name.to_s == prefix
70
+
71
+ [prefix, name].join('_').to_sym
72
+ end
73
+
74
+ def clear!
75
+ @store.clear
76
+ end
77
+
78
+ private
79
+
80
+ def validate_kind_consistency!(key, new_kind)
81
+ definition = @store[key]
82
+ return if definition.kind == new_kind
83
+
84
+ raise ConfigurationError,
85
+ "Cannot redefine :#{key} as #{new_kind}, already defined as #{definition.kind}"
86
+ end
87
+
88
+ def merge(key, block:, deprecated:, description:, example:)
89
+ definition = @store[key]
90
+ @store[key] = definition.merge(
91
+ block:,
92
+ deprecated:,
93
+ description:,
94
+ example:,
95
+ )
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module API
5
+ # @api public
6
+ # Block context for defining reusable union types.
7
+ #
8
+ # Accessed via `union :name do` in API or contract definitions.
9
+ # Use {#variant} to define possible types.
10
+ #
11
+ # @see API::Element Block context for variant types
12
+ # @see Contract::Union Block context for inline unions
13
+ #
14
+ # @example instance_eval style
15
+ # union :payment_method, discriminator: :type do
16
+ # variant tag: 'card' do
17
+ # object do
18
+ # string :last_four
19
+ # end
20
+ # end
21
+ # variant tag: 'bank' do
22
+ # object do
23
+ # string :account_number
24
+ # end
25
+ # end
26
+ # end
27
+ #
28
+ # @example yield style
29
+ # union :payment_method, discriminator: :type do |union|
30
+ # union.variant tag: 'card' do |variant|
31
+ # variant.object do |object|
32
+ # object.string :last_four
33
+ # end
34
+ # end
35
+ # union.variant tag: 'bank' do |variant|
36
+ # variant.object do |object|
37
+ # object.string :account_number
38
+ # end
39
+ # end
40
+ # end
41
+ class Union < Apiwork::Union
42
+ private
43
+
44
+ def build_element
45
+ Element.new
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ # @api public
5
+ module API
6
+ class << self
7
+ # @!method find(base_path)
8
+ # @api public
9
+ # Finds an API by base path.
10
+ # @param base_path [String]
11
+ # The API base path.
12
+ # @return [Class<API::Base>, nil]
13
+ # @see .find!
14
+ # @example
15
+ # Apiwork::API.find('/api/v1')
16
+ #
17
+ # @!method find!(base_path)
18
+ # @api public
19
+ # Finds an API by base path.
20
+ # @param base_path [String]
21
+ # The API base path.
22
+ # @return [Class<API::Base>]
23
+ # @raise [KeyError] if the API is not found
24
+ # @see .find
25
+ # @example
26
+ # Apiwork::API.find!('/api/v1')
27
+ delegate :clear!,
28
+ :exists?,
29
+ :find,
30
+ :find!,
31
+ :keys,
32
+ :unregister,
33
+ :values,
34
+ to: Registry
35
+
36
+ # @api public
37
+ # Defines a new API at the given base path.
38
+ #
39
+ # This is the main entry point for creating an Apiwork API.
40
+ # The block receives an API recorder for defining resources,
41
+ # types, and configuration.
42
+ #
43
+ # @param base_path [String]
44
+ # The API base path.
45
+ # @yield block for API definition
46
+ # @return [Class<API::Base>]
47
+ #
48
+ # @example Basic API
49
+ # Apiwork::API.define '/api/v1' do
50
+ # resources :users
51
+ # resources :posts
52
+ # end
53
+ #
54
+ # @example With configuration
55
+ # Apiwork::API.define '/api/v1' do
56
+ # key_format :camel
57
+ #
58
+ # resources :invoices
59
+ # end
60
+ def define(base_path, &block)
61
+ return unless block
62
+
63
+ Class.new(Base).tap do |klass|
64
+ klass.mount(base_path)
65
+ klass.class_eval(&block)
66
+ end
67
+ end
68
+
69
+ # @api public
70
+ # The introspection data for an API.
71
+ #
72
+ # @param base_path [String]
73
+ # The API base path.
74
+ # @param locale [Symbol, nil] (nil)
75
+ # The locale for descriptions.
76
+ # @return [Introspection::API]
77
+ #
78
+ # @example
79
+ # Apiwork::API.introspect('/api/v1')
80
+ def introspect(base_path, locale: nil)
81
+ find!(base_path).introspect(locale:)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Configurable
5
+ extend ActiveSupport::Concern
6
+
7
+ class << self
8
+ def define(extends: nil, &block)
9
+ Class.new do
10
+ include Configurable
11
+
12
+ self.options = extends.options.dup if extends
13
+
14
+ class_eval(&block) if block
15
+ end
16
+ end
17
+ end
18
+
19
+ included do
20
+ class_attribute :options, default: {}, instance_predicate: false
21
+ end
22
+
23
+ # @!method option(name, type:, default: nil, enum: nil, &block)
24
+ # @!scope class
25
+ # @api public
26
+ # Defines a configuration option.
27
+ #
28
+ # For nested options, use `type: :hash` with a block. Inside the block,
29
+ # call `option` to define child options.
30
+ #
31
+ # @param name [Symbol]
32
+ # The option name.
33
+ # @param type [Symbol] [:boolean, :hash, :integer, :string, :symbol]
34
+ # The option type.
35
+ # @param default [Object, nil] (nil)
36
+ # The default value.
37
+ # @param enum [Array, nil] (nil)
38
+ # The allowed values.
39
+ # @yield block evaluated in {Configuration::Option} context (for :hash type)
40
+ # @return [void]
41
+ # @see Configuration::Option
42
+ #
43
+ # @example Symbol option
44
+ # option :locale, type: :symbol, default: :en
45
+ #
46
+ # @example String option with enum
47
+ # option :version, type: :string, default: '5', enum: %w[4 5]
48
+ #
49
+ # @example Nested options
50
+ # option :pagination, type: :hash do
51
+ # option :strategy, type: :symbol, default: :offset, enum: %i[offset cursor]
52
+ # option :default_size, type: :integer, default: 20
53
+ # option :max_size, type: :integer, default: 100
54
+ # end
55
+
56
+ class_methods do
57
+ def inherited(subclass)
58
+ super
59
+ subclass.options = options.dup
60
+ end
61
+
62
+ def option(name, default: nil, enum: nil, type:, &block)
63
+ options[name] = Configuration::Option.new(name, type, default:, enum:, &block)
64
+ end
65
+
66
+ def default_options
67
+ options.transform_values(&:effective_default).compact
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ class Configuration
5
+ # @api public
6
+ # Block context for nested configuration options.
7
+ #
8
+ # Used inside `option :name, type: :hash do ... end` blocks
9
+ # in {Adapter::Base} and {Export::Base} subclasses.
10
+ #
11
+ # @see Adapter::Base
12
+ # @see Export::Base
13
+ # @see Configuration
14
+ #
15
+ # @example instance_eval style
16
+ # option :pagination, type: :hash do
17
+ # option :strategy, type: :symbol, default: :offset
18
+ # option :default_size, type: :integer, default: 20
19
+ # end
20
+ #
21
+ # @example yield style
22
+ # option :pagination, type: :hash do |option|
23
+ # option.option :strategy, type: :symbol, default: :offset
24
+ # option.option :default_size, type: :integer, default: 20
25
+ # end
26
+ class Option
27
+ include Validatable
28
+
29
+ attr_reader :children,
30
+ :default,
31
+ :enum,
32
+ :name,
33
+ :type
34
+
35
+ def initialize(name, type, children: nil, default: nil, enum: nil, &block)
36
+ @name = name
37
+ @type = type
38
+ @default = default
39
+ @enum = enum
40
+ @children = children || {}
41
+
42
+ return unless block && type == :hash
43
+
44
+ block.arity.positive? ? yield(self) : instance_eval(&block)
45
+ end
46
+
47
+ # @api public
48
+ # Defines a nested option.
49
+ #
50
+ # @param name [Symbol]
51
+ # The option name.
52
+ # @param default [Object, nil] (nil)
53
+ # The default value.
54
+ # @param enum [Array, nil] (nil)
55
+ # The allowed values.
56
+ # @param type [Symbol] [:boolean, :hash, :integer, :string, :symbol]
57
+ # The option type.
58
+ # @yield block for nested options (type: :hash)
59
+ # @yieldparam option [Option]
60
+ # @return [void]
61
+ #
62
+ # @example instance_eval style
63
+ # option :pagination, type: :hash do
64
+ # option :strategy, type: :symbol, default: :offset
65
+ # option :default_size, type: :integer, default: 20
66
+ # end
67
+ #
68
+ # @example yield style
69
+ # option :pagination, type: :hash do |option|
70
+ # option.option :strategy, type: :symbol, default: :offset
71
+ # option.option :default_size, type: :integer, default: 20
72
+ # end
73
+ def option(name, default: nil, enum: nil, type:, &block)
74
+ @children[name] = Option.new(name, type, default:, enum:, &block)
75
+ end
76
+
77
+ def nested?
78
+ children.any?
79
+ end
80
+
81
+ def effective_default
82
+ return default unless nested?
83
+
84
+ children.transform_values(&:default)
85
+ end
86
+
87
+ def validate!(value)
88
+ return if value.nil?
89
+
90
+ if nested?
91
+ raise ConfigurationError, "#{name} must be a Hash" unless value.is_a?(Hash)
92
+
93
+ validate_children!(value)
94
+ else
95
+ validate_type!(value)
96
+ validate_enum!(value) if enum
97
+ end
98
+ end
99
+
100
+ def cast(value)
101
+ return nil if value.nil?
102
+ return value unless value.is_a?(String)
103
+
104
+ case type
105
+ when :symbol then value.to_sym
106
+ when :string then value
107
+ when :integer then value.to_i
108
+ when :boolean then %w[true 1 yes].include?(value.downcase)
109
+ when :hash then value
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def validate_children!(hash)
116
+ hash.each do |key, value|
117
+ child = children[key]
118
+ raise ConfigurationError, "Unknown option: #{name}.#{key}" unless child
119
+
120
+ child.validate!(value)
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ class Configuration
5
+ module Validatable
6
+ private
7
+
8
+ def validate_type!(value)
9
+ valid = case type
10
+ when :symbol then value.is_a?(Symbol)
11
+ when :string then value.is_a?(String)
12
+ when :integer then value.is_a?(Integer)
13
+ when :hash then value.is_a?(Hash)
14
+ end
15
+ raise ConfigurationError, "#{name} must be #{type}, got #{value.class}" unless valid
16
+ end
17
+
18
+ def validate_enum!(value)
19
+ return if enum.include?(value)
20
+
21
+ raise ConfigurationError, "#{name} must be one of #{enum.inspect}, got #{value.inspect}"
22
+ end
23
+ end
24
+ end
25
+ end