apiwork 0.0.0.pre → 0.1.2

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 +638 -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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fef277bf01c26fedb2244ef9380c0bca12473d89a237de282b6c2968cee46469
4
- data.tar.gz: 4f68775b3061032af69de441b9d653c4f23f3e35075e9e6ca31a0139fd7f2fe8
3
+ metadata.gz: 14f70185b89b359e58791101591102755c5e523fed61fe4e4cdf7e5ad3cd1615
4
+ data.tar.gz: 10303ee74f360fa49fd711497dec982094cd7d3f7621d08a47c68abecf22bd3f
5
5
  SHA512:
6
- metadata.gz: ee34e78a369b305b726d8254abb692c04c4867e9e7ce8ad86e107674b4ff8dbb3fe63ccace7e30d738a1bf2fe56975208d14010f4c7930b64ee769191e397187
7
- data.tar.gz: f6da4511ef0111b3994b452f49b151924c3a870adadf9e84618ef6913141aa3dd42fc03a672e4af443a5716adfcc10224c7d3fa1eddbbd42a0ac4952986541fa
6
+ metadata.gz: cb799505dac690b1807a31f3c6f87d5b4893d85ba79f007966a2e6bfe19c58e2e63a0bf6bed4080de6b8c7a1aee3fbd9deab76f4fbabf8c416ff9ab9e78598b6
7
+ data.tar.gz: 0bfe2378cfc17aab81dd0af3ccd1fff1e0bbafad476e97e9c71f0b2c7764485d2f02f44290a7c9015d2ada479ae2b37d00c76786369515361a424688cb1b390a
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
- The MIT License (MIT)
1
+ Copyright (c) 2026 skiftle
2
2
 
3
- Copyright (c) 2025 joauh
3
+ MIT License
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1 +1,117 @@
1
- Coming soon!
1
+ # Apiwork
2
+
3
+ Typed APIs for Rails.
4
+
5
+ Apiwork lets you define your API once and derive validation, serialization, querying, and typed exports from the same definition.
6
+
7
+ It integrates with Rails rather than replacing it. Controllers, ActiveRecord models, and application logic remain unchanged.
8
+
9
+ See https://apiwork.dev for full documentation.
10
+
11
+ ---
12
+
13
+ ## Overview
14
+
15
+ Apiwork introduces an explicit, typed boundary to a Rails application.
16
+
17
+ From a single definition, it provides:
18
+
19
+ - Runtime request validation
20
+ - Response serialization
21
+ - Filtering, sorting, and pagination
22
+ - Nested writes
23
+ - OpenAPI specification
24
+ - Generated TypeScript and Zod types
25
+
26
+ The same structures that validate requests in production are used to generate client artifacts. There is no parallel schema layer.
27
+
28
+ ---
29
+
30
+ ## Example
31
+
32
+ A representation describes how a model appears through the API:
33
+
34
+ ```ruby
35
+ class InvoiceRepresentation < Apiwork::Representation::Base
36
+ attribute :id
37
+ attribute :number, writable: true, filterable: true, sortable: true
38
+ attribute :status, filterable: true
39
+ attribute :issued_on, writable: true, sortable: true
40
+
41
+ belongs_to :customer, filterable: true
42
+ has_many :lines, writable: true
43
+ end
44
+ ```
45
+
46
+ Types and nullability are inferred from ActiveRecord metadata.
47
+
48
+ From this definition, Apiwork derives:
49
+
50
+ - Typed request contracts
51
+ - Response serializers
52
+ - Query parameters for filtering and sorting
53
+ - Offset or cursor-based pagination
54
+ - Nested write handling
55
+ - OpenAPI and client type exports
56
+
57
+ A minimal controller:
58
+
59
+ ```ruby
60
+ def index
61
+ expose Invoice.all
62
+ end
63
+
64
+ def create
65
+ expose Invoice.create(contract.body[:invoice])
66
+ end
67
+ ```
68
+
69
+ `contract.body` contains validated parameters.
70
+ `expose` serializes the response according to the representation.
71
+
72
+ ---
73
+
74
+ ## Querying
75
+
76
+ Filtering and sorting are declared on attributes and associations.
77
+
78
+ Example query parameters:
79
+
80
+ ```
81
+ ?filter[status][eq]=sent
82
+ ?sort[issued_on]=desc
83
+ ```
84
+
85
+ Operators are typed and validated. Generated client types reflect the same structure.
86
+
87
+ ---
88
+
89
+ ## Standalone Contracts
90
+
91
+ Representations are optional. Contracts can be defined independently of ActiveRecord for webhooks, external APIs, or custom request and response shapes.
92
+
93
+ ---
94
+
95
+ ## Installation
96
+
97
+ Add to your Gemfile:
98
+
99
+ ```bash
100
+ bundle add apiwork
101
+ ```
102
+
103
+ Then run:
104
+
105
+ ```bash
106
+ rails generate apiwork:install
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Status
112
+
113
+ Under active development.
114
+
115
+ ## License
116
+
117
+ MIT
data/Rakefile CHANGED
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "rubocop/rake_task"
3
+ require 'bundler/gem_tasks'
4
+ require 'rubocop/rake_task'
5
+ require 'rspec/core/rake_task'
5
6
 
6
7
  RuboCop::RakeTask.new
8
+ RSpec::Core::RakeTask.new(:spec)
7
9
 
8
- task default: :rubocop
10
+ task default: %i[rubocop spec]
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ class ErrorsController < ActionController::API
5
+ include Controller
6
+
7
+ skip_contract_validation!
8
+
9
+ def not_found
10
+ expose_error :not_found
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ class ExportsController < ActionController::API
5
+ def show
6
+ api_class = API.find!(params[:api_base_path])
7
+ export_name = params[:export_name].to_sym
8
+ export_class = Export.find!(export_name)
9
+
10
+ raw_options = { key_format: api_class.key_format }
11
+ .merge(api_class.export_configs[export_name].to_h)
12
+ .merge(params.to_unsafe_h.symbolize_keys)
13
+
14
+ format = raw_options[:format]&.to_sym
15
+ options = export_class.extract_options(raw_options)
16
+
17
+ result = Export.generate(export_name, api_class.base_path, format:, **options)
18
+
19
+ render content_type: export_class.content_type_for(format:), plain: result
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Abstractable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :_abstract, default: false, instance_predicate: false
9
+ end
10
+
11
+ class_methods do
12
+ def abstract!
13
+ self._abstract = true
14
+ end
15
+
16
+ def abstract?
17
+ _abstract
18
+ end
19
+
20
+ def inherited(subclass)
21
+ super
22
+ subclass._abstract = false
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,369 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ # @api public
6
+ # Base class for adapters.
7
+ #
8
+ # The engine of an API. Handles both introspection (generating types from
9
+ # representations) and runtime (processing requests through capabilities,
10
+ # serializing, and wrapping responses). The class declaration acts as a manifest.
11
+ #
12
+ # @example
13
+ # class MyAdapter < Apiwork::Adapter::Base
14
+ # adapter_name :my
15
+ #
16
+ # resource_serializer Serializer::Resource::Default
17
+ # error_serializer Serializer::Error::Default
18
+ #
19
+ # member_wrapper Wrapper::Member::Default
20
+ # collection_wrapper Wrapper::Collection::Default
21
+ # error_wrapper Wrapper::Error::Default
22
+ #
23
+ # capability Capability::Filtering
24
+ # capability Capability::Pagination
25
+ # end
26
+ class Base
27
+ include Configurable
28
+
29
+ class << self
30
+ # @api public
31
+ # The adapter name for this adapter.
32
+ #
33
+ # @param value [Symbol, String, nil] (nil)
34
+ # The adapter name.
35
+ # @return [Symbol, nil]
36
+ #
37
+ # @example
38
+ # adapter_name :my
39
+ def adapter_name(value = nil)
40
+ @adapter_name = value.to_sym if value
41
+ @adapter_name
42
+ end
43
+
44
+ # @api public
45
+ # Registers a capability for this adapter.
46
+ #
47
+ # Capabilities are self-contained concerns (pagination, filtering, etc.)
48
+ # that handle both introspection and runtime behavior.
49
+ #
50
+ # @param klass [Class<Capability::Base>]
51
+ # The capability class.
52
+ # @return [void]
53
+ #
54
+ # @example
55
+ # capability Capability::Filtering
56
+ # capability Capability::Pagination
57
+ def capability(klass)
58
+ @capabilities ||= []
59
+ @capabilities << klass
60
+
61
+ return unless klass.options.any?
62
+
63
+ name = klass.capability_name
64
+ options[name] = Configuration::Option.new(name, :hash, children: klass.options)
65
+ end
66
+
67
+ # @api public
68
+ # Skips an inherited capability by name.
69
+ #
70
+ # @param name [Symbol]
71
+ # The capability name to skip.
72
+ # @return [void]
73
+ #
74
+ # @example
75
+ # skip_capability :pagination
76
+ def skip_capability(name)
77
+ @skipped_capabilities ||= []
78
+ @skipped_capabilities << name.to_sym
79
+ end
80
+
81
+ def capabilities
82
+ inherited = superclass.respond_to?(:capabilities) ? superclass.capabilities : []
83
+ skipped = @skipped_capabilities || []
84
+ all = (inherited + (@capabilities || [])).uniq
85
+ all.reject { |capability| skipped.include?(capability.capability_name) }
86
+ end
87
+
88
+ # @api public
89
+ # Sets the serializer class for records and collections.
90
+ #
91
+ # @param klass [Class<Serializer::Resource::Base>, nil] (nil)
92
+ # The serializer class.
93
+ # @return [Class<Serializer::Resource::Base>, nil]
94
+ #
95
+ # @example
96
+ # resource_serializer Serializer::Resource::Default
97
+ def resource_serializer(klass = nil)
98
+ if klass
99
+ validate_class_setter!(:resource_serializer, klass, Serializer::Resource::Base, 'Serializer')
100
+ @resource_serializer = klass
101
+ end
102
+ @resource_serializer || (superclass.respond_to?(:resource_serializer) && superclass.resource_serializer)
103
+ end
104
+
105
+ # @api public
106
+ # Sets the serializer class for errors.
107
+ #
108
+ # @param klass [Class<Serializer::Error::Base>, nil] (nil)
109
+ # The serializer class.
110
+ # @return [Class<Serializer::Error::Base>, nil]
111
+ #
112
+ # @example
113
+ # error_serializer Serializer::Error::Default
114
+ def error_serializer(klass = nil)
115
+ if klass
116
+ validate_class_setter!(:error_serializer, klass, Serializer::Error::Base, 'Serializer')
117
+ @error_serializer = klass
118
+ end
119
+ @error_serializer || (superclass.respond_to?(:error_serializer) && superclass.error_serializer)
120
+ end
121
+
122
+ # @api public
123
+ # Sets the wrapper class for single-record responses.
124
+ #
125
+ # @param klass [Class<Wrapper::Member::Base>, nil] (nil)
126
+ # The wrapper class.
127
+ # @return [Class<Wrapper::Member::Base>, nil]
128
+ #
129
+ # @example
130
+ # member_wrapper Wrapper::Member::Default
131
+ def member_wrapper(klass = nil)
132
+ if klass
133
+ validate_class_setter!(:member_wrapper, klass, Wrapper::Member::Base, 'Wrapper')
134
+ @member_wrapper = klass
135
+ end
136
+ @member_wrapper || (superclass.respond_to?(:member_wrapper) && superclass.member_wrapper)
137
+ end
138
+
139
+ # @api public
140
+ # Sets the wrapper class for collection responses.
141
+ #
142
+ # @param klass [Class<Wrapper::Collection::Base>, nil] (nil)
143
+ # The wrapper class.
144
+ # @return [Class<Wrapper::Collection::Base>, nil]
145
+ #
146
+ # @example
147
+ # collection_wrapper Wrapper::Collection::Default
148
+ def collection_wrapper(klass = nil)
149
+ if klass
150
+ validate_class_setter!(:collection_wrapper, klass, Wrapper::Collection::Base, 'Wrapper')
151
+ @collection_wrapper = klass
152
+ end
153
+ @collection_wrapper || (superclass.respond_to?(:collection_wrapper) && superclass.collection_wrapper)
154
+ end
155
+
156
+ # @api public
157
+ # Sets the wrapper class for error responses.
158
+ #
159
+ # @param klass [Class<Wrapper::Error::Base>, nil] (nil)
160
+ # The wrapper class.
161
+ # @return [Class<Wrapper::Error::Base>, nil]
162
+ #
163
+ # @example
164
+ # error_wrapper Wrapper::Error::Default
165
+ def error_wrapper(klass = nil)
166
+ if klass
167
+ validate_class_setter!(:error_wrapper, klass, Wrapper::Error::Base, 'Wrapper')
168
+ @error_wrapper = klass
169
+ end
170
+ @error_wrapper || (superclass.respond_to?(:error_wrapper) && superclass.error_wrapper)
171
+ end
172
+
173
+ private
174
+
175
+ def validate_class_setter!(name, klass, base_class, label)
176
+ unless klass.is_a?(Class)
177
+ raise ConfigurationError,
178
+ "#{name} must be a #{label} class, got #{klass.class}. " \
179
+ "Use: #{name} Example (not 'Example' or :example)"
180
+ end
181
+ return if klass < base_class
182
+
183
+ raise ConfigurationError,
184
+ "#{name} must be a #{label} class (subclass of #{base_class.name}), " \
185
+ "got #{klass}"
186
+ end
187
+ end
188
+
189
+ def process_collection(collection, representation_class, request, context: {}, meta: {})
190
+ collection, metadata, serialize_options = apply_capabilities(collection, representation_class, request, wrapper_type: :collection)
191
+ data = self.class.resource_serializer.serialize(representation_class, collection, context:, serialize_options:)
192
+ self.class.collection_wrapper.wrap(data, metadata, representation_class.root_key, meta)
193
+ end
194
+
195
+ def process_member(record, representation_class, request, context: {}, meta: {})
196
+ record, metadata, serialize_options = apply_capabilities(record, representation_class, request, wrapper_type: :member)
197
+ data = self.class.resource_serializer.serialize(representation_class, record, context:, serialize_options:)
198
+ self.class.member_wrapper.wrap(data, metadata, representation_class.root_key, meta)
199
+ end
200
+
201
+ def process_error(error, representation_class, context: {})
202
+ data = self.class.error_serializer.serialize(error, context:)
203
+ self.class.error_wrapper.wrap(data)
204
+ end
205
+
206
+ def register_api(api_class)
207
+ capabilities.each do |capability|
208
+ capability.api_types(api_class)
209
+ end
210
+
211
+ error_serializer_class = self.class.error_serializer
212
+ error_serializer_class.new.api_types(api_class)
213
+
214
+ build_error_response_body(api_class, error_serializer_class)
215
+ end
216
+
217
+ def register_contract(contract_class, representation_class, actions)
218
+ capabilities.each do |capability|
219
+ capability.contract_types(contract_class, representation_class, actions)
220
+ end
221
+
222
+ self.class.resource_serializer.new(representation_class).contract_types(contract_class)
223
+
224
+ build_action_responses(contract_class, representation_class, actions)
225
+ end
226
+
227
+ def apply_request_transformers(request, phase:)
228
+ run_capability_request_transformers(request, phase:)
229
+ end
230
+
231
+ def apply_response_transformers(response)
232
+ run_capability_response_transformers(response)
233
+ end
234
+
235
+ def capabilities
236
+ @capabilities ||= self.class.capabilities.map do |klass|
237
+ klass.new({}, adapter_name: self.class.adapter_name)
238
+ end
239
+ end
240
+
241
+ private
242
+
243
+ def apply_capabilities(data, representation_class, request, wrapper_type:)
244
+ Capability::Runner.run(
245
+ capabilities,
246
+ data:,
247
+ representation_class:,
248
+ request:,
249
+ wrapper_type:,
250
+ )
251
+ end
252
+
253
+ def build_action_responses(contract_class, representation_class, actions)
254
+ actions.each_value do |action|
255
+ build_action_response(contract_class, representation_class, action)
256
+ end
257
+ end
258
+
259
+ def build_action_response(contract_class, representation_class, action)
260
+ contract_action = contract_class.action(action.name)
261
+ return if contract_action.resets_response?
262
+
263
+ case action.name
264
+ when :index
265
+ build_collection_action_response(contract_class, representation_class, action, contract_action)
266
+ when :show, :create, :update
267
+ build_member_action_response(contract_class, representation_class, action, contract_action)
268
+ when :destroy
269
+ contract_action.response { no_content! }
270
+ else
271
+ build_custom_action_response(contract_class, representation_class, action, contract_action)
272
+ end
273
+ end
274
+
275
+ def build_member_action_response(contract_class, representation_class, action, contract_action)
276
+ result_wrapper = build_result_wrapper(contract_class, representation_class, action.name, :member)
277
+ member_shape_class = self.class.member_wrapper.shape_class
278
+ data_type = resolve_resource_data_type(representation_class)
279
+
280
+ contract_action.response do |response|
281
+ response.result_wrapper = result_wrapper
282
+ response.body do |body|
283
+ member_shape_class.apply(body, representation_class.root_key, capabilities, representation_class, :member, data_type:)
284
+ end
285
+ end
286
+ end
287
+
288
+ def build_collection_action_response(contract_class, representation_class, action, contract_action)
289
+ result_wrapper = build_result_wrapper(contract_class, representation_class, action.name, :collection)
290
+ collection_shape_class = self.class.collection_wrapper.shape_class
291
+ data_type = resolve_resource_data_type(representation_class)
292
+
293
+ contract_action.response do |response|
294
+ response.result_wrapper = result_wrapper
295
+ response.body do |body|
296
+ collection_shape_class.apply(body, representation_class.root_key, capabilities, representation_class, :collection, data_type:)
297
+ end
298
+ end
299
+ end
300
+
301
+ def build_custom_action_response(contract_class, representation_class, action, contract_action)
302
+ if action.method == :delete
303
+ contract_action.response { no_content! }
304
+ elsif action.collection?
305
+ build_collection_action_response(contract_class, representation_class, action, contract_action)
306
+ elsif action.member?
307
+ build_member_action_response(contract_class, representation_class, action, contract_action)
308
+ end
309
+ end
310
+
311
+ def build_result_wrapper(contract_class, representation_class, action_name, response_type)
312
+ success_type_name = [action_name, 'success_response_body'].join('_').to_sym
313
+
314
+ unless contract_class.type?(success_type_name)
315
+ shape_class = if response_type == :collection
316
+ self.class.collection_wrapper.shape_class
317
+ else
318
+ self.class.member_wrapper.shape_class
319
+ end
320
+ data_type = resolve_resource_data_type(representation_class)
321
+
322
+ contract_class.object(success_type_name) do |object|
323
+ shape_class.apply(object, representation_class.root_key, capabilities, representation_class, response_type, data_type:)
324
+ end
325
+ end
326
+
327
+ { error_type: :error_response_body, success_type: contract_class.scoped_type_name(success_type_name) }
328
+ end
329
+
330
+ def resolve_resource_data_type(representation_class)
331
+ self.class.resource_serializer.data_type.call(representation_class)
332
+ end
333
+
334
+ def build_error_response_body(api_class, error_serializer_class)
335
+ return if api_class.type?(:error_response_body)
336
+
337
+ shape_class = self.class.error_wrapper.shape_class
338
+ return unless shape_class
339
+
340
+ data_type = error_serializer_class.data_type
341
+
342
+ api_class.object(:error_response_body) do |object|
343
+ shape_class.apply(object, nil, [], nil, :error, data_type:)
344
+ end
345
+ end
346
+
347
+ def run_capability_request_transformers(request, phase:)
348
+ transformers = capability_request_transformers.select { |transformer_class| transformer_class.phase == phase }
349
+ result = request
350
+ transformers.each { |transformer_class| result = transformer_class.transform(result) }
351
+ result
352
+ end
353
+
354
+ def run_capability_response_transformers(response)
355
+ result = response
356
+ capability_response_transformers.each { |transformer_class| result = transformer_class.transform(result) }
357
+ result
358
+ end
359
+
360
+ def capability_request_transformers
361
+ self.class.capabilities.flat_map(&:request_transformers)
362
+ end
363
+
364
+ def capability_response_transformers
365
+ self.class.capabilities.flat_map(&:response_transformers)
366
+ end
367
+ end
368
+ end
369
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ module Builder
6
+ module API
7
+ # @api public
8
+ # Base class for API-phase type builders.
9
+ #
10
+ # API phase runs once per API at initialization time.
11
+ # Use it to register shared types used across all contracts.
12
+ #
13
+ # @example
14
+ # module Builder
15
+ # class API < Adapter::Builder::API::Base
16
+ # def build
17
+ # enum(:status, values: %w[active inactive])
18
+ # object(:error) do |object|
19
+ # object.string(:message)
20
+ # end
21
+ # end
22
+ # end
23
+ # end
24
+ class Base
25
+ attr_reader :data_type
26
+
27
+ # @!method enum(name, values:, **options, &block)
28
+ # @api public
29
+ # @see API::Base#enum
30
+ # @!method enum?(name)
31
+ # @api public
32
+ # @see API::Base#enum?
33
+ # @!method object(name, **options, &block)
34
+ # @api public
35
+ # @see API::Base#object
36
+ # @!method type?(name)
37
+ # @api public
38
+ # @see API::Base#type?
39
+ # @!method union(name, **options, &block)
40
+ # @api public
41
+ # @see API::Base#union
42
+ delegate :enum,
43
+ :enum?,
44
+ :object,
45
+ :type?,
46
+ :union,
47
+ to: :@api_class
48
+
49
+ def initialize(api_class, data_type: nil)
50
+ @api_class = api_class
51
+ @data_type = data_type
52
+ end
53
+
54
+ # @api public
55
+ # Builds API-level types.
56
+ #
57
+ # Override this method to register shared types.
58
+ # @return [void]
59
+ def build
60
+ raise NotImplementedError
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end