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,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,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
|