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.
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 +622 -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,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ class Standard
6
+ module Capability
7
+ class Sorting
8
+ class Operation < Adapter::Capability::Operation::Base
9
+ target :collection
10
+
11
+ def apply
12
+ params = request.query[:sort]
13
+ return if params.blank?
14
+
15
+ result(**Sort.apply(data, representation_class, params))
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ class Standard
6
+ module Capability
7
+ class Sorting < Adapter::Capability::Base
8
+ capability_name :sorting
9
+
10
+ api_builder APIBuilder
11
+ contract_builder ContractBuilder
12
+ operation Operation
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ class Standard
6
+ module Capability
7
+ class Writing < Adapter::Capability::Base
8
+ module Constants
9
+ OP = :OP
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ class Standard
6
+ module Capability
7
+ class Writing
8
+ class ContractBuilder < Adapter::Capability::Contract::Base
9
+ def build
10
+ build_enums
11
+ build_payload_types
12
+ build_nested_payload_union if api_class.representation_registry.nested_writable?(representation_class)
13
+
14
+ %i[create update].each do |action_name|
15
+ next unless scope.action?(action_name)
16
+
17
+ payload_type_name = [action_name, 'payload'].join('_').to_sym
18
+ next unless type?(payload_type_name)
19
+
20
+ contract_action = action(action_name)
21
+ next if contract_action.resets_request?
22
+
23
+ contract_action.request do |request|
24
+ request.body do |body|
25
+ body.reference(representation_class.root_key.singular.to_sym, to: payload_type_name)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def build_enums
34
+ representation_class.attributes.each do |name, attribute|
35
+ next unless attribute.enum&.any?
36
+
37
+ enum(name, values: attribute.enum)
38
+ end
39
+ end
40
+
41
+ def build_payload_types
42
+ build_payload_type(:create)
43
+ build_payload_type(:update)
44
+ end
45
+
46
+ def build_payload_type(action_name)
47
+ if sti_base_representation?
48
+ build_sti_payload_union(action_name)
49
+ else
50
+ build_standard_payload(action_name)
51
+ end
52
+ end
53
+
54
+ def build_standard_payload(action_name)
55
+ type_name = [action_name, 'payload'].join('_').to_sym
56
+ return if type?(type_name)
57
+
58
+ object(type_name, description: representation_class.description) do |object|
59
+ if representation_class.subclass?
60
+ parent_inheritance = representation_class.superclass.inheritance
61
+
62
+ object.literal(
63
+ parent_inheritance.column,
64
+ optional: action_name == :update,
65
+ value: representation_class.sti_name,
66
+ )
67
+ end
68
+
69
+ collect_writable_params(action_name).each do |param_config|
70
+ object.param(param_config[:name], **param_config[:options])
71
+ end
72
+ end
73
+ end
74
+
75
+ def build_nested_payload_union
76
+ build_nested_payload(:create)
77
+ build_nested_payload(:update)
78
+ build_nested_payload(:delete)
79
+ build_nested_union
80
+ end
81
+
82
+ def build_nested_payload(action_name)
83
+ type_name = [:nested, action_name, :payload].join('_').to_sym
84
+ return if type?(type_name)
85
+
86
+ writable = action_name != :delete
87
+
88
+ object(type_name) do |object|
89
+ object.literal(Constants::OP, optional: true, value: action_name.to_s)
90
+ object.param(:id, optional: action_name != :delete, type: primary_key_type) unless action_name == :create
91
+
92
+ next unless writable
93
+
94
+ collect_writable_params(action_name).each do |param_config|
95
+ object.param(param_config[:name], **param_config[:options])
96
+ end
97
+ end
98
+ end
99
+
100
+ def build_nested_union
101
+ return if type?(:nested_payload)
102
+
103
+ union(:nested_payload, discriminator: Constants::OP) do |union|
104
+ union.variant(tag: 'create') do |element|
105
+ element.reference(scoped_type_name(:nested_create_payload))
106
+ end
107
+ union.variant(tag: 'update') do |element|
108
+ element.reference(scoped_type_name(:nested_update_payload))
109
+ end
110
+ union.variant(tag: 'delete') do |element|
111
+ element.reference(scoped_type_name(:nested_delete_payload))
112
+ end
113
+ end
114
+ end
115
+
116
+ def build_sti_payload_union(action_name)
117
+ representation_inheritance = representation_class.inheritance
118
+
119
+ variant_refs = representation_inheritance.subclasses.filter_map do |subclass|
120
+ subclass_contract = contract_for(subclass)
121
+ next unless subclass_contract
122
+
123
+ alias_name = subclass.root_key.singular.to_sym
124
+ import(subclass_contract, as: alias_name)
125
+
126
+ { tag: subclass.sti_name, type: [alias_name, action_name, 'payload'].join('_').to_sym }
127
+ end
128
+
129
+ union([action_name, 'payload'].join('_').to_sym, discriminator: representation_inheritance.column) do |union|
130
+ variant_refs.each do |variant_ref|
131
+ union.variant(tag: variant_ref[:tag]) do |element|
132
+ element.reference(variant_ref[:type])
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ def collect_writable_params(action_name)
139
+ params = []
140
+
141
+ representation_class.attributes.each do |name, attribute|
142
+ next unless attribute.writable_for?(action_name)
143
+
144
+ params << { name:, options: attribute_options(attribute, action_name) }
145
+ end
146
+
147
+ representation_class.associations.each do |name, association|
148
+ next unless association.writable_for?(action_name)
149
+
150
+ params << { name:, options: association_options(association) }
151
+ end
152
+
153
+ params
154
+ end
155
+
156
+ def attribute_options(attribute, action_name)
157
+ options = {
158
+ deprecated: attribute.deprecated?,
159
+ description: attribute.description,
160
+ example: attribute.example,
161
+ format: attribute.format,
162
+ nullable: attribute.nullable?,
163
+ optional: action_name == :update || attribute.optional?,
164
+ type: attribute.type,
165
+ }
166
+
167
+ options[:min] = attribute.min if attribute.min
168
+ options[:max] = attribute.max if attribute.max
169
+ options[:of] = attribute.of if attribute.of
170
+ options[:enum] = attribute.name if attribute.enum
171
+
172
+ if attribute.element
173
+ element = attribute.element
174
+
175
+ if element.type == :array
176
+ options[:of] = element.inner
177
+ else
178
+ options[:shape] = element.shape
179
+ options[:discriminator] = element.discriminator if element.discriminator
180
+ end
181
+ end
182
+
183
+ polymorphic_options = polymorphic_type_options(attribute)
184
+ options.merge!(polymorphic_options) if polymorphic_options
185
+
186
+ options
187
+ end
188
+
189
+ def polymorphic_type_options(attribute)
190
+ association = representation_class.polymorphic_association_for_type_column(attribute.name)
191
+ return nil unless association
192
+
193
+ allowed_values = association.polymorphic.map(&:polymorphic_name)
194
+
195
+ { enum: allowed_values }
196
+ end
197
+
198
+ def association_options(association)
199
+ payload_type = resolve_association_payload_type(association)
200
+
201
+ options = {
202
+ as: [association.name, 'attributes'].join('_').to_sym,
203
+ deprecated: association.deprecated?,
204
+ description: association.description,
205
+ example: association.example,
206
+ nullable: association.nullable?,
207
+ optional: true,
208
+ }
209
+
210
+ if payload_type
211
+ if association.collection?
212
+ options[:type] = :array
213
+ options[:of] = payload_type
214
+ else
215
+ options[:type] = payload_type
216
+ end
217
+ else
218
+ options[:type] = association.collection? ? :array : :object
219
+ end
220
+
221
+ options
222
+ end
223
+
224
+ def resolve_association_payload_type(association)
225
+ return nil if association.polymorphic?
226
+
227
+ representation_class = association.representation_class
228
+ return nil unless representation_class
229
+
230
+ association_contract = contract_for(representation_class)
231
+ return nil unless association_contract
232
+
233
+ alias_name = representation_class.root_key.singular.to_sym
234
+ import(association_contract, as: alias_name)
235
+
236
+ [alias_name, 'nested_payload'].join('_').to_sym
237
+ end
238
+
239
+ def sti_base_representation?
240
+ inheritance = representation_class.inheritance
241
+ inheritance&.subclasses&.any? && inheritance.base_class == representation_class
242
+ end
243
+
244
+ def primary_key_type
245
+ model = representation_class.model_class
246
+ model.type_for_attribute(model.primary_key).type
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ class Standard
6
+ module Capability
7
+ class Writing
8
+ class Operation
9
+ class IssueMapper
10
+ CODE_MAP = {
11
+ accepted: :accepted,
12
+ blank: :required,
13
+ confirmation: :confirmed,
14
+ empty: :required,
15
+ equal_to: :eq,
16
+ even: :even,
17
+ exclusion: :not_in,
18
+ greater_than: :gt,
19
+ greater_than_or_equal_to: :gte,
20
+ in: :in,
21
+ inclusion: :in,
22
+ invalid: :invalid,
23
+ less_than: :lt,
24
+ less_than_or_equal_to: :lte,
25
+ not_a_number: :number,
26
+ not_an_integer: :integer,
27
+ odd: :odd,
28
+ other_than: :ne,
29
+ present: :forbidden,
30
+ restrict_dependent_destroy: :associated,
31
+ taken: :unique,
32
+ too_long: :max,
33
+ too_short: :min,
34
+ wrong_length: :length,
35
+ }.freeze
36
+
37
+ DETAIL_MAP = {
38
+ accepted: 'Must be accepted',
39
+ associated: 'Invalid',
40
+ confirmed: 'Does not match',
41
+ eq: 'Wrong value',
42
+ even: 'Must be even',
43
+ forbidden: 'Must be blank',
44
+ format: 'Invalid format',
45
+ gt: 'Too small',
46
+ gte: 'Too small',
47
+ in: 'Invalid value',
48
+ integer: 'Not an integer',
49
+ invalid: 'Invalid',
50
+ length: 'Wrong length',
51
+ lt: 'Too large',
52
+ lte: 'Too large',
53
+ max: 'Too long',
54
+ min: 'Too short',
55
+ ne: 'Reserved value',
56
+ not_in: 'Reserved value',
57
+ number: 'Not a number',
58
+ odd: 'Must be odd',
59
+ required: 'Required',
60
+ unique: 'Already taken',
61
+ }.freeze
62
+
63
+ META_CODES = %i[min max length gt gte lt lte eq ne in].freeze
64
+
65
+ class << self
66
+ def map(record, translator, root_path: [])
67
+ new(record, root_path, translator).map
68
+ end
69
+ end
70
+
71
+ def initialize(record, root_path, translator)
72
+ @record = record
73
+ @root_path = Array(root_path)
74
+ @translator = translator
75
+ end
76
+
77
+ def map
78
+ return [] unless @record.respond_to?(:errors)
79
+ return [] unless @record.errors.any?
80
+
81
+ attribute_issues + association_issues
82
+ end
83
+
84
+ private
85
+
86
+ def attribute_issues
87
+ @record.errors.filter_map do |error|
88
+ next if nested_attribute_error?(error)
89
+
90
+ build_issue(error)
91
+ end
92
+ end
93
+
94
+ def association_issues
95
+ collect_association_issues(:has_many) + collect_association_issues(:has_one)
96
+ end
97
+
98
+ def collect_association_issues(association_type)
99
+ result = []
100
+
101
+ @record.class.reflect_on_all_associations(association_type).each do |association|
102
+ associated = @record.send(association.name)
103
+ next unless associated
104
+
105
+ items = association_type == :has_many ? associated : [associated]
106
+ next unless items.respond_to?(:each)
107
+
108
+ items.each_with_index do |item, index|
109
+ next unless item.respond_to?(:errors)
110
+ next unless item.errors.any?
111
+
112
+ nested_path = build_association_path(association.name, index, association_type)
113
+ result.concat(self.class.map(item, @translator, root_path: nested_path))
114
+ end
115
+ end
116
+
117
+ result
118
+ end
119
+
120
+ def build_association_path(name, index, type)
121
+ if type == :has_many
122
+ @root_path + [name, index]
123
+ else
124
+ @root_path + [name]
125
+ end
126
+ end
127
+
128
+ def nested_attribute_error?(error)
129
+ error.attribute.to_s.include?('.')
130
+ end
131
+
132
+ def build_issue(error)
133
+ code = normalize_code(error)
134
+ attribute = resolve_attribute(error.attribute)
135
+ path = attribute == :base ? @root_path : @root_path + [attribute]
136
+
137
+ Issue.new(
138
+ code,
139
+ detail_for(code),
140
+ path:,
141
+ meta: build_meta(code, error),
142
+ )
143
+ end
144
+
145
+ def normalize_code(error)
146
+ return :invalid unless error.type.is_a?(Symbol)
147
+ return error.type if error.attribute == :base
148
+
149
+ CODE_MAP[error.type] || error.type
150
+ end
151
+
152
+ def detail_for(code)
153
+ result = @translator.call(:issues, code, :detail)
154
+ return result if result
155
+
156
+ DETAIL_MAP[code] || code.to_s.humanize
157
+ end
158
+
159
+ def build_meta(code, error)
160
+ return {} unless META_CODES.include?(code)
161
+ return {} unless error.options
162
+
163
+ if code == :in
164
+ build_range_meta(error)
165
+ else
166
+ build_numeric_meta(code, error)
167
+ end
168
+ end
169
+
170
+ def build_numeric_meta(code, error)
171
+ value = error.options[:count]
172
+ return {} unless value.is_a?(Numeric)
173
+
174
+ meta_key = code == :length ? :exact : code
175
+ { meta_key => value }
176
+ end
177
+
178
+ def build_range_meta(error)
179
+ range = error.options[:in]
180
+ return {} unless range.is_a?(Range)
181
+ return {} unless range.begin.is_a?(Numeric) && range.end.is_a?(Numeric)
182
+
183
+ {
184
+ max: range.end,
185
+ max_exclusive: range.exclude_end?,
186
+ min: range.begin,
187
+ }
188
+ end
189
+
190
+ def resolve_attribute(attribute)
191
+ return attribute if attribute == :base
192
+ return attribute unless belongs_to_association?(attribute)
193
+
194
+ [attribute, 'id'].join('_').to_sym
195
+ end
196
+
197
+ def belongs_to_association?(attribute)
198
+ belongs_to_names.include?(attribute)
199
+ end
200
+
201
+ def belongs_to_names
202
+ @belongs_to_names ||= @record.class.reflect_on_all_associations(:belongs_to).map(&:name)
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ class Standard
6
+ module Capability
7
+ class Writing
8
+ class Operation < Adapter::Capability::Operation::Base
9
+ target :member
10
+
11
+ def apply
12
+ validate_record!(data, representation_class)
13
+ end
14
+
15
+ private
16
+
17
+ def validate_record!(record, representation_class)
18
+ return unless record.respond_to?(:errors) && record.errors.any?
19
+
20
+ issues = IssueMapper.map(
21
+ record,
22
+ method(:translate),
23
+ root_path: [representation_class.root_key.singular.to_sym],
24
+ )
25
+ raise DomainError, issues
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ 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 Writing < Adapter::Capability::Base
8
+ class RequestTransformer < Adapter::Capability::Transformer::Request::Base
9
+ phase :after
10
+
11
+ def transform
12
+ request.transform_body(&method(:transform_value))
13
+ end
14
+
15
+ private
16
+
17
+ def transform_value(value)
18
+ case value
19
+ when Hash then apply(value.transform_values(&method(:transform_value)))
20
+ when Array then value.map(&method(:transform_value))
21
+ else value
22
+ end
23
+ end
24
+
25
+ def apply(hash)
26
+ return hash unless hash.key?(Constants::OP)
27
+
28
+ result = hash.except(Constants::OP)
29
+ result[:_destroy] = true if hash[Constants::OP] == 'delete'
30
+ result
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ class Standard
6
+ module Capability
7
+ class Writing < Adapter::Capability::Base
8
+ capability_name :writing
9
+
10
+ request_transformer RequestTransformer
11
+ contract_builder ContractBuilder
12
+ operation Operation
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end