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.
- checksums.yaml +4 -4
- data/LICENSE.txt +2 -2
- data/README.md +117 -1
- data/Rakefile +5 -3
- data/app/controllers/apiwork/errors_controller.rb +13 -0
- data/app/controllers/apiwork/exports_controller.rb +22 -0
- data/lib/apiwork/abstractable.rb +26 -0
- data/lib/apiwork/adapter/base.rb +369 -0
- data/lib/apiwork/adapter/builder/api/base.rb +66 -0
- data/lib/apiwork/adapter/builder/contract/base.rb +86 -0
- data/lib/apiwork/adapter/capability/api/base.rb +51 -0
- data/lib/apiwork/adapter/capability/api/scope.rb +64 -0
- data/lib/apiwork/adapter/capability/base.rb +291 -0
- data/lib/apiwork/adapter/capability/contract/base.rb +37 -0
- data/lib/apiwork/adapter/capability/contract/scope.rb +110 -0
- data/lib/apiwork/adapter/capability/operation/base.rb +172 -0
- data/lib/apiwork/adapter/capability/operation/metadata_shape.rb +165 -0
- data/lib/apiwork/adapter/capability/result.rb +21 -0
- data/lib/apiwork/adapter/capability/runner.rb +56 -0
- data/lib/apiwork/adapter/capability/transformer/request/base.rb +72 -0
- data/lib/apiwork/adapter/capability/transformer/response/base.rb +45 -0
- data/lib/apiwork/adapter/registry.rb +16 -0
- data/lib/apiwork/adapter/serializer/error/base.rb +72 -0
- data/lib/apiwork/adapter/serializer/error/default/api_builder.rb +32 -0
- data/lib/apiwork/adapter/serializer/error/default.rb +37 -0
- data/lib/apiwork/adapter/serializer/resource/base.rb +84 -0
- data/lib/apiwork/adapter/serializer/resource/default/contract_builder.rb +209 -0
- data/lib/apiwork/adapter/serializer/resource/default.rb +39 -0
- data/lib/apiwork/adapter/standard/capability/filtering/api_builder.rb +75 -0
- data/lib/apiwork/adapter/standard/capability/filtering/constants.rb +37 -0
- data/lib/apiwork/adapter/standard/capability/filtering/contract_builder.rb +193 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/builder.rb +47 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/operator_builder.rb +36 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter.rb +462 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation.rb +22 -0
- data/lib/apiwork/adapter/standard/capability/filtering/request_transformer.rb +47 -0
- data/lib/apiwork/adapter/standard/capability/filtering.rb +18 -0
- data/lib/apiwork/adapter/standard/capability/including/contract_builder.rb +169 -0
- data/lib/apiwork/adapter/standard/capability/including/operation.rb +20 -0
- data/lib/apiwork/adapter/standard/capability/including.rb +16 -0
- data/lib/apiwork/adapter/standard/capability/pagination/api_builder.rb +34 -0
- data/lib/apiwork/adapter/standard/capability/pagination/contract_builder.rb +35 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/cursor.rb +84 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/offset.rb +66 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate.rb +24 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation.rb +24 -0
- data/lib/apiwork/adapter/standard/capability/pagination.rb +21 -0
- data/lib/apiwork/adapter/standard/capability/sorting/api_builder.rb +19 -0
- data/lib/apiwork/adapter/standard/capability/sorting/contract_builder.rb +84 -0
- data/lib/apiwork/adapter/standard/capability/sorting/operation/sort.rb +83 -0
- data/lib/apiwork/adapter/standard/capability/sorting/operation.rb +22 -0
- data/lib/apiwork/adapter/standard/capability/sorting.rb +17 -0
- data/lib/apiwork/adapter/standard/capability/writing/constants.rb +15 -0
- data/lib/apiwork/adapter/standard/capability/writing/contract_builder.rb +253 -0
- data/lib/apiwork/adapter/standard/capability/writing/operation/issue_mapper.rb +210 -0
- data/lib/apiwork/adapter/standard/capability/writing/operation.rb +32 -0
- data/lib/apiwork/adapter/standard/capability/writing/request_transformer.rb +37 -0
- data/lib/apiwork/adapter/standard/capability/writing.rb +17 -0
- data/lib/apiwork/adapter/standard/includes_resolver.rb +106 -0
- data/lib/apiwork/adapter/standard.rb +22 -0
- data/lib/apiwork/adapter/wrapper/base.rb +70 -0
- data/lib/apiwork/adapter/wrapper/collection/base.rb +60 -0
- data/lib/apiwork/adapter/wrapper/collection/default.rb +47 -0
- data/lib/apiwork/adapter/wrapper/error/base.rb +30 -0
- data/lib/apiwork/adapter/wrapper/error/default.rb +34 -0
- data/lib/apiwork/adapter/wrapper/member/base.rb +58 -0
- data/lib/apiwork/adapter/wrapper/member/default.rb +40 -0
- data/lib/apiwork/adapter/wrapper/shape.rb +203 -0
- data/lib/apiwork/adapter.rb +50 -0
- data/lib/apiwork/api/base.rb +802 -0
- data/lib/apiwork/api/element.rb +110 -0
- data/lib/apiwork/api/enum_registry/definition.rb +51 -0
- data/lib/apiwork/api/enum_registry.rb +98 -0
- data/lib/apiwork/api/info/contact.rb +67 -0
- data/lib/apiwork/api/info/license.rb +50 -0
- data/lib/apiwork/api/info/server.rb +50 -0
- data/lib/apiwork/api/info.rb +221 -0
- data/lib/apiwork/api/object.rb +235 -0
- data/lib/apiwork/api/registry.rb +33 -0
- data/lib/apiwork/api/representation_registry.rb +76 -0
- data/lib/apiwork/api/resource/action.rb +41 -0
- data/lib/apiwork/api/resource.rb +648 -0
- data/lib/apiwork/api/router.rb +104 -0
- data/lib/apiwork/api/type_registry/definition.rb +117 -0
- data/lib/apiwork/api/type_registry.rb +99 -0
- data/lib/apiwork/api/union.rb +49 -0
- data/lib/apiwork/api.rb +85 -0
- data/lib/apiwork/configurable.rb +71 -0
- data/lib/apiwork/configuration/option.rb +125 -0
- data/lib/apiwork/configuration/validatable.rb +25 -0
- data/lib/apiwork/configuration.rb +95 -0
- data/lib/apiwork/configuration_error.rb +6 -0
- data/lib/apiwork/constraint_error.rb +20 -0
- data/lib/apiwork/contract/action/request.rb +79 -0
- data/lib/apiwork/contract/action/response.rb +87 -0
- data/lib/apiwork/contract/action.rb +258 -0
- data/lib/apiwork/contract/base.rb +714 -0
- data/lib/apiwork/contract/element.rb +130 -0
- data/lib/apiwork/contract/object/coercer.rb +194 -0
- data/lib/apiwork/contract/object/deserializer.rb +101 -0
- data/lib/apiwork/contract/object/transformer.rb +95 -0
- data/lib/apiwork/contract/object/validator/result.rb +27 -0
- data/lib/apiwork/contract/object/validator.rb +734 -0
- data/lib/apiwork/contract/object.rb +566 -0
- data/lib/apiwork/contract/request_parser/result.rb +25 -0
- data/lib/apiwork/contract/request_parser.rb +72 -0
- data/lib/apiwork/contract/response_parser/result.rb +25 -0
- data/lib/apiwork/contract/response_parser.rb +35 -0
- data/lib/apiwork/contract/union.rb +56 -0
- data/lib/apiwork/contract_error.rb +9 -0
- data/lib/apiwork/controller.rb +300 -0
- data/lib/apiwork/domain_error.rb +13 -0
- data/lib/apiwork/element.rb +386 -0
- data/lib/apiwork/engine.rb +20 -0
- data/lib/apiwork/error.rb +6 -0
- data/lib/apiwork/error_code/definition.rb +63 -0
- data/lib/apiwork/error_code/registry.rb +18 -0
- data/lib/apiwork/error_code.rb +132 -0
- data/lib/apiwork/export/base.rb +291 -0
- data/lib/apiwork/export/open_api.rb +600 -0
- data/lib/apiwork/export/pipeline/writer.rb +66 -0
- data/lib/apiwork/export/pipeline.rb +84 -0
- data/lib/apiwork/export/registry.rb +16 -0
- data/lib/apiwork/export/surface_resolver.rb +189 -0
- data/lib/apiwork/export/type_analysis.rb +170 -0
- data/lib/apiwork/export/type_script.rb +23 -0
- data/lib/apiwork/export/type_script_mapper.rb +349 -0
- data/lib/apiwork/export/zod.rb +39 -0
- data/lib/apiwork/export/zod_mapper.rb +421 -0
- data/lib/apiwork/export.rb +80 -0
- data/lib/apiwork/http_error.rb +16 -0
- data/lib/apiwork/introspection/action/request.rb +66 -0
- data/lib/apiwork/introspection/action/response.rb +57 -0
- data/lib/apiwork/introspection/action.rb +124 -0
- data/lib/apiwork/introspection/api/info/contact.rb +59 -0
- data/lib/apiwork/introspection/api/info/license.rb +49 -0
- data/lib/apiwork/introspection/api/info/server.rb +50 -0
- data/lib/apiwork/introspection/api/info.rb +107 -0
- data/lib/apiwork/introspection/api/resource.rb +83 -0
- data/lib/apiwork/introspection/api.rb +92 -0
- data/lib/apiwork/introspection/contract.rb +63 -0
- data/lib/apiwork/introspection/dump/action.rb +101 -0
- data/lib/apiwork/introspection/dump/api.rb +119 -0
- data/lib/apiwork/introspection/dump/contract.rb +129 -0
- data/lib/apiwork/introspection/dump/param.rb +486 -0
- data/lib/apiwork/introspection/dump/resource.rb +112 -0
- data/lib/apiwork/introspection/dump/type.rb +339 -0
- data/lib/apiwork/introspection/dump.rb +17 -0
- data/lib/apiwork/introspection/enum.rb +63 -0
- data/lib/apiwork/introspection/error_code.rb +44 -0
- data/lib/apiwork/introspection/param/array.rb +88 -0
- data/lib/apiwork/introspection/param/base.rb +285 -0
- data/lib/apiwork/introspection/param/binary.rb +73 -0
- data/lib/apiwork/introspection/param/boolean.rb +73 -0
- data/lib/apiwork/introspection/param/date.rb +73 -0
- data/lib/apiwork/introspection/param/date_time.rb +73 -0
- data/lib/apiwork/introspection/param/decimal.rb +121 -0
- data/lib/apiwork/introspection/param/integer.rb +131 -0
- data/lib/apiwork/introspection/param/literal.rb +45 -0
- data/lib/apiwork/introspection/param/number.rb +121 -0
- data/lib/apiwork/introspection/param/object.rb +59 -0
- data/lib/apiwork/introspection/param/reference.rb +45 -0
- data/lib/apiwork/introspection/param/string.rb +122 -0
- data/lib/apiwork/introspection/param/time.rb +73 -0
- data/lib/apiwork/introspection/param/union.rb +57 -0
- data/lib/apiwork/introspection/param/unknown.rb +26 -0
- data/lib/apiwork/introspection/param/uuid.rb +73 -0
- data/lib/apiwork/introspection/param.rb +31 -0
- data/lib/apiwork/introspection/type.rb +129 -0
- data/lib/apiwork/introspection.rb +28 -0
- data/lib/apiwork/issue.rb +80 -0
- data/lib/apiwork/json_pointer.rb +21 -0
- data/lib/apiwork/object.rb +1618 -0
- data/lib/apiwork/reference_generator.rb +622 -0
- data/lib/apiwork/registry.rb +56 -0
- data/lib/apiwork/representation/association.rb +391 -0
- data/lib/apiwork/representation/attribute.rb +335 -0
- data/lib/apiwork/representation/base.rb +819 -0
- data/lib/apiwork/representation/deserializer.rb +95 -0
- data/lib/apiwork/representation/element.rb +128 -0
- data/lib/apiwork/representation/inheritance.rb +78 -0
- data/lib/apiwork/representation/model_detector.rb +75 -0
- data/lib/apiwork/representation/root_key.rb +35 -0
- data/lib/apiwork/representation/serializer.rb +127 -0
- data/lib/apiwork/request.rb +79 -0
- data/lib/apiwork/response.rb +56 -0
- data/lib/apiwork/union.rb +102 -0
- data/lib/apiwork/version.rb +2 -2
- data/lib/apiwork.rb +61 -3
- data/lib/generators/apiwork/api_generator.rb +38 -0
- data/lib/generators/apiwork/contract_generator.rb +25 -0
- data/lib/generators/apiwork/install_generator.rb +27 -0
- data/lib/generators/apiwork/representation_generator.rb +25 -0
- data/lib/generators/apiwork/templates/api/api.rb.tt +4 -0
- data/lib/generators/apiwork/templates/contract/contract.rb.tt +6 -0
- data/lib/generators/apiwork/templates/install/application_contract.rb.tt +5 -0
- data/lib/generators/apiwork/templates/install/application_representation.rb.tt +5 -0
- data/lib/generators/apiwork/templates/representation/representation.rb.tt +6 -0
- data/lib/tasks/apiwork.rake +102 -0
- metadata +319 -19
- data/.rubocop.yml +0 -8
- data/sig/apiwork.rbs +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dffa2da3925e765760fc1b380f2f5fdb48ed0d055a4d6bfe87055b81577183ef
|
|
4
|
+
data.tar.gz: 85995c7465a4875da8ebc72ef04606b198667b8344c9f4717da3465fe5d06f19
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7d71c49e6cb7d238d5adb40c0a7b829536bdfa4cee09cd4993542906cec0aa6f861115a1e4dafcb7f4523a91be06aeffa02e1643d7b71a483b7b0cb5a9b6775b
|
|
7
|
+
data.tar.gz: e61222b9596f21de4a8af7cf1279db74a5db1c426fbd5f10affc083ce30d4c6f0da205105cec0355c2704830aad8574804dbb635f75582b5463b3391be455d9f
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
|
@@ -1 +1,117 @@
|
|
|
1
|
-
|
|
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
|
|
4
|
-
require
|
|
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:
|
|
10
|
+
task default: %i[rubocop spec]
|
|
@@ -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
|