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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
module Serializer
|
|
6
|
+
module Resource
|
|
7
|
+
# @api public
|
|
8
|
+
# Base class for resource serializers.
|
|
9
|
+
#
|
|
10
|
+
# Resource serializers handle serialization of records and collections
|
|
11
|
+
# and define resource types at the contract level.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# class MyResourceSerializer < Serializer::Resource::Base
|
|
15
|
+
# contract_builder Builder::Contract
|
|
16
|
+
#
|
|
17
|
+
# def serialize(resource, context:, serialize_options:)
|
|
18
|
+
# representation_class.serialize(resource, context:)
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
class Base
|
|
22
|
+
class << self
|
|
23
|
+
def serialize(representation_class, resource, context:, serialize_options:)
|
|
24
|
+
new(representation_class).serialize(resource, context:, serialize_options:)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @api public
|
|
28
|
+
# The data type for this serializer.
|
|
29
|
+
#
|
|
30
|
+
# @param block [Proc, nil] (nil)
|
|
31
|
+
# Block that receives representation_class and returns type name.
|
|
32
|
+
# @return [Proc, nil]
|
|
33
|
+
def data_type(&block)
|
|
34
|
+
@data_type = block if block
|
|
35
|
+
@data_type
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @api public
|
|
39
|
+
# The contract builder for this serializer.
|
|
40
|
+
#
|
|
41
|
+
# @param klass [Class<Builder::Contract::Base>, nil] (nil)
|
|
42
|
+
# The builder class.
|
|
43
|
+
# @return [Class<Builder::Contract::Base>, nil]
|
|
44
|
+
def contract_builder(klass = nil)
|
|
45
|
+
@contract_builder = klass if klass
|
|
46
|
+
@contract_builder
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @api public
|
|
51
|
+
# The representation class for this serializer.
|
|
52
|
+
#
|
|
53
|
+
# @return [Class<Representation::Base>]
|
|
54
|
+
attr_reader :representation_class
|
|
55
|
+
|
|
56
|
+
def initialize(representation_class)
|
|
57
|
+
@representation_class = representation_class
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def contract_types(contract_class)
|
|
61
|
+
builder_class = self.class.contract_builder
|
|
62
|
+
return unless builder_class
|
|
63
|
+
|
|
64
|
+
builder_class.new(contract_class, representation_class).build
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @api public
|
|
68
|
+
# Serializes a resource.
|
|
69
|
+
#
|
|
70
|
+
# @param resource [Object]
|
|
71
|
+
# The resource to serialize.
|
|
72
|
+
# @param context [Hash]
|
|
73
|
+
# The serialization context.
|
|
74
|
+
# @param serialize_options [Hash]
|
|
75
|
+
# The options (e.g., include).
|
|
76
|
+
# @return [Hash]
|
|
77
|
+
def serialize(resource, context:, serialize_options:)
|
|
78
|
+
raise NotImplementedError
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
module Serializer
|
|
6
|
+
module Resource
|
|
7
|
+
class Default < Base
|
|
8
|
+
class ContractBuilder < Adapter::Builder::Contract::Base
|
|
9
|
+
def build
|
|
10
|
+
build_enums
|
|
11
|
+
resource_type_name
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def resource_type_name
|
|
15
|
+
if sti_base_representation?
|
|
16
|
+
build_sti_response_union_type
|
|
17
|
+
else
|
|
18
|
+
register_type(representation_class.root_key.singular.to_sym) unless type?(scoped_type_name(nil))
|
|
19
|
+
|
|
20
|
+
scoped_type_name(nil)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def import_association_contract(association_representation, visited)
|
|
25
|
+
return nil if visited.include?(association_representation)
|
|
26
|
+
|
|
27
|
+
association_contract = contract_for(association_representation)
|
|
28
|
+
return nil unless association_contract
|
|
29
|
+
|
|
30
|
+
alias_name = association_representation.root_key.singular.to_sym
|
|
31
|
+
import(association_contract, as: alias_name)
|
|
32
|
+
alias_name
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def build_enums
|
|
38
|
+
representation_class.attributes.each do |name, attribute|
|
|
39
|
+
next unless attribute.enum&.any?
|
|
40
|
+
|
|
41
|
+
enum(name, values: attribute.enum)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def register_type(type_name)
|
|
46
|
+
association_type_map = {}
|
|
47
|
+
representation_class.associations.each do |name, association|
|
|
48
|
+
association_type_map[name] = build_association_type(association)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
object(
|
|
52
|
+
type_name,
|
|
53
|
+
description: representation_class.description,
|
|
54
|
+
example: representation_class.example,
|
|
55
|
+
) do |object|
|
|
56
|
+
if representation_class.subclass?
|
|
57
|
+
discriminator_name = representation_class.superclass.inheritance.column
|
|
58
|
+
object.literal(discriminator_name, value: representation_class.sti_name)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
representation_class.attributes.each do |name, attribute|
|
|
62
|
+
param_options = {
|
|
63
|
+
deprecated: attribute.deprecated?,
|
|
64
|
+
description: attribute.description,
|
|
65
|
+
example: attribute.example,
|
|
66
|
+
format: attribute.format,
|
|
67
|
+
nullable: attribute.nullable?,
|
|
68
|
+
type: attribute.type,
|
|
69
|
+
**(attribute.enum ? { enum: name } : {}),
|
|
70
|
+
**(attribute.of ? { of: attribute.of } : {}),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
element = attribute.element
|
|
74
|
+
if element
|
|
75
|
+
if element.type == :array
|
|
76
|
+
param_options[:of] = element.inner
|
|
77
|
+
else
|
|
78
|
+
param_options[:shape] = element.shape
|
|
79
|
+
param_options[:discriminator] = element.discriminator if element.discriminator
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
object.param(name, **param_options)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
representation_class.associations.each do |name, association|
|
|
87
|
+
association_type = association_type_map[name]
|
|
88
|
+
|
|
89
|
+
base_options = {
|
|
90
|
+
deprecated: association.deprecated?,
|
|
91
|
+
description: association.description,
|
|
92
|
+
example: association.example,
|
|
93
|
+
nullable: association.nullable?,
|
|
94
|
+
optional: association.include != :always,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if association.singular?
|
|
98
|
+
object.param(name, type: association_type || :object, **base_options)
|
|
99
|
+
elsif association.collection?
|
|
100
|
+
if association_type
|
|
101
|
+
object.param(name, type: :array, **base_options) do |param|
|
|
102
|
+
param.of(association_type)
|
|
103
|
+
end
|
|
104
|
+
else
|
|
105
|
+
object.param(name, type: :array, **base_options)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def sti_base_representation?
|
|
113
|
+
inheritance = representation_class.inheritance
|
|
114
|
+
inheritance&.subclasses&.any? && inheritance.base_class == representation_class
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def build_sti_union(union_type_name:, visited: Set.new)
|
|
118
|
+
representation_inheritance = representation_class.inheritance
|
|
119
|
+
return nil unless representation_inheritance.subclasses.any?
|
|
120
|
+
|
|
121
|
+
discriminator_name = representation_inheritance.column
|
|
122
|
+
|
|
123
|
+
variant_types = representation_inheritance.subclasses.filter_map do |subclass|
|
|
124
|
+
variant_type = yield(subclass)
|
|
125
|
+
{ tag: subclass.sti_name, type: variant_type } if variant_type
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
union(union_type_name, discriminator: discriminator_name) do |union|
|
|
129
|
+
variant_types.each do |variant_type|
|
|
130
|
+
union.variant(tag: variant_type[:tag]) do |variant|
|
|
131
|
+
variant.reference(variant_type[:type])
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
union_type_name
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def build_sti_response_union_type(visited: Set.new)
|
|
140
|
+
union_type_name = representation_class.root_key.singular.to_sym
|
|
141
|
+
|
|
142
|
+
build_sti_union(union_type_name:, visited:) do |variant_representation_class|
|
|
143
|
+
variant_contract = contract_for(variant_representation_class)
|
|
144
|
+
next nil unless variant_contract
|
|
145
|
+
|
|
146
|
+
alias_name = variant_representation_class.root_key.singular.to_sym
|
|
147
|
+
import(variant_contract, as: alias_name)
|
|
148
|
+
|
|
149
|
+
alias_name
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def build_association_type(association, visited: Set.new)
|
|
154
|
+
return build_polymorphic_association_type(association, visited:) if association.polymorphic?
|
|
155
|
+
|
|
156
|
+
association_info = resolve_association(association)
|
|
157
|
+
return nil unless association_info
|
|
158
|
+
|
|
159
|
+
representation_class = association_info[:representation_class]
|
|
160
|
+
|
|
161
|
+
return import_association_contract(representation_class, visited) if association_info[:sti]
|
|
162
|
+
return nil if visited.include?(representation_class)
|
|
163
|
+
|
|
164
|
+
association_contract = contract_for(representation_class)
|
|
165
|
+
return nil unless association_contract
|
|
166
|
+
|
|
167
|
+
type_name = representation_class.root_key.singular.to_sym
|
|
168
|
+
import(association_contract, as: type_name)
|
|
169
|
+
type_name
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def build_polymorphic_association_type(association, visited: Set.new)
|
|
173
|
+
polymorphic = association.polymorphic
|
|
174
|
+
return nil unless polymorphic.any?
|
|
175
|
+
|
|
176
|
+
union_type_name = association.name
|
|
177
|
+
|
|
178
|
+
existing_type = type?(union_type_name)
|
|
179
|
+
return union_type_name if existing_type
|
|
180
|
+
|
|
181
|
+
union(union_type_name, discriminator: association.discriminator) do |union|
|
|
182
|
+
polymorphic.each do |polymorphic_representation_class|
|
|
183
|
+
tag = polymorphic_representation_class.polymorphic_name
|
|
184
|
+
alias_name = import_association_contract(polymorphic_representation_class, visited)
|
|
185
|
+
next unless alias_name
|
|
186
|
+
|
|
187
|
+
union.variant(tag:) do |variant|
|
|
188
|
+
variant.reference(alias_name)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
union_type_name
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def resolve_association(association)
|
|
197
|
+
return nil if association.polymorphic?
|
|
198
|
+
|
|
199
|
+
representation_class = association.representation_class
|
|
200
|
+
return nil unless representation_class
|
|
201
|
+
|
|
202
|
+
{ representation_class:, sti: representation_class.inheritance&.subclasses&.any? }
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
module Serializer
|
|
6
|
+
module Resource
|
|
7
|
+
# @api public
|
|
8
|
+
# Default resource serializer.
|
|
9
|
+
#
|
|
10
|
+
# Delegates serialization to the representation class using its root key as data type.
|
|
11
|
+
#
|
|
12
|
+
# @example Configuration
|
|
13
|
+
# class MyAdapter < Adapter::Base
|
|
14
|
+
# serializer Serializer::Resource::Default
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# @example Output
|
|
18
|
+
# {
|
|
19
|
+
# "id": 1,
|
|
20
|
+
# "number": "INV-001",
|
|
21
|
+
# "customer": { "id": 5, "name": "Acme" }
|
|
22
|
+
# }
|
|
23
|
+
class Default < Base
|
|
24
|
+
data_type { |representation_class| representation_class.root_key.singular.to_sym }
|
|
25
|
+
|
|
26
|
+
contract_builder ContractBuilder
|
|
27
|
+
|
|
28
|
+
def serialize(resource, context:, serialize_options:)
|
|
29
|
+
representation_class.serialize(
|
|
30
|
+
resource,
|
|
31
|
+
context:,
|
|
32
|
+
include: serialize_options[:include],
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Filtering
|
|
8
|
+
class APIBuilder < Adapter::Capability::API::Base
|
|
9
|
+
def build
|
|
10
|
+
return unless scope.filterable?
|
|
11
|
+
|
|
12
|
+
scope.filter_types.each { |type| register_filter(type, nullable: false) }
|
|
13
|
+
scope.nullable_filter_types.each { |type| register_filter(type, nullable: true) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def register_filter(type, nullable:)
|
|
19
|
+
type_name = type_name(type, nullable:)
|
|
20
|
+
return if type?(type_name)
|
|
21
|
+
|
|
22
|
+
operators = operators_for(type)
|
|
23
|
+
register_between(type) if operators.include?(:between)
|
|
24
|
+
|
|
25
|
+
object(type_name) do |object|
|
|
26
|
+
operators.each { |operator| add_operator(object, operator, type) }
|
|
27
|
+
object.boolean?(:null) if nullable
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def add_operator(object, operator, type)
|
|
32
|
+
case operator
|
|
33
|
+
when :in
|
|
34
|
+
object.array?(:in) { |array| array.of(type) }
|
|
35
|
+
when :between
|
|
36
|
+
object.reference?(:between, to: between_type_name(type))
|
|
37
|
+
else
|
|
38
|
+
object.param(operator, type:, optional: true)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def register_between(type)
|
|
43
|
+
type_name = between_type_name(type)
|
|
44
|
+
return if type?(type_name)
|
|
45
|
+
|
|
46
|
+
object(type_name) do |object|
|
|
47
|
+
object.param(:from, type:, optional: true)
|
|
48
|
+
object.param(:to, type:, optional: true)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def operators_for(type)
|
|
53
|
+
case type
|
|
54
|
+
when :string, :binary then Constants::STRING_OPERATORS
|
|
55
|
+
when :date, :datetime, :time then Constants::DATE_OPERATORS
|
|
56
|
+
when :integer, :decimal, :number then Constants::NUMERIC_OPERATORS
|
|
57
|
+
when :uuid then Constants::UUID_OPERATORS
|
|
58
|
+
when :boolean then Constants::BOOLEAN_OPERATORS
|
|
59
|
+
else Constants::STRING_OPERATORS
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def type_name(type, nullable:)
|
|
64
|
+
nullable ? [:nullable, type, :filter].join('_').to_sym : [type, :filter].join('_').to_sym
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def between_type_name(type)
|
|
68
|
+
[type, :filter, :between].join('_').to_sym
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Filtering < Adapter::Capability::Base
|
|
8
|
+
module Constants
|
|
9
|
+
AND = :AND
|
|
10
|
+
OR = :OR
|
|
11
|
+
NOT = :NOT
|
|
12
|
+
LOGICAL_OPERATORS = [AND, OR, NOT].freeze
|
|
13
|
+
|
|
14
|
+
EQUALITY_OPERATORS = %i[eq].freeze
|
|
15
|
+
COMPARISON_OPERATORS = %i[gt gte lt lte].freeze
|
|
16
|
+
RANGE_OPERATORS = %i[between].freeze
|
|
17
|
+
COLLECTION_OPERATORS = %i[in].freeze
|
|
18
|
+
STRING_SPECIFIC_OPERATORS = %i[contains starts_with ends_with].freeze
|
|
19
|
+
NULL_OPERATORS = %i[null].freeze
|
|
20
|
+
|
|
21
|
+
STRING_OPERATORS = (EQUALITY_OPERATORS + COLLECTION_OPERATORS + STRING_SPECIFIC_OPERATORS).freeze
|
|
22
|
+
DATE_OPERATORS = (EQUALITY_OPERATORS + COMPARISON_OPERATORS + RANGE_OPERATORS + COLLECTION_OPERATORS).freeze
|
|
23
|
+
NUMERIC_OPERATORS = (EQUALITY_OPERATORS + COMPARISON_OPERATORS + RANGE_OPERATORS + COLLECTION_OPERATORS).freeze
|
|
24
|
+
UUID_OPERATORS = (EQUALITY_OPERATORS + COLLECTION_OPERATORS).freeze
|
|
25
|
+
BOOLEAN_OPERATORS = EQUALITY_OPERATORS.freeze
|
|
26
|
+
|
|
27
|
+
NULLABLE_STRING_OPERATORS = (STRING_OPERATORS + NULL_OPERATORS).freeze
|
|
28
|
+
NULLABLE_DATE_OPERATORS = (DATE_OPERATORS + NULL_OPERATORS).freeze
|
|
29
|
+
NULLABLE_NUMERIC_OPERATORS = (NUMERIC_OPERATORS + NULL_OPERATORS).freeze
|
|
30
|
+
NULLABLE_UUID_OPERATORS = (UUID_OPERATORS + NULL_OPERATORS).freeze
|
|
31
|
+
NULLABLE_BOOLEAN_OPERATORS = (BOOLEAN_OPERATORS + NULL_OPERATORS).freeze
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Filtering
|
|
8
|
+
class ContractBuilder < Adapter::Capability::Contract::Base
|
|
9
|
+
TYPE_NAME = :filter
|
|
10
|
+
UNFILTERABLE_TYPES = %i[unknown array object union].freeze
|
|
11
|
+
|
|
12
|
+
def build
|
|
13
|
+
return unless filterable?
|
|
14
|
+
|
|
15
|
+
representation_class.attributes.each do |name, attribute|
|
|
16
|
+
next unless attribute.filterable? && attribute.enum
|
|
17
|
+
|
|
18
|
+
type_name = [name, TYPE_NAME].join('_').to_sym
|
|
19
|
+
next if type?(type_name)
|
|
20
|
+
|
|
21
|
+
build_enum_filter_union(type_name, scoped_enum_name(name))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
build_polymorphic_type_filters
|
|
25
|
+
build_sti_type_filters
|
|
26
|
+
|
|
27
|
+
attributes = representation_class.attributes.filter_map do |name, attribute|
|
|
28
|
+
next unless attribute.filterable? && UNFILTERABLE_TYPES.exclude?(attribute.type)
|
|
29
|
+
|
|
30
|
+
filter_type = filter_type_for(attribute)
|
|
31
|
+
has_custom_filter = attribute.enum || polymorphic_type_column?(attribute) || sti_type_column?(attribute)
|
|
32
|
+
shorthand = !has_custom_filter && !%i[object array union].include?(attribute.type)
|
|
33
|
+
[name, attribute.type, filter_type, shorthand]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
associations = representation_class.associations.filter_map do |name, association|
|
|
37
|
+
next unless association.filterable?
|
|
38
|
+
next if association.polymorphic?
|
|
39
|
+
|
|
40
|
+
representation = association.representation_class
|
|
41
|
+
next unless representation
|
|
42
|
+
|
|
43
|
+
contract = contract_for(representation)
|
|
44
|
+
next unless contract
|
|
45
|
+
|
|
46
|
+
alias_name = representation.root_key.singular.to_sym
|
|
47
|
+
import(contract, as: alias_name)
|
|
48
|
+
|
|
49
|
+
filter_type = [alias_name, TYPE_NAME].join('_').to_sym
|
|
50
|
+
next unless type?(filter_type)
|
|
51
|
+
|
|
52
|
+
[name, filter_type]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
object(TYPE_NAME) do |object|
|
|
56
|
+
object.array?(Constants::AND) do |element|
|
|
57
|
+
element.reference(TYPE_NAME)
|
|
58
|
+
end
|
|
59
|
+
object.array?(Constants::OR) do |element|
|
|
60
|
+
element.reference(TYPE_NAME)
|
|
61
|
+
end
|
|
62
|
+
object.reference?(Constants::NOT, to: TYPE_NAME)
|
|
63
|
+
|
|
64
|
+
attributes.each do |name, type, filter_type, shorthand|
|
|
65
|
+
if shorthand
|
|
66
|
+
object.union?(name) do |union|
|
|
67
|
+
union.variant do |element|
|
|
68
|
+
element.of(type)
|
|
69
|
+
end
|
|
70
|
+
union.variant do |element|
|
|
71
|
+
element.reference(filter_type)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
else
|
|
75
|
+
object.reference?(name, to: filter_type)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
associations.each do |name, filter_type|
|
|
80
|
+
object.reference?(name, to: filter_type)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
return unless type?(TYPE_NAME)
|
|
85
|
+
|
|
86
|
+
action(:index) do |act|
|
|
87
|
+
act.request do |request|
|
|
88
|
+
request.query do |query|
|
|
89
|
+
query.union?(TYPE_NAME) do |union|
|
|
90
|
+
union.variant do |element|
|
|
91
|
+
element.reference(TYPE_NAME)
|
|
92
|
+
end
|
|
93
|
+
union.variant do |element|
|
|
94
|
+
element.array do |array|
|
|
95
|
+
array.reference(TYPE_NAME)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def build_polymorphic_type_filters
|
|
107
|
+
representation_class.attributes.each do |name, attribute|
|
|
108
|
+
next unless attribute.filterable?
|
|
109
|
+
|
|
110
|
+
association = representation_class.polymorphic_association_for_type_column(name)
|
|
111
|
+
next unless association
|
|
112
|
+
|
|
113
|
+
type_name = [name, TYPE_NAME].join('_').to_sym
|
|
114
|
+
next if type?(type_name)
|
|
115
|
+
|
|
116
|
+
enum name, values: association.polymorphic.map(&:polymorphic_name)
|
|
117
|
+
build_enum_filter_union(type_name, scoped_enum_name(name))
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build_sti_type_filters
|
|
122
|
+
return if representation_class.subclass?
|
|
123
|
+
|
|
124
|
+
representation_class.attributes.each do |name, attribute|
|
|
125
|
+
next unless attribute.filterable?
|
|
126
|
+
|
|
127
|
+
inheritance = representation_class.inheritance_for_column(name)
|
|
128
|
+
next unless inheritance
|
|
129
|
+
|
|
130
|
+
type_name = [name, TYPE_NAME].join('_').to_sym
|
|
131
|
+
next if type?(type_name)
|
|
132
|
+
|
|
133
|
+
enum(name, values: inheritance.subclasses.map(&:sti_name))
|
|
134
|
+
build_enum_filter_union(type_name, scoped_enum_name(name))
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def build_enum_filter_union(type_name, enum_name)
|
|
139
|
+
union(type_name) do |union|
|
|
140
|
+
union.variant do |element|
|
|
141
|
+
element.reference(enum_name)
|
|
142
|
+
end
|
|
143
|
+
union.variant(partial: true) do |element|
|
|
144
|
+
element.object do |object|
|
|
145
|
+
object.reference(:eq, to: enum_name)
|
|
146
|
+
object.array(:in) do |array|
|
|
147
|
+
array.reference(enum_name)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def polymorphic_type_column?(attribute)
|
|
155
|
+
representation_class.polymorphic_association_for_type_column(attribute.name).present?
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def sti_type_column?(attribute)
|
|
159
|
+
representation_class.inheritance_for_column(attribute.name).present?
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def filter_type_for(attribute)
|
|
163
|
+
custom_type = [attribute.name, TYPE_NAME].join('_').to_sym
|
|
164
|
+
return custom_type if attribute.enum
|
|
165
|
+
return custom_type if polymorphic_type_column?(attribute)
|
|
166
|
+
return custom_type if sti_type_column?(attribute)
|
|
167
|
+
|
|
168
|
+
type = case attribute.type
|
|
169
|
+
when :string, :binary then :string_filter
|
|
170
|
+
when :date then :date_filter
|
|
171
|
+
when :datetime then :datetime_filter
|
|
172
|
+
when :time then :time_filter
|
|
173
|
+
when :integer then :integer_filter
|
|
174
|
+
when :decimal then :decimal_filter
|
|
175
|
+
when :number then :number_filter
|
|
176
|
+
when :uuid then :uuid_filter
|
|
177
|
+
when :boolean then :boolean_filter
|
|
178
|
+
else :string_filter
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
attribute.nullable? ? [:nullable, type].join('_').to_sym : type
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def filterable?
|
|
185
|
+
representation_class.attributes.values.any? { |attribute| attribute.filterable? && UNFILTERABLE_TYPES.exclude?(attribute.type) } ||
|
|
186
|
+
representation_class.associations.values.any?(&:filterable?)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Filtering
|
|
8
|
+
class Operation
|
|
9
|
+
class Filter
|
|
10
|
+
class Builder
|
|
11
|
+
attr_reader :allowed_types,
|
|
12
|
+
:column,
|
|
13
|
+
:field_name
|
|
14
|
+
|
|
15
|
+
def initialize(column, field_name, allowed_types:)
|
|
16
|
+
@column = column
|
|
17
|
+
@field_name = field_name
|
|
18
|
+
@allowed_types = Array(allowed_types)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def build(value, normalizer: nil, valid_operators:, &block)
|
|
22
|
+
value = normalize_value(value, normalizer) if normalizer
|
|
23
|
+
return nil unless valid_value_type?(value)
|
|
24
|
+
|
|
25
|
+
builder = OperatorBuilder.new(column, field_name, valid_operators:)
|
|
26
|
+
builder.build(value, &block)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def normalize_value(value, normalizer)
|
|
32
|
+
normalizer.call(value)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def valid_value_type?(value)
|
|
36
|
+
return true if allowed_types.empty?
|
|
37
|
+
|
|
38
|
+
allowed_types.any? { |type| value.is_a?(type) }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|