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,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Representation
|
|
5
|
+
class Deserializer
|
|
6
|
+
class << self
|
|
7
|
+
def deserialize(representation_class, payload)
|
|
8
|
+
new(representation_class).deserialize(payload)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(representation_class)
|
|
13
|
+
@representation_class = representation_class
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def deserialize(payload)
|
|
17
|
+
if payload.is_a?(Array)
|
|
18
|
+
payload.map { |hash| deserialize_hash(hash) }
|
|
19
|
+
else
|
|
20
|
+
deserialize_hash(payload)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def deserialize_hash(hash)
|
|
25
|
+
return hash unless hash.is_a?(Hash)
|
|
26
|
+
|
|
27
|
+
result = hash.dup
|
|
28
|
+
|
|
29
|
+
transform_type_columns(result)
|
|
30
|
+
|
|
31
|
+
@representation_class.attributes.each do |name, attribute|
|
|
32
|
+
next unless result.key?(name)
|
|
33
|
+
|
|
34
|
+
result[name] = attribute.decode(result[name])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
@representation_class.associations.each do |name, association|
|
|
38
|
+
next unless result.key?(name)
|
|
39
|
+
|
|
40
|
+
nested_representation_class = association.representation_class
|
|
41
|
+
next unless nested_representation_class
|
|
42
|
+
|
|
43
|
+
value = result[name]
|
|
44
|
+
result[name] = if association.collection? && value.is_a?(Array)
|
|
45
|
+
value.map { |item| nested_representation_class.deserialize(item) }
|
|
46
|
+
elsif value.is_a?(Hash)
|
|
47
|
+
nested_representation_class.deserialize(value)
|
|
48
|
+
else
|
|
49
|
+
value
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
result
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def transform_type_columns(hash)
|
|
59
|
+
transform_sti_type(hash)
|
|
60
|
+
transform_polymorphic_types(hash)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def transform_sti_type(hash)
|
|
64
|
+
inheritance_config = @representation_class.subclass? ? @representation_class.superclass.inheritance : @representation_class.inheritance
|
|
65
|
+
return unless inheritance_config&.transform?
|
|
66
|
+
|
|
67
|
+
column = inheritance_config.column
|
|
68
|
+
return unless hash.key?(column)
|
|
69
|
+
|
|
70
|
+
api_value = hash[column]
|
|
71
|
+
db_value = inheritance_config.mapping[api_value]
|
|
72
|
+
hash[column] = db_value if db_value
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def transform_polymorphic_types(hash)
|
|
76
|
+
@representation_class.attributes.each_key do |name|
|
|
77
|
+
next unless hash.key?(name)
|
|
78
|
+
|
|
79
|
+
association = @representation_class.polymorphic_association_for_type_column(name)
|
|
80
|
+
next unless association
|
|
81
|
+
|
|
82
|
+
api_value = hash[name]
|
|
83
|
+
next unless api_value.is_a?(String)
|
|
84
|
+
|
|
85
|
+
polymorphic_representation = association.polymorphic.find do |representation|
|
|
86
|
+
representation.polymorphic_name == api_value
|
|
87
|
+
end
|
|
88
|
+
next unless polymorphic_representation
|
|
89
|
+
|
|
90
|
+
hash[name] = polymorphic_representation.model_class.polymorphic_name
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Representation
|
|
5
|
+
# @api public
|
|
6
|
+
# Block context for defining JSON blob structure in representation attributes.
|
|
7
|
+
#
|
|
8
|
+
# Used inside attribute blocks to define the shape of JSON/JSONB columns,
|
|
9
|
+
# Rails store attributes, or any serialized data structure.
|
|
10
|
+
#
|
|
11
|
+
# Only complex types are allowed at the top level:
|
|
12
|
+
# - {#object} for key-value structures
|
|
13
|
+
# - {#array} for ordered collections
|
|
14
|
+
# - {#union} for polymorphic structures
|
|
15
|
+
#
|
|
16
|
+
# Inside these blocks, the full type DSL is available including
|
|
17
|
+
# nested objects, arrays, primitives, and unions.
|
|
18
|
+
#
|
|
19
|
+
# @see API::Element Block context for array elements
|
|
20
|
+
# @see API::Object Block context for object fields
|
|
21
|
+
# @see API::Union Block context for union variants
|
|
22
|
+
#
|
|
23
|
+
# @example Object structure
|
|
24
|
+
# attribute :settings do
|
|
25
|
+
# object do
|
|
26
|
+
# string :theme
|
|
27
|
+
# boolean :notifications
|
|
28
|
+
# integer :max_items, min: 1, max: 100
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# @example Array of objects
|
|
33
|
+
# attribute :addresses do
|
|
34
|
+
# array do
|
|
35
|
+
# object do
|
|
36
|
+
# string :street
|
|
37
|
+
# string :city
|
|
38
|
+
# string :zip
|
|
39
|
+
# boolean :primary
|
|
40
|
+
# end
|
|
41
|
+
# end
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# @example Nested structures
|
|
45
|
+
# attribute :config do
|
|
46
|
+
# object do
|
|
47
|
+
# string :name
|
|
48
|
+
# array :tags do
|
|
49
|
+
# string
|
|
50
|
+
# end
|
|
51
|
+
# object :metadata do
|
|
52
|
+
# datetime :created_at
|
|
53
|
+
# datetime :updated_at
|
|
54
|
+
# end
|
|
55
|
+
# end
|
|
56
|
+
# end
|
|
57
|
+
#
|
|
58
|
+
# @example Union for polymorphic data
|
|
59
|
+
# attribute :payment_details do
|
|
60
|
+
# union discriminator: :type do
|
|
61
|
+
# variant tag: 'card' do
|
|
62
|
+
# object do
|
|
63
|
+
# string :last_four
|
|
64
|
+
# string :brand
|
|
65
|
+
# end
|
|
66
|
+
# end
|
|
67
|
+
# variant tag: 'bank' do
|
|
68
|
+
# object do
|
|
69
|
+
# string :account_number
|
|
70
|
+
# string :routing_number
|
|
71
|
+
# end
|
|
72
|
+
# end
|
|
73
|
+
# end
|
|
74
|
+
# end
|
|
75
|
+
class Element < Apiwork::Element
|
|
76
|
+
def validate!
|
|
77
|
+
raise ConfigurationError, 'must define exactly one type (object, array, or union)' unless @defined
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @api public
|
|
81
|
+
# Defines the element type.
|
|
82
|
+
#
|
|
83
|
+
# Only complex types (:object, :array, :union) are allowed.
|
|
84
|
+
#
|
|
85
|
+
# @param type [Symbol] [:array, :object, :union]
|
|
86
|
+
# The element type.
|
|
87
|
+
# @param discriminator [Symbol, nil] (nil)
|
|
88
|
+
# The discriminator field name. Unions only.
|
|
89
|
+
# @yield block for defining nested structure (instance_eval style)
|
|
90
|
+
# @yieldparam shape [API::Object, API::Union, API::Element]
|
|
91
|
+
# @return [void]
|
|
92
|
+
# @raise [ArgumentError] if object, array, or union type is missing block
|
|
93
|
+
def of(type, discriminator: nil, &block)
|
|
94
|
+
case type
|
|
95
|
+
when :object
|
|
96
|
+
raise ConfigurationError, 'object requires a block' unless block
|
|
97
|
+
|
|
98
|
+
builder = API::Object.new
|
|
99
|
+
block.arity.positive? ? yield(builder) : builder.instance_eval(&block)
|
|
100
|
+
@type = :object
|
|
101
|
+
@shape = builder
|
|
102
|
+
@defined = true
|
|
103
|
+
when :array
|
|
104
|
+
raise ConfigurationError, 'array requires a block' unless block
|
|
105
|
+
|
|
106
|
+
inner = API::Element.new
|
|
107
|
+
block.arity.positive? ? yield(inner) : inner.instance_eval(&block)
|
|
108
|
+
inner.validate!
|
|
109
|
+
@type = :array
|
|
110
|
+
@inner = inner
|
|
111
|
+
@shape = inner.shape
|
|
112
|
+
@defined = true
|
|
113
|
+
when :union
|
|
114
|
+
raise ConfigurationError, 'union requires a block' unless block
|
|
115
|
+
|
|
116
|
+
builder = API::Union.new(discriminator:)
|
|
117
|
+
block.arity.positive? ? yield(builder) : builder.instance_eval(&block)
|
|
118
|
+
@type = :union
|
|
119
|
+
@shape = builder
|
|
120
|
+
@discriminator = discriminator
|
|
121
|
+
@defined = true
|
|
122
|
+
else
|
|
123
|
+
raise ConfigurationError, "Representation::Element only supports :object, :array, :union - got #{type.inspect}"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Representation
|
|
5
|
+
# @api public
|
|
6
|
+
# Tracks STI subclass representations for a base representation.
|
|
7
|
+
#
|
|
8
|
+
# Created automatically when a representation's model uses STI.
|
|
9
|
+
# Provides resolution of records to their correct subclass representation.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# ClientRepresentation.inheritance.column # => :type
|
|
13
|
+
# ClientRepresentation.inheritance.subclasses # => [PersonClientRepresentation, ...]
|
|
14
|
+
# ClientRepresentation.inheritance.resolve(record) # => PersonClientRepresentation
|
|
15
|
+
class Inheritance
|
|
16
|
+
# @!attribute [r] base_class
|
|
17
|
+
# @api public
|
|
18
|
+
# The base class for this inheritance.
|
|
19
|
+
#
|
|
20
|
+
# @return [Class<Representation::Base>]
|
|
21
|
+
# @!attribute [r] subclasses
|
|
22
|
+
# @api public
|
|
23
|
+
# The subclasses for this inheritance.
|
|
24
|
+
#
|
|
25
|
+
# @return [Array<Class<Representation::Base>>]
|
|
26
|
+
attr_reader :base_class,
|
|
27
|
+
:subclasses
|
|
28
|
+
|
|
29
|
+
def initialize(base_class)
|
|
30
|
+
@base_class = base_class
|
|
31
|
+
@subclasses = []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @api public
|
|
35
|
+
# The column for this inheritance.
|
|
36
|
+
#
|
|
37
|
+
# @return [Symbol]
|
|
38
|
+
def column
|
|
39
|
+
@base_class.model_class.inheritance_column.to_sym
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @api public
|
|
43
|
+
# Resolves a record to its subclass representation.
|
|
44
|
+
#
|
|
45
|
+
# @param record [ActiveRecord::Base]
|
|
46
|
+
# The record to resolve.
|
|
47
|
+
# @return [Class<Representation::Base>, nil]
|
|
48
|
+
def resolve(record)
|
|
49
|
+
type_value = record.public_send(column)
|
|
50
|
+
@subclasses.find { |klass| klass.model_class.sti_name == type_value }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @api public
|
|
54
|
+
# Whether this inheritance requires type transformation.
|
|
55
|
+
#
|
|
56
|
+
# @return [Boolean]
|
|
57
|
+
def transform?
|
|
58
|
+
@subclasses.any? { |klass| klass.sti_name != klass.model_class.sti_name }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @api public
|
|
62
|
+
# Mapping of API names to database type values.
|
|
63
|
+
#
|
|
64
|
+
# @return [Hash{String => String}]
|
|
65
|
+
def mapping
|
|
66
|
+
@subclasses.to_h { |klass| [klass.sti_name, klass.model_class.sti_name] }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def register(representation_class)
|
|
70
|
+
@subclasses << representation_class
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def subclass?(representation_class)
|
|
74
|
+
@subclasses.include?(representation_class)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Representation
|
|
5
|
+
class ModelDetector
|
|
6
|
+
def initialize(representation_class)
|
|
7
|
+
@representation_class = representation_class
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def detect
|
|
11
|
+
return nil if @representation_class.abstract?
|
|
12
|
+
|
|
13
|
+
full_name = @representation_class.name
|
|
14
|
+
return nil unless full_name
|
|
15
|
+
|
|
16
|
+
representation_name = full_name.demodulize
|
|
17
|
+
model_name = representation_name.delete_suffix('Representation')
|
|
18
|
+
return nil if model_name.blank?
|
|
19
|
+
|
|
20
|
+
resolve_model_class(full_name, model_name)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def sti_base?(model_class)
|
|
24
|
+
return false if model_class.abstract_class?
|
|
25
|
+
|
|
26
|
+
column = model_class.inheritance_column
|
|
27
|
+
return false unless column
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
return false unless model_class.column_names.include?(column.to_s)
|
|
31
|
+
rescue ActiveRecord::StatementInvalid, ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
|
|
32
|
+
return false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
model_class == model_class.base_class
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def sti_subclass?(model_class)
|
|
39
|
+
return false if model_class.abstract_class?
|
|
40
|
+
|
|
41
|
+
model_class != model_class.base_class
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def superclass_is_sti_base?(model_class)
|
|
45
|
+
parent_model = @representation_class.superclass.model_class
|
|
46
|
+
return false unless parent_model
|
|
47
|
+
|
|
48
|
+
parent_model == model_class.base_class
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def resolve_model_class(full_name, model_name)
|
|
54
|
+
namespace = full_name.deconstantize
|
|
55
|
+
model_class = if namespace.present?
|
|
56
|
+
"#{namespace}::#{model_name}".safe_constantize || model_name.safe_constantize
|
|
57
|
+
else
|
|
58
|
+
model_name.safe_constantize
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if model_class.is_a?(Class) && model_class < ActiveRecord::Base
|
|
62
|
+
model_class
|
|
63
|
+
else
|
|
64
|
+
raise ConfigurationError.new(
|
|
65
|
+
code: :model_not_found,
|
|
66
|
+
detail: "Could not find model '#{model_name}' for #{full_name}. " \
|
|
67
|
+
"Either create the model, declare it explicitly with 'model YourModel', " \
|
|
68
|
+
"or mark this representation as abstract with 'abstract!'",
|
|
69
|
+
path: [],
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Representation
|
|
5
|
+
# @api public
|
|
6
|
+
# Represents the JSON root key for a representation.
|
|
7
|
+
#
|
|
8
|
+
# Root keys wrap response data in a named container.
|
|
9
|
+
# Used by adapters to structure JSON responses.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# root_key = InvoiceRepresentation.root_key
|
|
13
|
+
# root_key.singular # => "invoice"
|
|
14
|
+
# root_key.plural # => "invoices"
|
|
15
|
+
class RootKey
|
|
16
|
+
# @!attribute [r] plural
|
|
17
|
+
# @api public
|
|
18
|
+
# The plural root key.
|
|
19
|
+
#
|
|
20
|
+
# @return [String]
|
|
21
|
+
# @!attribute [r] singular
|
|
22
|
+
# @api public
|
|
23
|
+
# The singular root key.
|
|
24
|
+
#
|
|
25
|
+
# @return [String]
|
|
26
|
+
attr_reader :plural,
|
|
27
|
+
:singular
|
|
28
|
+
|
|
29
|
+
def initialize(singular, plural = singular.pluralize)
|
|
30
|
+
@singular = singular
|
|
31
|
+
@plural = plural
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Representation
|
|
5
|
+
class Serializer
|
|
6
|
+
class << self
|
|
7
|
+
def serialize(representation, includes)
|
|
8
|
+
new(representation, includes).serialize
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(representation, includes)
|
|
13
|
+
@representation = representation
|
|
14
|
+
@representation_class = representation.class
|
|
15
|
+
@includes = includes
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def serialize
|
|
19
|
+
fields = {}
|
|
20
|
+
|
|
21
|
+
add_discriminator_field(fields) if @representation_class.subclass?
|
|
22
|
+
|
|
23
|
+
@representation_class.attributes.each do |name, attribute|
|
|
24
|
+
value = @representation.respond_to?(name) ? @representation.public_send(name) : @representation.record.public_send(name)
|
|
25
|
+
value = map_type_column_output(name, value)
|
|
26
|
+
value = attribute.encode(value)
|
|
27
|
+
fields[name] = value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
@representation_class.associations.each do |name, association|
|
|
31
|
+
next unless include_association?(name, association)
|
|
32
|
+
|
|
33
|
+
fields[name] = serialize_association(name, association)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
fields
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def add_discriminator_field(fields)
|
|
42
|
+
parent_representation = @representation_class.superclass
|
|
43
|
+
return unless parent_representation.inheritance
|
|
44
|
+
|
|
45
|
+
fields[parent_representation.inheritance.column] = @representation_class.sti_name
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def map_type_column_output(attribute_name, value)
|
|
49
|
+
return value if value.nil?
|
|
50
|
+
|
|
51
|
+
association = @representation_class.polymorphic_association_for_type_column(attribute_name)
|
|
52
|
+
if association
|
|
53
|
+
found_class = association.find_representation_for_type(value)
|
|
54
|
+
return found_class.polymorphic_name if found_class
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
inheritance = @representation_class.inheritance_for_column(attribute_name)
|
|
58
|
+
if inheritance
|
|
59
|
+
klass = inheritance.subclasses.find { |subclass| subclass.model_class.sti_name == value }
|
|
60
|
+
return klass.sti_name if klass
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
value
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def serialize_association(name, association)
|
|
67
|
+
target = @representation.respond_to?(name) ? @representation.public_send(name) : @representation.record.public_send(name)
|
|
68
|
+
return nil if target.nil?
|
|
69
|
+
|
|
70
|
+
target_representation_class = association.representation_class
|
|
71
|
+
return nil unless target_representation_class
|
|
72
|
+
|
|
73
|
+
nested_includes = @includes[name] || @includes[name.to_s] || @includes[name.to_sym] if @includes.is_a?(Hash)
|
|
74
|
+
|
|
75
|
+
if association.collection?
|
|
76
|
+
target.map { |record| serialize_variant_aware(record, target_representation_class, nested_includes) }
|
|
77
|
+
else
|
|
78
|
+
serialize_variant_aware(target, target_representation_class, nested_includes)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def serialize_variant_aware(record, target_representation_class, nested_includes)
|
|
83
|
+
if target_representation_class.inheritance&.subclasses&.any?
|
|
84
|
+
subclass_representation_class = target_representation_class.inheritance.resolve(record)
|
|
85
|
+
end
|
|
86
|
+
representation_class = subclass_representation_class || target_representation_class
|
|
87
|
+
|
|
88
|
+
representation_class.new(record, context: @representation.context, include: nested_includes).as_json
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def include_association?(name, association)
|
|
92
|
+
return explicitly_included?(name) unless association.include == :always
|
|
93
|
+
return true unless circular_reference?(association)
|
|
94
|
+
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def circular_reference?(association)
|
|
99
|
+
return false unless association.representation_class
|
|
100
|
+
|
|
101
|
+
association.representation_class.associations.values.any? do |nested_association|
|
|
102
|
+
nested_association.include == :always && nested_association.representation_class == @representation_class
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def explicitly_included?(name)
|
|
107
|
+
return false if @includes.nil?
|
|
108
|
+
|
|
109
|
+
case @includes
|
|
110
|
+
when Symbol, String
|
|
111
|
+
@includes.to_sym == name
|
|
112
|
+
when Array
|
|
113
|
+
include_symbols.include?(name)
|
|
114
|
+
when Hash
|
|
115
|
+
name = name.to_sym
|
|
116
|
+
@includes.key?(name) || @includes.key?(name.to_s)
|
|
117
|
+
else
|
|
118
|
+
false
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def include_symbols
|
|
123
|
+
@include_symbols ||= @includes.map(&:to_sym)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
# @api public
|
|
5
|
+
# Immutable value object representing a request.
|
|
6
|
+
#
|
|
7
|
+
# Encapsulates query and body parameters. Transformations return
|
|
8
|
+
# new instances, preserving immutability.
|
|
9
|
+
#
|
|
10
|
+
# @example Creating a request
|
|
11
|
+
# request = Request.new(query: { page: 1 }, body: { title: "Hello" })
|
|
12
|
+
# request.query # => { page: 1 }
|
|
13
|
+
# request.body # => { title: "Hello" }
|
|
14
|
+
#
|
|
15
|
+
# @example Transforming keys
|
|
16
|
+
# request.transform { |data| normalize(data) }
|
|
17
|
+
class Request
|
|
18
|
+
# @!attribute [r] query
|
|
19
|
+
# @api public
|
|
20
|
+
# The query for this request.
|
|
21
|
+
#
|
|
22
|
+
# @return [Hash]
|
|
23
|
+
# @!attribute [r] body
|
|
24
|
+
# @api public
|
|
25
|
+
# The body for this request.
|
|
26
|
+
#
|
|
27
|
+
# @return [Hash]
|
|
28
|
+
attr_reader :body,
|
|
29
|
+
:query
|
|
30
|
+
|
|
31
|
+
# @api public
|
|
32
|
+
# Creates a new request context.
|
|
33
|
+
#
|
|
34
|
+
# @param body [Hash]
|
|
35
|
+
# The body parameters.
|
|
36
|
+
# @param query [Hash]
|
|
37
|
+
# The query parameters.
|
|
38
|
+
def initialize(body:, query:)
|
|
39
|
+
@query = query
|
|
40
|
+
@body = body
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @api public
|
|
44
|
+
# Transforms both query and body with the same block.
|
|
45
|
+
#
|
|
46
|
+
# @yield [Hash] each field (query, then body)
|
|
47
|
+
# @return [Request]
|
|
48
|
+
#
|
|
49
|
+
# @example
|
|
50
|
+
# request.transform { |data| normalize(data) }
|
|
51
|
+
def transform
|
|
52
|
+
self.class.new(body: yield(body), query: yield(query))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @api public
|
|
56
|
+
# Transforms only the query.
|
|
57
|
+
#
|
|
58
|
+
# @yield [Hash] the query parameters
|
|
59
|
+
# @return [Request]
|
|
60
|
+
#
|
|
61
|
+
# @example
|
|
62
|
+
# request.transform_query { |query| normalize(query) }
|
|
63
|
+
def transform_query
|
|
64
|
+
self.class.new(body: body, query: yield(query))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @api public
|
|
68
|
+
# Transforms only the body.
|
|
69
|
+
#
|
|
70
|
+
# @yield [Hash] the body parameters
|
|
71
|
+
# @return [Request]
|
|
72
|
+
#
|
|
73
|
+
# @example
|
|
74
|
+
# request.transform_body { |body| prepare(body) }
|
|
75
|
+
def transform_body
|
|
76
|
+
self.class.new(body: yield(body), query: query)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
# @api public
|
|
5
|
+
# Immutable value object representing a response.
|
|
6
|
+
#
|
|
7
|
+
# Encapsulates body parameters. Transformations return new instances,
|
|
8
|
+
# preserving immutability.
|
|
9
|
+
#
|
|
10
|
+
# @example Creating a response
|
|
11
|
+
# response = Response.new(body: { id: 1, title: "Hello" })
|
|
12
|
+
# response.body # => { id: 1, title: "Hello" }
|
|
13
|
+
#
|
|
14
|
+
# @example Transforming keys
|
|
15
|
+
# response.transform { |data| camelize(data) }
|
|
16
|
+
class Response
|
|
17
|
+
# @api public
|
|
18
|
+
# The body for this response.
|
|
19
|
+
#
|
|
20
|
+
# @return [Hash]
|
|
21
|
+
attr_reader :body
|
|
22
|
+
|
|
23
|
+
# @api public
|
|
24
|
+
# Creates a new response context.
|
|
25
|
+
#
|
|
26
|
+
# @param body [Hash]
|
|
27
|
+
# The body parameters.
|
|
28
|
+
def initialize(body:)
|
|
29
|
+
@body = body
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @api public
|
|
33
|
+
# Transforms the body parameters.
|
|
34
|
+
#
|
|
35
|
+
# @yield [Hash] the body parameters
|
|
36
|
+
# @return [Response]
|
|
37
|
+
#
|
|
38
|
+
# @example
|
|
39
|
+
# response.transform { |data| camelize(data) }
|
|
40
|
+
def transform
|
|
41
|
+
self.class.new(body: yield(body))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @api public
|
|
45
|
+
# Transforms the body parameters.
|
|
46
|
+
#
|
|
47
|
+
# @yield [Hash] the body parameters
|
|
48
|
+
# @return [Response]
|
|
49
|
+
#
|
|
50
|
+
# @example
|
|
51
|
+
# response.transform_body { |data| camelize(data) }
|
|
52
|
+
def transform_body
|
|
53
|
+
self.class.new(body: yield(body))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|