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.
- 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 +638 -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,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Including
|
|
8
|
+
class ContractBuilder < Adapter::Capability::Contract::Base
|
|
9
|
+
TYPE_NAME = :include
|
|
10
|
+
MAX_RECURSION_DEPTH = 3
|
|
11
|
+
|
|
12
|
+
def build
|
|
13
|
+
return unless build_type(representation_class)
|
|
14
|
+
|
|
15
|
+
scope.actions.each_key do |action_name|
|
|
16
|
+
action(action_name) do |action|
|
|
17
|
+
action.request do |request|
|
|
18
|
+
request.query do |query|
|
|
19
|
+
query.reference?(TYPE_NAME)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def build_type(representation_class, depth: 0, visited: Set.new)
|
|
29
|
+
return nil unless representation_class.associations.any?
|
|
30
|
+
return nil unless includable_params?(representation_class, depth:, visited:)
|
|
31
|
+
|
|
32
|
+
type_name = type_name_for(representation_class, depth)
|
|
33
|
+
return type_name if type?(type_name)
|
|
34
|
+
return type_name if depth >= MAX_RECURSION_DEPTH
|
|
35
|
+
|
|
36
|
+
visited = visited.dup.add(representation_class)
|
|
37
|
+
|
|
38
|
+
association_params = compute_association_params(
|
|
39
|
+
representation_class,
|
|
40
|
+
depth:,
|
|
41
|
+
visited:,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
object(type_name) do |object|
|
|
45
|
+
association_params.each do |param_options|
|
|
46
|
+
name = param_options[:name]
|
|
47
|
+
include_type = param_options[:include_type]
|
|
48
|
+
|
|
49
|
+
case param_options[:param_type]
|
|
50
|
+
when :boolean
|
|
51
|
+
object.boolean(name, optional: true) unless param_options[:include_mode] == :always
|
|
52
|
+
when :reference
|
|
53
|
+
object.reference(name, optional: true, to: include_type)
|
|
54
|
+
when :union
|
|
55
|
+
object.union(name, optional: true) do |union|
|
|
56
|
+
union.variant(&:boolean)
|
|
57
|
+
union.variant do |element|
|
|
58
|
+
element.reference(include_type)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
type_name
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def compute_association_params(representation_class, depth:, visited:)
|
|
69
|
+
representation_class.associations.filter_map do |name, association|
|
|
70
|
+
compute_single_association_param(name, association, representation_class, depth:, visited:)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def compute_single_association_param(name, association, representation_class, depth:, visited:)
|
|
75
|
+
if association.polymorphic?
|
|
76
|
+
return {
|
|
77
|
+
name:,
|
|
78
|
+
include_mode: association.include,
|
|
79
|
+
include_type: nil,
|
|
80
|
+
param_type: :boolean,
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
nested_representation_class = association.representation_class
|
|
85
|
+
return nil unless nested_representation_class
|
|
86
|
+
|
|
87
|
+
if visited.include?(nested_representation_class)
|
|
88
|
+
return {
|
|
89
|
+
name:,
|
|
90
|
+
include_mode: association.include,
|
|
91
|
+
include_type: nil,
|
|
92
|
+
param_type: :boolean,
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
include_type = resolve_association_include_type(
|
|
97
|
+
nested_representation_class,
|
|
98
|
+
depth:,
|
|
99
|
+
visited:,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if include_type.nil?
|
|
103
|
+
{
|
|
104
|
+
include_type:,
|
|
105
|
+
name:,
|
|
106
|
+
include_mode: association.include,
|
|
107
|
+
param_type: :boolean,
|
|
108
|
+
}
|
|
109
|
+
elsif association.include == :always
|
|
110
|
+
{
|
|
111
|
+
include_type:,
|
|
112
|
+
name:,
|
|
113
|
+
include_mode: association.include,
|
|
114
|
+
param_type: :reference,
|
|
115
|
+
}
|
|
116
|
+
else
|
|
117
|
+
{
|
|
118
|
+
include_type:,
|
|
119
|
+
name:,
|
|
120
|
+
include_mode: association.include,
|
|
121
|
+
param_type: :union,
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def resolve_association_include_type(representation_class, depth:, visited:)
|
|
127
|
+
contract_class = contract_for(representation_class)
|
|
128
|
+
return build_type(representation_class, visited:, depth: depth + 1) unless contract_class
|
|
129
|
+
|
|
130
|
+
alias_name = representation_class.root_key.singular.to_sym
|
|
131
|
+
import(contract_class, as: alias_name)
|
|
132
|
+
imported_type = [alias_name, TYPE_NAME].join('_').to_sym
|
|
133
|
+
type?(imported_type) ? imported_type : nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def includable_params?(representation_class, depth:, visited:)
|
|
137
|
+
return false if depth >= MAX_RECURSION_DEPTH
|
|
138
|
+
|
|
139
|
+
new_visited = visited.dup.add(representation_class)
|
|
140
|
+
|
|
141
|
+
representation_class.associations.values.any? do |association|
|
|
142
|
+
if association.polymorphic?
|
|
143
|
+
association.include != :always
|
|
144
|
+
else
|
|
145
|
+
nested_representation_class = association.representation_class
|
|
146
|
+
next false unless nested_representation_class
|
|
147
|
+
|
|
148
|
+
if new_visited.include?(nested_representation_class)
|
|
149
|
+
association.include != :always
|
|
150
|
+
elsif association.include == :always
|
|
151
|
+
includable_params?(nested_representation_class, depth: depth + 1, visited: new_visited)
|
|
152
|
+
else
|
|
153
|
+
true
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def type_name_for(representation_class, depth)
|
|
160
|
+
return TYPE_NAME if depth.zero?
|
|
161
|
+
|
|
162
|
+
[representation_class.root_key.singular, TYPE_NAME].join('_').to_sym
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Including
|
|
8
|
+
class Operation < Adapter::Capability::Operation::Base
|
|
9
|
+
def apply
|
|
10
|
+
params = request.query.fetch(:include, {})
|
|
11
|
+
includes = IncludesResolver.resolve(representation_class, params, include_always: true)
|
|
12
|
+
|
|
13
|
+
result(includes:, serialize_options: { include: params })
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Including < Adapter::Capability::Base
|
|
8
|
+
capability_name :including
|
|
9
|
+
|
|
10
|
+
contract_builder ContractBuilder
|
|
11
|
+
operation Operation
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Pagination
|
|
8
|
+
class APIBuilder < Adapter::Capability::API::Base
|
|
9
|
+
def build
|
|
10
|
+
return unless scope.index_actions?
|
|
11
|
+
|
|
12
|
+
if configured(:strategy).include?(:offset)
|
|
13
|
+
object(:offset_pagination) do |object|
|
|
14
|
+
object.integer(:current)
|
|
15
|
+
object.integer?(:next, nullable: true)
|
|
16
|
+
object.integer?(:prev, nullable: true)
|
|
17
|
+
object.integer(:total)
|
|
18
|
+
object.integer(:items)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
return unless configured(:strategy).include?(:cursor)
|
|
23
|
+
|
|
24
|
+
object(:cursor_pagination) do |object|
|
|
25
|
+
object.string?(:next, nullable: true)
|
|
26
|
+
object.string?(:prev, nullable: true)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Pagination
|
|
8
|
+
class ContractBuilder < Adapter::Capability::Contract::Base
|
|
9
|
+
def build
|
|
10
|
+
return unless scope.action?(:index)
|
|
11
|
+
|
|
12
|
+
object(:page) do |object|
|
|
13
|
+
if options.strategy == :cursor
|
|
14
|
+
object.string?(:after)
|
|
15
|
+
object.string?(:before)
|
|
16
|
+
else
|
|
17
|
+
object.integer?(:number, min: 1)
|
|
18
|
+
end
|
|
19
|
+
object.integer?(:size, max: options.max_size, min: 1)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
action(:index) do |action|
|
|
23
|
+
action.request do |request|
|
|
24
|
+
request.query do |query|
|
|
25
|
+
query.reference?(:page)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Pagination
|
|
8
|
+
class Operation
|
|
9
|
+
module Paginate
|
|
10
|
+
class Cursor
|
|
11
|
+
class << self
|
|
12
|
+
def apply(relation, config, params)
|
|
13
|
+
new(relation, config, params).apply
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(relation, config, params)
|
|
18
|
+
@relation = relation
|
|
19
|
+
@config = config
|
|
20
|
+
@params = params
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def apply
|
|
24
|
+
size = @params.fetch(:size, @config.default_size).to_i
|
|
25
|
+
records = fetch_records(size)
|
|
26
|
+
has_more = records.length > size
|
|
27
|
+
records = records.first(size)
|
|
28
|
+
|
|
29
|
+
{ data: records, metadata: build_metadata(records, has_more) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def fetch_records(size)
|
|
35
|
+
table = @relation.klass.arel_table
|
|
36
|
+
column = table[primary_key]
|
|
37
|
+
|
|
38
|
+
if @params[:after]
|
|
39
|
+
cursor_value = decode_cursor(@params[:after], field: :after)[primary_key]
|
|
40
|
+
@relation.where(column.gt(cursor_value)).order(column.asc).limit(size + 1).to_a
|
|
41
|
+
elsif @params[:before]
|
|
42
|
+
cursor_value = decode_cursor(@params[:before], field: :before)[primary_key]
|
|
43
|
+
@relation.where(column.lt(cursor_value)).order(column.desc).limit(size + 1).to_a.reverse
|
|
44
|
+
else
|
|
45
|
+
@relation.order(column.asc).limit(size + 1).to_a
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def primary_key
|
|
50
|
+
return @primary_key if defined?(@primary_key)
|
|
51
|
+
|
|
52
|
+
key = @relation.klass.primary_key
|
|
53
|
+
raise NotImplementedError, 'Cursor pagination does not support composite primary keys' if key.is_a?(Array)
|
|
54
|
+
|
|
55
|
+
@primary_key = key.to_sym
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build_metadata(records, has_more)
|
|
59
|
+
{
|
|
60
|
+
pagination: {
|
|
61
|
+
next: has_more && records.any? ? encode_cursor(records.last) : nil,
|
|
62
|
+
prev: (@params[:after] || @params[:before]) && records.any? ? encode_cursor(records.first) : nil,
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def encode_cursor(record)
|
|
68
|
+
Base64.urlsafe_encode64({ primary_key => record.public_send(primary_key) }.to_json)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def decode_cursor(cursor, field:)
|
|
72
|
+
JSON.parse(Base64.urlsafe_decode64(cursor)).symbolize_keys
|
|
73
|
+
rescue ArgumentError, JSON::ParserError
|
|
74
|
+
issue = Issue.new(:value_invalid, 'Invalid value', meta: { field:, expected: 'cursor' }, path: [:page, field])
|
|
75
|
+
raise ContractError, issue
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Pagination
|
|
8
|
+
class Operation
|
|
9
|
+
module Paginate
|
|
10
|
+
class Offset
|
|
11
|
+
class << self
|
|
12
|
+
def apply(relation, config, params)
|
|
13
|
+
new(relation, config, params).apply
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(relation, config, params)
|
|
18
|
+
@relation = relation
|
|
19
|
+
@config = config
|
|
20
|
+
@params = params
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def apply
|
|
24
|
+
number = @params.fetch(:number, 1).to_i
|
|
25
|
+
size = [@params.fetch(:size, @config.default_size).to_i, 1].max
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
data: @relation.limit(size).offset((number - 1) * size),
|
|
29
|
+
metadata: build_metadata(number, size),
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def build_metadata(number, size)
|
|
36
|
+
items = count_items
|
|
37
|
+
total = (items.to_f / size).ceil
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
pagination: {
|
|
41
|
+
items:,
|
|
42
|
+
total:,
|
|
43
|
+
current: number,
|
|
44
|
+
next: (number < total ? number + 1 : nil),
|
|
45
|
+
prev: (number > 1 ? number - 1 : nil),
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def count_items
|
|
51
|
+
count_result = if @relation.joins_values.any?
|
|
52
|
+
@relation.except(:limit, :offset, :group).distinct.count(:all)
|
|
53
|
+
else
|
|
54
|
+
@relation.except(:limit, :offset, :group).count
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
count_result.is_a?(Hash) ? count_result.size : count_result
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Pagination
|
|
8
|
+
class Operation
|
|
9
|
+
module Paginate
|
|
10
|
+
class << self
|
|
11
|
+
def apply(data, options, params)
|
|
12
|
+
case options.strategy
|
|
13
|
+
when :offset then Offset.apply(data, options, params)
|
|
14
|
+
when :cursor then Cursor.apply(data, options, params)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Pagination
|
|
8
|
+
class Operation < Adapter::Capability::Operation::Base
|
|
9
|
+
target :collection
|
|
10
|
+
|
|
11
|
+
metadata_shape do
|
|
12
|
+
reference(:pagination, to: (options.strategy == :cursor ? :cursor_pagination : :offset_pagination))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def apply
|
|
16
|
+
params = request.query.fetch(:page, {})
|
|
17
|
+
result(**Paginate.apply(data, options, params))
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Pagination < Adapter::Capability::Base
|
|
8
|
+
capability_name :pagination
|
|
9
|
+
|
|
10
|
+
option :strategy, default: :offset, enum: %i[offset cursor], type: :symbol
|
|
11
|
+
option :default_size, default: 20, type: :integer
|
|
12
|
+
option :max_size, default: 100, type: :integer
|
|
13
|
+
|
|
14
|
+
api_builder APIBuilder
|
|
15
|
+
contract_builder ContractBuilder
|
|
16
|
+
operation Operation
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Sorting
|
|
8
|
+
class APIBuilder < Adapter::Capability::API::Base
|
|
9
|
+
def build
|
|
10
|
+
return unless scope.sortable?
|
|
11
|
+
|
|
12
|
+
enum :sort_direction, values: %w[asc desc]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Sorting
|
|
8
|
+
class ContractBuilder < Adapter::Capability::Contract::Base
|
|
9
|
+
TYPE_NAME = :sort
|
|
10
|
+
|
|
11
|
+
def build
|
|
12
|
+
return unless build_type
|
|
13
|
+
|
|
14
|
+
action(:index) do |action|
|
|
15
|
+
action.request do |request|
|
|
16
|
+
request.query do |query|
|
|
17
|
+
query.union?(TYPE_NAME) do |union|
|
|
18
|
+
union.variant do |element|
|
|
19
|
+
element.reference(TYPE_NAME)
|
|
20
|
+
end
|
|
21
|
+
union.variant do |element|
|
|
22
|
+
element.array do |array|
|
|
23
|
+
array.reference(TYPE_NAME)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def build_type
|
|
35
|
+
return unless sortable_content?
|
|
36
|
+
|
|
37
|
+
object(TYPE_NAME) do |object|
|
|
38
|
+
collect_attribute_sorts.each do |name|
|
|
39
|
+
object.reference?(name, to: :sort_direction)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
collect_association_sorts.each do |sort_config|
|
|
43
|
+
object.reference?(sort_config[:name], to: sort_config[:type])
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def collect_attribute_sorts
|
|
49
|
+
representation_class.attributes.filter_map do |name, attribute|
|
|
50
|
+
name if attribute.sortable?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def collect_association_sorts
|
|
55
|
+
representation_class.associations.filter_map do |name, association|
|
|
56
|
+
next unless association.sortable?
|
|
57
|
+
next if association.polymorphic?
|
|
58
|
+
|
|
59
|
+
representation = association.representation_class
|
|
60
|
+
next unless representation
|
|
61
|
+
|
|
62
|
+
contract = contract_for(representation)
|
|
63
|
+
next unless contract
|
|
64
|
+
|
|
65
|
+
alias_name = representation.root_key.singular.to_sym
|
|
66
|
+
import(contract, as: alias_name)
|
|
67
|
+
|
|
68
|
+
nested_type = [alias_name, TYPE_NAME].join('_').to_sym
|
|
69
|
+
next unless type?(nested_type)
|
|
70
|
+
|
|
71
|
+
{ name:, type: nested_type }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def sortable_content?
|
|
76
|
+
representation_class.attributes.values.any?(&:sortable?) ||
|
|
77
|
+
representation_class.associations.values.any?(&:sortable?)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Sorting
|
|
8
|
+
class Operation
|
|
9
|
+
class Sort
|
|
10
|
+
attr_reader :representation_class
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def apply(relation, representation_class, params)
|
|
14
|
+
new(relation, representation_class).apply(params)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(relation, representation_class)
|
|
19
|
+
@relation = relation
|
|
20
|
+
@representation_class = representation_class
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def apply(params)
|
|
24
|
+
data = sort_data(params)
|
|
25
|
+
includes = IncludesResolver.resolve(representation_class, params)
|
|
26
|
+
{ data:, includes: }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def build_order_clauses(params, target_klass = representation_class.model_class)
|
|
30
|
+
params.each_with_object([[], []]) do |(key, value), (orders, joins)|
|
|
31
|
+
key = key.to_sym
|
|
32
|
+
|
|
33
|
+
if value.is_a?(String) || value.is_a?(Symbol)
|
|
34
|
+
attribute = representation_class.attributes[key]
|
|
35
|
+
next unless attribute&.sortable?
|
|
36
|
+
|
|
37
|
+
column = target_klass.arel_table[key]
|
|
38
|
+
direction = value.to_sym
|
|
39
|
+
|
|
40
|
+
case direction
|
|
41
|
+
when :asc then orders << column.asc
|
|
42
|
+
when :desc then orders << column.desc
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
elsif value.is_a?(Hash)
|
|
46
|
+
association = target_klass.reflect_on_association(key)
|
|
47
|
+
next unless association
|
|
48
|
+
|
|
49
|
+
association_definition = representation_class.associations[key]
|
|
50
|
+
next unless association_definition&.sortable?
|
|
51
|
+
|
|
52
|
+
association_resource = association_definition.representation_class
|
|
53
|
+
next unless association_resource
|
|
54
|
+
|
|
55
|
+
nested_query = Sort.new(association.klass.all, association_resource)
|
|
56
|
+
nested_orders, nested_joins = nested_query.build_order_clauses(value, association.klass)
|
|
57
|
+
orders.concat(nested_orders)
|
|
58
|
+
|
|
59
|
+
joins << (nested_joins.any? ? { key => nested_joins } : key)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def sort_data(params)
|
|
67
|
+
return @relation if params.blank?
|
|
68
|
+
|
|
69
|
+
params = params.each_with_object({}) { |hash, acc| acc.merge!(hash) } if params.is_a?(Array)
|
|
70
|
+
return @relation unless params.is_a?(Hash)
|
|
71
|
+
|
|
72
|
+
orders, joins = build_order_clauses(params, representation_class.model_class)
|
|
73
|
+
scope = @relation.joins(joins).order(orders)
|
|
74
|
+
scope = scope.distinct if joins.present?
|
|
75
|
+
scope
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|