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,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Contract
|
|
5
|
+
# @api public
|
|
6
|
+
# Block context for defining a single type expression.
|
|
7
|
+
#
|
|
8
|
+
# Used inside `array do` and `variant do` blocks where
|
|
9
|
+
# exactly one element type must be defined.
|
|
10
|
+
#
|
|
11
|
+
# @see Contract::Object Block context for object params
|
|
12
|
+
# @see Contract::Union Block context for union variants
|
|
13
|
+
#
|
|
14
|
+
# @example instance_eval style
|
|
15
|
+
# array :ids do
|
|
16
|
+
# integer
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @example yield style
|
|
20
|
+
# array :ids do |element|
|
|
21
|
+
# element.integer
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# @example Array of references
|
|
25
|
+
# array :items do |element|
|
|
26
|
+
# element.reference :item
|
|
27
|
+
# end
|
|
28
|
+
class Element < Apiwork::Element
|
|
29
|
+
def initialize(contract_class)
|
|
30
|
+
super()
|
|
31
|
+
@contract_class = contract_class
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @api public
|
|
35
|
+
# Defines the element type.
|
|
36
|
+
#
|
|
37
|
+
# This is the verbose form. Prefer sugar methods (string, integer, etc.)
|
|
38
|
+
# for static definitions. Use `of` for dynamic element generation.
|
|
39
|
+
#
|
|
40
|
+
# @param type [Symbol] [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :string, :time, :union, :uuid]
|
|
41
|
+
# The element type. Custom type references are also allowed.
|
|
42
|
+
# @param discriminator [Symbol, nil] (nil)
|
|
43
|
+
# The discriminator field name. Unions only.
|
|
44
|
+
# @param enum [Array, Symbol, nil] (nil)
|
|
45
|
+
# The allowed values or enum reference. Strings and integers only.
|
|
46
|
+
# @param format [Symbol, nil] (nil) [:date, :datetime, :double, :email, :float, :hostname, :int32, :int64, :ipv4, :ipv6, :password, :url, :uuid]
|
|
47
|
+
# Format hint for exports. Does not change the type, but exports may add validation or documentation based on it.
|
|
48
|
+
# Valid formats by type: `:decimal`/`:number` (`:double`, `:float`), `:integer` (`:int32`, `:int64`),
|
|
49
|
+
# `:string` (`:date`, `:datetime`, `:email`, `:hostname`, `:ipv4`, `:ipv6`, `:password`, `:url`, `:uuid`).
|
|
50
|
+
# @param max [Integer, nil] (nil)
|
|
51
|
+
# The maximum. For `:decimal`, `:integer`, `:number`: value. For `:string`: length.
|
|
52
|
+
# @param min [Integer, nil] (nil)
|
|
53
|
+
# The minimum. For `:decimal`, `:integer`, `:number`: value. For `:string`: length.
|
|
54
|
+
# @param value [Object, nil] (nil)
|
|
55
|
+
# The literal value. Literals only.
|
|
56
|
+
# @yield block for defining nested structure (instance_eval style)
|
|
57
|
+
# @yieldparam shape [Contract::Object, Contract::Union, Contract::Element]
|
|
58
|
+
# @return [void]
|
|
59
|
+
# @raise [ArgumentError] if object, array, or union type is missing block
|
|
60
|
+
#
|
|
61
|
+
# @example Dynamic element type
|
|
62
|
+
# element_type = :string
|
|
63
|
+
# array :values do
|
|
64
|
+
# of element_type
|
|
65
|
+
# end
|
|
66
|
+
#
|
|
67
|
+
# @example Object with block
|
|
68
|
+
# array :tags do
|
|
69
|
+
# of :object do
|
|
70
|
+
# string :name
|
|
71
|
+
# string :color
|
|
72
|
+
# end
|
|
73
|
+
# end
|
|
74
|
+
def of(type, discriminator: nil, enum: nil, format: nil, max: nil, min: nil, value: nil, &block)
|
|
75
|
+
resolved_enum = enum.is_a?(Symbol) ? resolve_enum(enum) : enum
|
|
76
|
+
|
|
77
|
+
case type
|
|
78
|
+
when :string, :integer, :decimal, :boolean, :number, :datetime, :date, :uuid, :time, :binary
|
|
79
|
+
set_type(type, format:, max:, min:, enum: resolved_enum)
|
|
80
|
+
when :literal
|
|
81
|
+
@type = :literal
|
|
82
|
+
@value = value
|
|
83
|
+
@defined = true
|
|
84
|
+
when :object
|
|
85
|
+
@type = :object
|
|
86
|
+
if block
|
|
87
|
+
shape = Object.new(@contract_class)
|
|
88
|
+
block.arity.positive? ? yield(shape) : shape.instance_eval(&block)
|
|
89
|
+
@shape = shape
|
|
90
|
+
end
|
|
91
|
+
@defined = true
|
|
92
|
+
when :array
|
|
93
|
+
raise ConfigurationError, 'array requires a block' unless block
|
|
94
|
+
|
|
95
|
+
inner = Element.new(@contract_class)
|
|
96
|
+
block.arity.positive? ? yield(inner) : inner.instance_eval(&block)
|
|
97
|
+
inner.validate!
|
|
98
|
+
@type = :array
|
|
99
|
+
@inner = inner
|
|
100
|
+
@shape = inner.shape
|
|
101
|
+
@defined = true
|
|
102
|
+
when :union
|
|
103
|
+
raise ConfigurationError, 'union requires a block' unless block
|
|
104
|
+
|
|
105
|
+
shape = Union.new(@contract_class, discriminator:)
|
|
106
|
+
block.arity.positive? ? yield(shape) : shape.instance_eval(&block)
|
|
107
|
+
@type = :union
|
|
108
|
+
@shape = shape
|
|
109
|
+
@discriminator = discriminator
|
|
110
|
+
@defined = true
|
|
111
|
+
else
|
|
112
|
+
@type = type
|
|
113
|
+
@custom_type = type
|
|
114
|
+
@defined = true
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def resolve_enum(enum)
|
|
121
|
+
return nil if enum.nil?
|
|
122
|
+
return enum if enum.is_a?(Array)
|
|
123
|
+
|
|
124
|
+
raise ConfigurationError, "Enum :#{enum} not found." unless @contract_class.enum?(enum)
|
|
125
|
+
|
|
126
|
+
enum
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Contract
|
|
5
|
+
class Object
|
|
6
|
+
class Coercer
|
|
7
|
+
PRIMITIVES = {
|
|
8
|
+
boolean: lambda { |value|
|
|
9
|
+
return value if [true, false].include?(value)
|
|
10
|
+
return true if %w[true 1 yes].include?(value.to_s.downcase)
|
|
11
|
+
return false if %w[false 0 no].include?(value.to_s.downcase)
|
|
12
|
+
|
|
13
|
+
nil
|
|
14
|
+
},
|
|
15
|
+
date: lambda { |value|
|
|
16
|
+
return value if value.is_a?(Date)
|
|
17
|
+
|
|
18
|
+
Date.parse(value) if value.is_a?(String)
|
|
19
|
+
},
|
|
20
|
+
datetime: lambda { |value|
|
|
21
|
+
return value if value.is_a?(Time) || value.is_a?(DateTime) || value.is_a?(ActiveSupport::TimeWithZone)
|
|
22
|
+
|
|
23
|
+
Time.zone.parse(value) if value.is_a?(String)
|
|
24
|
+
},
|
|
25
|
+
decimal: lambda { |value|
|
|
26
|
+
return value if value.is_a?(BigDecimal)
|
|
27
|
+
|
|
28
|
+
BigDecimal(value.to_s) if value.is_a?(Numeric) || value.is_a?(String)
|
|
29
|
+
},
|
|
30
|
+
integer: lambda { |value|
|
|
31
|
+
return value if value.is_a?(Integer)
|
|
32
|
+
|
|
33
|
+
Integer(value) if value.is_a?(String) && value.match?(/\A-?\d+\z/)
|
|
34
|
+
},
|
|
35
|
+
number: lambda { |value|
|
|
36
|
+
return value if value.is_a?(Float) || value.is_a?(Integer)
|
|
37
|
+
return nil if value.is_a?(String) && value.blank?
|
|
38
|
+
|
|
39
|
+
Float(value) if value.is_a?(String)
|
|
40
|
+
},
|
|
41
|
+
string: lambda { |value|
|
|
42
|
+
return value if value.is_a?(String)
|
|
43
|
+
|
|
44
|
+
value.to_s
|
|
45
|
+
},
|
|
46
|
+
time: lambda { |value|
|
|
47
|
+
return value if value.is_a?(Time) || value.is_a?(DateTime) || value.is_a?(ActiveSupport::TimeWithZone)
|
|
48
|
+
|
|
49
|
+
Time.zone.parse("2000-01-01T#{value}") if value.is_a?(String) && value.match?(/\A\d{2}:\d{2}(:\d{2})?\z/)
|
|
50
|
+
},
|
|
51
|
+
uuid: lambda { |value|
|
|
52
|
+
return value if value.is_a?(String) && value.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
|
|
53
|
+
|
|
54
|
+
nil
|
|
55
|
+
},
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
58
|
+
class << self
|
|
59
|
+
def coerce(shape, hash)
|
|
60
|
+
new(shape).coerce(hash)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def initialize(shape)
|
|
65
|
+
@shape = shape
|
|
66
|
+
@type_cache = {}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def coerce(hash)
|
|
70
|
+
coerced = hash.dup
|
|
71
|
+
|
|
72
|
+
@shape.params.each do |name, param_options|
|
|
73
|
+
next unless coerced.key?(name)
|
|
74
|
+
|
|
75
|
+
coerced[name] = coerce_value(coerced[name], param_options)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
coerced
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def coerce_value(value, param_options)
|
|
84
|
+
type = param_options[:type]
|
|
85
|
+
|
|
86
|
+
return coerce_union(value, param_options[:union]) if type == :union
|
|
87
|
+
return coerce_array(value, param_options) if type == :array && value.is_a?(Array)
|
|
88
|
+
return Coercer.coerce(param_options[:shape], value) if param_options[:shape] && value.is_a?(Hash)
|
|
89
|
+
|
|
90
|
+
if value.is_a?(Hash) && type && !PRIMITIVES.key?(type)
|
|
91
|
+
custom_shape = resolve_custom_shape(type)
|
|
92
|
+
return Coercer.coerce(custom_shape, value) if custom_shape
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
coerced = coerce_primitive(value, type)
|
|
96
|
+
coerced.nil? ? value : coerced
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def coerce_array(array, param_options)
|
|
100
|
+
custom_shape = nil
|
|
101
|
+
of = param_options[:of]
|
|
102
|
+
of_type = of&.type
|
|
103
|
+
of_shape = of&.shape
|
|
104
|
+
|
|
105
|
+
custom_shape = resolve_custom_shape(of_type) if of_type && !PRIMITIVES.key?(of_type)
|
|
106
|
+
|
|
107
|
+
array.map do |item|
|
|
108
|
+
if of_shape && item.is_a?(Hash)
|
|
109
|
+
Coercer.coerce(of_shape, item)
|
|
110
|
+
elsif of_type && PRIMITIVES.key?(of_type)
|
|
111
|
+
coerced = coerce_primitive(item, of_type)
|
|
112
|
+
coerced.nil? ? item : coerced
|
|
113
|
+
elsif of_type == :array && item.is_a?(Array) && of&.inner
|
|
114
|
+
coerce_array(item, { of: of.inner })
|
|
115
|
+
elsif custom_shape && item.is_a?(Hash)
|
|
116
|
+
Coercer.coerce(custom_shape, item)
|
|
117
|
+
else
|
|
118
|
+
item
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def coerce_union(value, union)
|
|
124
|
+
if union.variants.any? { |variant| variant[:type] == :boolean }
|
|
125
|
+
coerced = coerce_primitive(value, :boolean)
|
|
126
|
+
return coerced unless coerced.nil?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
discriminator = union.discriminator
|
|
130
|
+
|
|
131
|
+
if discriminator && value.is_a?(Hash)
|
|
132
|
+
discriminator_value = value[discriminator]
|
|
133
|
+
matching_variant = union.variants.find do |variant|
|
|
134
|
+
variant[:tag].to_s == discriminator_value.to_s
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if matching_variant
|
|
138
|
+
custom_shape = resolve_custom_shape(matching_variant[:type])
|
|
139
|
+
return Coercer.coerce(custom_shape, value) if custom_shape
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
union.variants.each do |variant|
|
|
144
|
+
variant_type = variant[:type]
|
|
145
|
+
variant_of = variant[:of]
|
|
146
|
+
variant_of_type = variant_of&.type
|
|
147
|
+
|
|
148
|
+
if variant_type == :array && value.is_a?(Array) && variant_of_type
|
|
149
|
+
custom_shape = resolve_custom_shape(variant_of_type)
|
|
150
|
+
if custom_shape
|
|
151
|
+
return value.map do |item|
|
|
152
|
+
item.is_a?(Hash) ? Coercer.coerce(custom_shape, item) : item
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
next if discriminator
|
|
158
|
+
|
|
159
|
+
custom_shape = resolve_custom_shape(variant_type)
|
|
160
|
+
next unless custom_shape
|
|
161
|
+
|
|
162
|
+
return Coercer.coerce(custom_shape, value) if value.is_a?(Hash)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
value
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def coerce_primitive(value, type)
|
|
169
|
+
return nil if value.nil?
|
|
170
|
+
|
|
171
|
+
coercer = PRIMITIVES[type]
|
|
172
|
+
return nil unless coercer
|
|
173
|
+
|
|
174
|
+
begin
|
|
175
|
+
coercer.call(value)
|
|
176
|
+
rescue ArgumentError, TypeError
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def resolve_custom_shape(type_name)
|
|
182
|
+
return @type_cache[type_name] if @type_cache.key?(type_name)
|
|
183
|
+
|
|
184
|
+
type_definition = @shape.contract_class.resolve_custom_type(type_name)
|
|
185
|
+
return @type_cache[type_name] = nil unless type_definition
|
|
186
|
+
|
|
187
|
+
@type_cache[type_name] = Object.new(@shape.contract_class).tap do |type_shape|
|
|
188
|
+
type_shape.copy_type_definition_params(type_definition, type_shape)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Contract
|
|
5
|
+
class Object
|
|
6
|
+
class Deserializer
|
|
7
|
+
class << self
|
|
8
|
+
def deserialize(shape, hash)
|
|
9
|
+
new(shape).deserialize(hash)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(shape)
|
|
14
|
+
@shape = shape
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def deserialize(hash)
|
|
18
|
+
deserialized = hash.dup
|
|
19
|
+
|
|
20
|
+
@shape.params.each do |name, param_options|
|
|
21
|
+
next unless deserialized.key?(name)
|
|
22
|
+
|
|
23
|
+
value = deserialized[name]
|
|
24
|
+
|
|
25
|
+
deserialized[name] = deserialize_value(value, param_options)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
deserialized
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def deserialize_value(value, param_options)
|
|
34
|
+
if param_options[:union] && value.is_a?(Hash)
|
|
35
|
+
deserialize_union(value, param_options[:union])
|
|
36
|
+
elsif param_options[:shape] && value.is_a?(Hash)
|
|
37
|
+
deserialize_shape(value, param_options[:shape])
|
|
38
|
+
elsif param_options[:type] == :array && value.is_a?(Array)
|
|
39
|
+
deserialize_array(value, param_options)
|
|
40
|
+
else
|
|
41
|
+
value
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def deserialize_shape(hash, nested_shape)
|
|
46
|
+
representation_class = nested_shape.contract_class.representation_class
|
|
47
|
+
return representation_class.deserialize(hash) if representation_class && nested_shape.params.none? { |_, options| options[:union] }
|
|
48
|
+
|
|
49
|
+
Deserializer.deserialize(nested_shape, hash)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def deserialize_union(hash, union)
|
|
53
|
+
variant = resolve_variant(hash, union)
|
|
54
|
+
return hash unless variant
|
|
55
|
+
|
|
56
|
+
if variant[:shape]
|
|
57
|
+
representation_class = variant[:shape].contract_class.representation_class
|
|
58
|
+
return representation_class.deserialize(hash) if representation_class
|
|
59
|
+
|
|
60
|
+
Deserializer.deserialize(variant[:shape], hash)
|
|
61
|
+
elsif variant[:custom_type]
|
|
62
|
+
deserialize_custom_type(hash, variant[:custom_type])
|
|
63
|
+
else
|
|
64
|
+
hash
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def resolve_variant(hash, union)
|
|
69
|
+
discriminator = union.discriminator
|
|
70
|
+
return union.variants.first unless discriminator
|
|
71
|
+
|
|
72
|
+
tag = hash[discriminator]
|
|
73
|
+
union.variants.find { |v| v[:tag].to_s == tag.to_s }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def deserialize_custom_type(hash, type_name)
|
|
77
|
+
type_definition = @shape.contract_class.resolve_custom_type(type_name)
|
|
78
|
+
return hash unless type_definition
|
|
79
|
+
|
|
80
|
+
representation_class = (type_definition.scope || @shape.contract_class).representation_class
|
|
81
|
+
|
|
82
|
+
return representation_class.deserialize(hash) if representation_class
|
|
83
|
+
|
|
84
|
+
hash
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def deserialize_array(array, param_options)
|
|
88
|
+
array.map do |item|
|
|
89
|
+
next item unless item.is_a?(Hash)
|
|
90
|
+
|
|
91
|
+
if param_options[:shape]
|
|
92
|
+
Deserializer.deserialize(param_options[:shape], item)
|
|
93
|
+
else
|
|
94
|
+
item
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Contract
|
|
5
|
+
class Object
|
|
6
|
+
class Transformer
|
|
7
|
+
class << self
|
|
8
|
+
def transform(shape, params)
|
|
9
|
+
new(shape).transform(params)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(shape)
|
|
14
|
+
@shape = shape
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def transform(params)
|
|
18
|
+
return params unless params.is_a?(Hash)
|
|
19
|
+
|
|
20
|
+
transformed = params.dup
|
|
21
|
+
|
|
22
|
+
@shape.params.each do |name, param_options|
|
|
23
|
+
next unless transformed.key?(name)
|
|
24
|
+
|
|
25
|
+
value = transformed[name]
|
|
26
|
+
|
|
27
|
+
if param_options[:as]
|
|
28
|
+
transformed[param_options[:as]] = transformed.delete(name)
|
|
29
|
+
name = param_options[:as]
|
|
30
|
+
value = transformed[name]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if param_options[:shape] && value.is_a?(Hash)
|
|
34
|
+
transformed[name] = Transformer.transform(param_options[:shape], value)
|
|
35
|
+
elsif param_options[:type] == :array && value.is_a?(Array)
|
|
36
|
+
of = param_options[:of]
|
|
37
|
+
of_shape = of&.shape
|
|
38
|
+
|
|
39
|
+
if of_shape
|
|
40
|
+
transformed[name] = value.map do |item|
|
|
41
|
+
item.is_a?(Hash) ? Transformer.transform(of_shape, item) : item
|
|
42
|
+
end
|
|
43
|
+
elsif of && (array_result = transform_custom_type_array(value, param_options))
|
|
44
|
+
transformed[name] = array_result
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
transformed
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def transform_custom_type_array(value, param_options)
|
|
55
|
+
of = param_options[:of]
|
|
56
|
+
type_name = of&.type
|
|
57
|
+
custom_type_shape = resolve_custom_type_shape(type_name)
|
|
58
|
+
return nil unless custom_type_shape
|
|
59
|
+
|
|
60
|
+
value.map do |item|
|
|
61
|
+
item.is_a?(Hash) ? Transformer.transform(custom_type_shape, item) : item
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def resolve_custom_type_shape(type_name)
|
|
66
|
+
contract_class = @shape.contract_class
|
|
67
|
+
type_definition = contract_class.resolve_custom_type(type_name)
|
|
68
|
+
return nil unless type_definition
|
|
69
|
+
|
|
70
|
+
scope_contract_class = type_definition.scope || contract_class
|
|
71
|
+
|
|
72
|
+
if type_definition.object?
|
|
73
|
+
build_type_shape(type_definition, scope_contract_class)
|
|
74
|
+
elsif type_definition.union?
|
|
75
|
+
first_variant = type_definition.variants.first
|
|
76
|
+
return nil unless first_variant
|
|
77
|
+
|
|
78
|
+
variant_type_definition = scope_contract_class.resolve_custom_type(first_variant[:type])
|
|
79
|
+
return nil unless variant_type_definition
|
|
80
|
+
return nil unless variant_type_definition.object?
|
|
81
|
+
|
|
82
|
+
variant_contract_class = variant_type_definition.scope || scope_contract_class
|
|
83
|
+
build_type_shape(variant_type_definition, variant_contract_class)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build_type_shape(type_definition, contract_class)
|
|
88
|
+
type_shape = Object.new(contract_class, action_name: @shape.action_name)
|
|
89
|
+
type_shape.copy_type_definition_params(type_definition, type_shape)
|
|
90
|
+
type_shape
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Contract
|
|
5
|
+
class Object
|
|
6
|
+
class Validator
|
|
7
|
+
class Result
|
|
8
|
+
attr_reader :issues,
|
|
9
|
+
:params
|
|
10
|
+
|
|
11
|
+
def initialize(issues: [], params:)
|
|
12
|
+
@issues = issues
|
|
13
|
+
@params = params
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def valid?
|
|
17
|
+
issues.empty?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def invalid?
|
|
21
|
+
issues.any?
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|