apiwork 0.0.0.pre → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +2 -2
  3. data/README.md +117 -1
  4. data/Rakefile +5 -3
  5. data/app/controllers/apiwork/errors_controller.rb +13 -0
  6. data/app/controllers/apiwork/exports_controller.rb +22 -0
  7. data/lib/apiwork/abstractable.rb +26 -0
  8. data/lib/apiwork/adapter/base.rb +369 -0
  9. data/lib/apiwork/adapter/builder/api/base.rb +66 -0
  10. data/lib/apiwork/adapter/builder/contract/base.rb +86 -0
  11. data/lib/apiwork/adapter/capability/api/base.rb +51 -0
  12. data/lib/apiwork/adapter/capability/api/scope.rb +64 -0
  13. data/lib/apiwork/adapter/capability/base.rb +291 -0
  14. data/lib/apiwork/adapter/capability/contract/base.rb +37 -0
  15. data/lib/apiwork/adapter/capability/contract/scope.rb +110 -0
  16. data/lib/apiwork/adapter/capability/operation/base.rb +172 -0
  17. data/lib/apiwork/adapter/capability/operation/metadata_shape.rb +165 -0
  18. data/lib/apiwork/adapter/capability/result.rb +21 -0
  19. data/lib/apiwork/adapter/capability/runner.rb +56 -0
  20. data/lib/apiwork/adapter/capability/transformer/request/base.rb +72 -0
  21. data/lib/apiwork/adapter/capability/transformer/response/base.rb +45 -0
  22. data/lib/apiwork/adapter/registry.rb +16 -0
  23. data/lib/apiwork/adapter/serializer/error/base.rb +72 -0
  24. data/lib/apiwork/adapter/serializer/error/default/api_builder.rb +32 -0
  25. data/lib/apiwork/adapter/serializer/error/default.rb +37 -0
  26. data/lib/apiwork/adapter/serializer/resource/base.rb +84 -0
  27. data/lib/apiwork/adapter/serializer/resource/default/contract_builder.rb +209 -0
  28. data/lib/apiwork/adapter/serializer/resource/default.rb +39 -0
  29. data/lib/apiwork/adapter/standard/capability/filtering/api_builder.rb +75 -0
  30. data/lib/apiwork/adapter/standard/capability/filtering/constants.rb +37 -0
  31. data/lib/apiwork/adapter/standard/capability/filtering/contract_builder.rb +193 -0
  32. data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/builder.rb +47 -0
  33. data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/operator_builder.rb +36 -0
  34. data/lib/apiwork/adapter/standard/capability/filtering/operation/filter.rb +462 -0
  35. data/lib/apiwork/adapter/standard/capability/filtering/operation.rb +22 -0
  36. data/lib/apiwork/adapter/standard/capability/filtering/request_transformer.rb +47 -0
  37. data/lib/apiwork/adapter/standard/capability/filtering.rb +18 -0
  38. data/lib/apiwork/adapter/standard/capability/including/contract_builder.rb +169 -0
  39. data/lib/apiwork/adapter/standard/capability/including/operation.rb +20 -0
  40. data/lib/apiwork/adapter/standard/capability/including.rb +16 -0
  41. data/lib/apiwork/adapter/standard/capability/pagination/api_builder.rb +34 -0
  42. data/lib/apiwork/adapter/standard/capability/pagination/contract_builder.rb +35 -0
  43. data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/cursor.rb +84 -0
  44. data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/offset.rb +66 -0
  45. data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate.rb +24 -0
  46. data/lib/apiwork/adapter/standard/capability/pagination/operation.rb +24 -0
  47. data/lib/apiwork/adapter/standard/capability/pagination.rb +21 -0
  48. data/lib/apiwork/adapter/standard/capability/sorting/api_builder.rb +19 -0
  49. data/lib/apiwork/adapter/standard/capability/sorting/contract_builder.rb +84 -0
  50. data/lib/apiwork/adapter/standard/capability/sorting/operation/sort.rb +83 -0
  51. data/lib/apiwork/adapter/standard/capability/sorting/operation.rb +22 -0
  52. data/lib/apiwork/adapter/standard/capability/sorting.rb +17 -0
  53. data/lib/apiwork/adapter/standard/capability/writing/constants.rb +15 -0
  54. data/lib/apiwork/adapter/standard/capability/writing/contract_builder.rb +253 -0
  55. data/lib/apiwork/adapter/standard/capability/writing/operation/issue_mapper.rb +210 -0
  56. data/lib/apiwork/adapter/standard/capability/writing/operation.rb +32 -0
  57. data/lib/apiwork/adapter/standard/capability/writing/request_transformer.rb +37 -0
  58. data/lib/apiwork/adapter/standard/capability/writing.rb +17 -0
  59. data/lib/apiwork/adapter/standard/includes_resolver.rb +106 -0
  60. data/lib/apiwork/adapter/standard.rb +22 -0
  61. data/lib/apiwork/adapter/wrapper/base.rb +70 -0
  62. data/lib/apiwork/adapter/wrapper/collection/base.rb +60 -0
  63. data/lib/apiwork/adapter/wrapper/collection/default.rb +47 -0
  64. data/lib/apiwork/adapter/wrapper/error/base.rb +30 -0
  65. data/lib/apiwork/adapter/wrapper/error/default.rb +34 -0
  66. data/lib/apiwork/adapter/wrapper/member/base.rb +58 -0
  67. data/lib/apiwork/adapter/wrapper/member/default.rb +40 -0
  68. data/lib/apiwork/adapter/wrapper/shape.rb +203 -0
  69. data/lib/apiwork/adapter.rb +50 -0
  70. data/lib/apiwork/api/base.rb +802 -0
  71. data/lib/apiwork/api/element.rb +110 -0
  72. data/lib/apiwork/api/enum_registry/definition.rb +51 -0
  73. data/lib/apiwork/api/enum_registry.rb +98 -0
  74. data/lib/apiwork/api/info/contact.rb +67 -0
  75. data/lib/apiwork/api/info/license.rb +50 -0
  76. data/lib/apiwork/api/info/server.rb +50 -0
  77. data/lib/apiwork/api/info.rb +221 -0
  78. data/lib/apiwork/api/object.rb +235 -0
  79. data/lib/apiwork/api/registry.rb +33 -0
  80. data/lib/apiwork/api/representation_registry.rb +76 -0
  81. data/lib/apiwork/api/resource/action.rb +41 -0
  82. data/lib/apiwork/api/resource.rb +648 -0
  83. data/lib/apiwork/api/router.rb +104 -0
  84. data/lib/apiwork/api/type_registry/definition.rb +117 -0
  85. data/lib/apiwork/api/type_registry.rb +99 -0
  86. data/lib/apiwork/api/union.rb +49 -0
  87. data/lib/apiwork/api.rb +85 -0
  88. data/lib/apiwork/configurable.rb +71 -0
  89. data/lib/apiwork/configuration/option.rb +125 -0
  90. data/lib/apiwork/configuration/validatable.rb +25 -0
  91. data/lib/apiwork/configuration.rb +95 -0
  92. data/lib/apiwork/configuration_error.rb +6 -0
  93. data/lib/apiwork/constraint_error.rb +20 -0
  94. data/lib/apiwork/contract/action/request.rb +79 -0
  95. data/lib/apiwork/contract/action/response.rb +87 -0
  96. data/lib/apiwork/contract/action.rb +258 -0
  97. data/lib/apiwork/contract/base.rb +714 -0
  98. data/lib/apiwork/contract/element.rb +130 -0
  99. data/lib/apiwork/contract/object/coercer.rb +194 -0
  100. data/lib/apiwork/contract/object/deserializer.rb +101 -0
  101. data/lib/apiwork/contract/object/transformer.rb +95 -0
  102. data/lib/apiwork/contract/object/validator/result.rb +27 -0
  103. data/lib/apiwork/contract/object/validator.rb +734 -0
  104. data/lib/apiwork/contract/object.rb +566 -0
  105. data/lib/apiwork/contract/request_parser/result.rb +25 -0
  106. data/lib/apiwork/contract/request_parser.rb +72 -0
  107. data/lib/apiwork/contract/response_parser/result.rb +25 -0
  108. data/lib/apiwork/contract/response_parser.rb +35 -0
  109. data/lib/apiwork/contract/union.rb +56 -0
  110. data/lib/apiwork/contract_error.rb +9 -0
  111. data/lib/apiwork/controller.rb +300 -0
  112. data/lib/apiwork/domain_error.rb +13 -0
  113. data/lib/apiwork/element.rb +386 -0
  114. data/lib/apiwork/engine.rb +20 -0
  115. data/lib/apiwork/error.rb +6 -0
  116. data/lib/apiwork/error_code/definition.rb +63 -0
  117. data/lib/apiwork/error_code/registry.rb +18 -0
  118. data/lib/apiwork/error_code.rb +132 -0
  119. data/lib/apiwork/export/base.rb +291 -0
  120. data/lib/apiwork/export/open_api.rb +600 -0
  121. data/lib/apiwork/export/pipeline/writer.rb +66 -0
  122. data/lib/apiwork/export/pipeline.rb +84 -0
  123. data/lib/apiwork/export/registry.rb +16 -0
  124. data/lib/apiwork/export/surface_resolver.rb +189 -0
  125. data/lib/apiwork/export/type_analysis.rb +170 -0
  126. data/lib/apiwork/export/type_script.rb +23 -0
  127. data/lib/apiwork/export/type_script_mapper.rb +349 -0
  128. data/lib/apiwork/export/zod.rb +39 -0
  129. data/lib/apiwork/export/zod_mapper.rb +421 -0
  130. data/lib/apiwork/export.rb +80 -0
  131. data/lib/apiwork/http_error.rb +16 -0
  132. data/lib/apiwork/introspection/action/request.rb +66 -0
  133. data/lib/apiwork/introspection/action/response.rb +57 -0
  134. data/lib/apiwork/introspection/action.rb +124 -0
  135. data/lib/apiwork/introspection/api/info/contact.rb +59 -0
  136. data/lib/apiwork/introspection/api/info/license.rb +49 -0
  137. data/lib/apiwork/introspection/api/info/server.rb +50 -0
  138. data/lib/apiwork/introspection/api/info.rb +107 -0
  139. data/lib/apiwork/introspection/api/resource.rb +83 -0
  140. data/lib/apiwork/introspection/api.rb +92 -0
  141. data/lib/apiwork/introspection/contract.rb +63 -0
  142. data/lib/apiwork/introspection/dump/action.rb +101 -0
  143. data/lib/apiwork/introspection/dump/api.rb +119 -0
  144. data/lib/apiwork/introspection/dump/contract.rb +129 -0
  145. data/lib/apiwork/introspection/dump/param.rb +486 -0
  146. data/lib/apiwork/introspection/dump/resource.rb +112 -0
  147. data/lib/apiwork/introspection/dump/type.rb +339 -0
  148. data/lib/apiwork/introspection/dump.rb +17 -0
  149. data/lib/apiwork/introspection/enum.rb +63 -0
  150. data/lib/apiwork/introspection/error_code.rb +44 -0
  151. data/lib/apiwork/introspection/param/array.rb +88 -0
  152. data/lib/apiwork/introspection/param/base.rb +285 -0
  153. data/lib/apiwork/introspection/param/binary.rb +73 -0
  154. data/lib/apiwork/introspection/param/boolean.rb +73 -0
  155. data/lib/apiwork/introspection/param/date.rb +73 -0
  156. data/lib/apiwork/introspection/param/date_time.rb +73 -0
  157. data/lib/apiwork/introspection/param/decimal.rb +121 -0
  158. data/lib/apiwork/introspection/param/integer.rb +131 -0
  159. data/lib/apiwork/introspection/param/literal.rb +45 -0
  160. data/lib/apiwork/introspection/param/number.rb +121 -0
  161. data/lib/apiwork/introspection/param/object.rb +59 -0
  162. data/lib/apiwork/introspection/param/reference.rb +45 -0
  163. data/lib/apiwork/introspection/param/string.rb +122 -0
  164. data/lib/apiwork/introspection/param/time.rb +73 -0
  165. data/lib/apiwork/introspection/param/union.rb +57 -0
  166. data/lib/apiwork/introspection/param/unknown.rb +26 -0
  167. data/lib/apiwork/introspection/param/uuid.rb +73 -0
  168. data/lib/apiwork/introspection/param.rb +31 -0
  169. data/lib/apiwork/introspection/type.rb +129 -0
  170. data/lib/apiwork/introspection.rb +28 -0
  171. data/lib/apiwork/issue.rb +80 -0
  172. data/lib/apiwork/json_pointer.rb +21 -0
  173. data/lib/apiwork/object.rb +1618 -0
  174. data/lib/apiwork/reference_generator.rb +638 -0
  175. data/lib/apiwork/registry.rb +56 -0
  176. data/lib/apiwork/representation/association.rb +391 -0
  177. data/lib/apiwork/representation/attribute.rb +335 -0
  178. data/lib/apiwork/representation/base.rb +819 -0
  179. data/lib/apiwork/representation/deserializer.rb +95 -0
  180. data/lib/apiwork/representation/element.rb +128 -0
  181. data/lib/apiwork/representation/inheritance.rb +78 -0
  182. data/lib/apiwork/representation/model_detector.rb +75 -0
  183. data/lib/apiwork/representation/root_key.rb +35 -0
  184. data/lib/apiwork/representation/serializer.rb +127 -0
  185. data/lib/apiwork/request.rb +79 -0
  186. data/lib/apiwork/response.rb +56 -0
  187. data/lib/apiwork/union.rb +102 -0
  188. data/lib/apiwork/version.rb +2 -2
  189. data/lib/apiwork.rb +61 -3
  190. data/lib/generators/apiwork/api_generator.rb +38 -0
  191. data/lib/generators/apiwork/contract_generator.rb +25 -0
  192. data/lib/generators/apiwork/install_generator.rb +27 -0
  193. data/lib/generators/apiwork/representation_generator.rb +25 -0
  194. data/lib/generators/apiwork/templates/api/api.rb.tt +4 -0
  195. data/lib/generators/apiwork/templates/contract/contract.rb.tt +6 -0
  196. data/lib/generators/apiwork/templates/install/application_contract.rb.tt +5 -0
  197. data/lib/generators/apiwork/templates/install/application_representation.rb.tt +5 -0
  198. data/lib/generators/apiwork/templates/representation/representation.rb.tt +6 -0
  199. data/lib/tasks/apiwork.rake +102 -0
  200. metadata +319 -19
  201. data/.rubocop.yml +0 -8
  202. data/sig/apiwork.rbs +0 -4
@@ -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