castkit 0.1.2 → 0.3.0
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/.rspec_status +195 -219
- data/CHANGELOG.md +42 -0
- data/README.md +744 -83
- data/castkit.gemspec +1 -0
- data/lib/castkit/attribute.rb +6 -24
- data/lib/castkit/castkit.rb +61 -10
- data/lib/castkit/cli/generate.rb +98 -0
- data/lib/castkit/cli/list.rb +200 -0
- data/lib/castkit/cli/main.rb +43 -0
- data/lib/castkit/cli.rb +24 -0
- data/lib/castkit/configuration.rb +116 -46
- data/lib/castkit/contract/base.rb +168 -0
- data/lib/castkit/contract/data_object.rb +62 -0
- data/lib/castkit/contract/result.rb +74 -0
- data/lib/castkit/contract/validator.rb +248 -0
- data/lib/castkit/contract.rb +67 -0
- data/lib/castkit/{data_object_extensions → core}/attribute_types.rb +21 -7
- data/lib/castkit/{data_object_extensions → core}/attributes.rb +8 -3
- data/lib/castkit/core/config.rb +74 -0
- data/lib/castkit/core/registerable.rb +59 -0
- data/lib/castkit/data_object.rb +56 -67
- data/lib/castkit/error.rb +15 -3
- data/lib/castkit/ext/attribute/access.rb +67 -0
- data/lib/castkit/ext/attribute/error_handling.rb +63 -0
- data/lib/castkit/ext/attribute/options.rb +142 -0
- data/lib/castkit/ext/attribute/validation.rb +85 -0
- data/lib/castkit/ext/data_object/contract.rb +96 -0
- data/lib/castkit/ext/data_object/deserialization.rb +167 -0
- data/lib/castkit/ext/data_object/plugins.rb +86 -0
- data/lib/castkit/ext/data_object/serialization.rb +61 -0
- data/lib/castkit/inflector.rb +47 -0
- data/lib/castkit/plugins.rb +82 -0
- data/lib/castkit/serializers/base.rb +94 -0
- data/lib/castkit/serializers/default_serializer.rb +156 -0
- data/lib/castkit/types/base.rb +122 -0
- data/lib/castkit/types/boolean.rb +47 -0
- data/lib/castkit/types/collection.rb +35 -0
- data/lib/castkit/types/date.rb +34 -0
- data/lib/castkit/types/date_time.rb +34 -0
- data/lib/castkit/types/float.rb +46 -0
- data/lib/castkit/types/integer.rb +46 -0
- data/lib/castkit/types/string.rb +44 -0
- data/lib/castkit/types.rb +15 -0
- data/lib/castkit/validators/base.rb +59 -0
- data/lib/castkit/validators/boolean_validator.rb +39 -0
- data/lib/castkit/validators/collection_validator.rb +29 -0
- data/lib/castkit/validators/float_validator.rb +31 -0
- data/lib/castkit/validators/integer_validator.rb +31 -0
- data/lib/castkit/validators/numeric_validator.rb +2 -2
- data/lib/castkit/validators/string_validator.rb +3 -4
- data/lib/castkit/version.rb +1 -1
- data/lib/castkit.rb +2 -0
- data/lib/generators/base.rb +97 -0
- data/lib/generators/contract.rb +68 -0
- data/lib/generators/data_object.rb +48 -0
- data/lib/generators/plugin.rb +25 -0
- data/lib/generators/serializer.rb +28 -0
- data/lib/generators/templates/contract.rb.tt +24 -0
- data/lib/generators/templates/contract_spec.rb.tt +76 -0
- data/lib/generators/templates/data_object.rb.tt +15 -0
- data/lib/generators/templates/data_object_spec.rb.tt +36 -0
- data/lib/generators/templates/plugin.rb.tt +37 -0
- data/lib/generators/templates/plugin_spec.rb.tt +18 -0
- data/lib/generators/templates/serializer.rb.tt +24 -0
- data/lib/generators/templates/serializer_spec.rb.tt +14 -0
- data/lib/generators/templates/type.rb.tt +55 -0
- data/lib/generators/templates/type_spec.rb.tt +42 -0
- data/lib/generators/templates/validator.rb.tt +26 -0
- data/lib/generators/templates/validator_spec.rb.tt +23 -0
- data/lib/generators/type.rb +29 -0
- data/lib/generators/validator.rb +41 -0
- metadata +74 -15
- data/lib/castkit/attribute_extensions/access.rb +0 -65
- data/lib/castkit/attribute_extensions/casting.rb +0 -147
- data/lib/castkit/attribute_extensions/error_handling.rb +0 -83
- data/lib/castkit/attribute_extensions/options.rb +0 -131
- data/lib/castkit/attribute_extensions/serialization.rb +0 -89
- data/lib/castkit/attribute_extensions/validation.rb +0 -72
- data/lib/castkit/data_object_extensions/config.rb +0 -113
- data/lib/castkit/data_object_extensions/deserialization.rb +0 -110
- data/lib/castkit/default_serializer.rb +0 -123
- data/lib/castkit/serializer.rb +0 -92
- data/lib/castkit/validators.rb +0 -4
@@ -1,39 +1,53 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "
|
3
|
+
require_relative "types"
|
4
4
|
|
5
5
|
module Castkit
|
6
6
|
# Configuration container for global Castkit settings.
|
7
7
|
#
|
8
|
-
# This includes
|
8
|
+
# This includes type registration, validation, and enforcement flags
|
9
|
+
# used throughout Castkit's attribute system.
|
9
10
|
class Configuration
|
10
|
-
# Default mapping of primitive
|
11
|
+
# Default mapping of primitive type definitions.
|
11
12
|
#
|
12
|
-
# @return [Hash
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
# @return [Hash{Symbol => Castkit::Types::Base}]
|
14
|
+
DEFAULT_TYPES = {
|
15
|
+
array: Castkit::Types::Collection.new,
|
16
|
+
boolean: Castkit::Types::Boolean.new,
|
17
|
+
date: Castkit::Types::Date.new,
|
18
|
+
datetime: Castkit::Types::DateTime.new,
|
19
|
+
float: Castkit::Types::Float.new,
|
20
|
+
hash: Castkit::Types::Base.new,
|
21
|
+
integer: Castkit::Types::Integer.new,
|
22
|
+
string: Castkit::Types::String.new
|
17
23
|
}.freeze
|
18
24
|
|
19
|
-
#
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
+
# Type aliases for primitive type definitions.
|
26
|
+
#
|
27
|
+
# @return [Hash{Symbol => Symbol}]
|
28
|
+
TYPE_ALIASES = {
|
29
|
+
collection: :array,
|
30
|
+
bool: :boolean,
|
31
|
+
int: :integer,
|
32
|
+
map: :hash,
|
33
|
+
number: :float,
|
34
|
+
str: :string,
|
35
|
+
timestamp: :datetime,
|
36
|
+
uuid: :string
|
37
|
+
}.freeze
|
25
38
|
|
26
|
-
#
|
27
|
-
|
28
|
-
attr_accessor :enforce_known_primitive_type
|
39
|
+
# @return [Hash{Symbol => Castkit::Types::Base}] registered types
|
40
|
+
attr_reader :types
|
29
41
|
|
30
|
-
#
|
31
|
-
#
|
32
|
-
|
42
|
+
# Set default plugins that will be used globally in all Castkit::DataObject subclasses.
|
43
|
+
# This is equivalent to calling `enable_plugins` in every class.
|
44
|
+
#
|
45
|
+
# @return [Array<Symbol>] default plugin names to be applied to all DataObject subclasses
|
46
|
+
attr_accessor :default_plugins
|
33
47
|
|
34
|
-
# Whether to raise an error if
|
48
|
+
# Whether to raise an error if values should be validated before deserializing, e.g. true -> "true"
|
35
49
|
# @return [Boolean]
|
36
|
-
attr_accessor :
|
50
|
+
attr_accessor :enforce_typing
|
37
51
|
|
38
52
|
# Whether to raise an error if access mode is not recognized.
|
39
53
|
# @return [Boolean]
|
@@ -47,55 +61,111 @@ module Castkit
|
|
47
61
|
# @return [Boolean]
|
48
62
|
attr_accessor :enforce_array_options
|
49
63
|
|
50
|
-
# Whether to
|
64
|
+
# Whether to raise an error for unknown and invalid type definitions.
|
65
|
+
# @return [Boolean]
|
66
|
+
attr_accessor :raise_type_errors
|
67
|
+
|
68
|
+
# Whether to emit warnings when Castkit detects misconfigurations.
|
51
69
|
# @return [Boolean]
|
52
70
|
attr_accessor :enable_warnings
|
53
71
|
|
54
|
-
#
|
72
|
+
# Whether the strict flag is enabled by default for all DataObjects and Contracts.
|
73
|
+
# @return [Boolean]
|
74
|
+
attr_accessor :strict_by_default
|
75
|
+
|
76
|
+
# Initializes the configuration with default types and enforcement flags.
|
55
77
|
#
|
56
78
|
# @return [void]
|
57
79
|
def initialize
|
58
|
-
@
|
59
|
-
@
|
60
|
-
@enforce_known_primitive_type = true
|
61
|
-
@enforce_boolean_casting = true
|
62
|
-
@enforce_union_match = true
|
80
|
+
@types = DEFAULT_TYPES.dup
|
81
|
+
@enforce_typing = true
|
63
82
|
@enforce_attribute_access = true
|
64
83
|
@enforce_unwrapped_prefix = true
|
65
84
|
@enforce_array_options = true
|
85
|
+
@raise_type_errors = true
|
66
86
|
@enable_warnings = true
|
87
|
+
@strict_by_default = true
|
88
|
+
@default_plugins = []
|
89
|
+
|
90
|
+
apply_type_aliases!
|
67
91
|
end
|
68
92
|
|
69
|
-
# Registers a
|
93
|
+
# Registers a new type definition.
|
70
94
|
#
|
71
|
-
# @param type [Symbol] the type
|
72
|
-
# @param
|
73
|
-
# @param override [Boolean] whether to
|
74
|
-
# @raise [Castkit::
|
95
|
+
# @param type [Symbol] the symbolic type name (e.g., :uuid)
|
96
|
+
# @param klass [Class<Castkit::Types::Base>] the class to register
|
97
|
+
# @param override [Boolean] whether to allow overwriting existing registration
|
98
|
+
# @raise [Castkit::TypeError] if the type class is invalid or not a subclass of Castkit::Types::Base
|
75
99
|
# @return [void]
|
76
|
-
def
|
77
|
-
|
100
|
+
def register_type(type, klass, aliases: [], override: false)
|
101
|
+
type = type.to_sym
|
102
|
+
return if types.key?(type) && !override
|
78
103
|
|
79
|
-
|
80
|
-
|
104
|
+
instance = klass.new
|
105
|
+
unless instance.is_a?(Castkit::Types::Base)
|
106
|
+
raise Castkit::TypeError, "Expected subclass of Castkit::Types::Base for `#{type}`"
|
81
107
|
end
|
82
108
|
|
83
|
-
|
109
|
+
types[type] = instance
|
110
|
+
|
111
|
+
Castkit::Core::AttributeTypes.define_type_dsl(type) if Castkit::Core::AttributeTypes.respond_to?(:define_type_dsl)
|
112
|
+
return unless aliases.any?
|
113
|
+
|
114
|
+
aliases.each { |alias_type| register_type(alias_type, klass, override: override) }
|
115
|
+
end
|
116
|
+
|
117
|
+
# Register a custom plugin for use with Castkit::DataObject.
|
118
|
+
#
|
119
|
+
# @example Loading as a default plugin
|
120
|
+
# Castkit.configure do |config|
|
121
|
+
# config.register_plugin(:custom, CustomPlugin)
|
122
|
+
# config.default_plugins [:custom]
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# @example Loading it directly in a Castkit::DataObject
|
126
|
+
# class UserDto < Castkit::DataObject
|
127
|
+
# enable_plugins :custom
|
128
|
+
# end
|
129
|
+
def register_plugin(name, plugin)
|
130
|
+
Castkit::Plugins.register(name, plugin)
|
84
131
|
end
|
85
132
|
|
86
|
-
# Returns the
|
133
|
+
# Returns the type handler for a given type symbol.
|
87
134
|
#
|
88
135
|
# @param type [Symbol]
|
89
|
-
# @return [
|
90
|
-
|
91
|
-
|
136
|
+
# @return [Castkit::Types::Base]
|
137
|
+
# @raise [Castkit::TypeError] if the type is not registered
|
138
|
+
def fetch_type(type)
|
139
|
+
@types.fetch(type.to_sym) do
|
140
|
+
raise Castkit::TypeError, "Unknown type `#{type.inspect}`" if raise_type_errors
|
141
|
+
end
|
92
142
|
end
|
93
143
|
|
94
|
-
#
|
144
|
+
# Returns whether a type is currently registered.
|
145
|
+
#
|
146
|
+
# @param type [Symbol]
|
147
|
+
# @return [Boolean]
|
148
|
+
def type_registered?(type)
|
149
|
+
@types.key?(type.to_sym)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Restores the type registry to its default state.
|
95
153
|
#
|
96
154
|
# @return [void]
|
97
|
-
def
|
98
|
-
@
|
155
|
+
def reset_types!
|
156
|
+
@types = DEFAULT_TYPES.dup
|
157
|
+
apply_type_aliases!
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
# Registers aliases for primitive type definitions.
|
163
|
+
#
|
164
|
+
# @return [void]
|
165
|
+
def apply_type_aliases!
|
166
|
+
TYPE_ALIASES.each do |alias_key, canonical|
|
167
|
+
register_type(alias_key, DEFAULT_TYPES[canonical].class)
|
168
|
+
end
|
99
169
|
end
|
100
170
|
end
|
101
171
|
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../core/config"
|
4
|
+
require_relative "../core/attribute_types"
|
5
|
+
require_relative "../core/registerable"
|
6
|
+
require_relative "result"
|
7
|
+
|
8
|
+
module Castkit
|
9
|
+
module Contract
|
10
|
+
# Base class for all Castkit contracts.
|
11
|
+
#
|
12
|
+
# Castkit contracts define validation logic over a set of attributes using a DSL.
|
13
|
+
# You can either subclass this directly or use {Castkit::Contract.build} to generate
|
14
|
+
# ephemeral or reusable contract classes.
|
15
|
+
#
|
16
|
+
# @example Subclassing directly
|
17
|
+
# class MyContract < Castkit::Contract::Base
|
18
|
+
# string :id
|
19
|
+
# integer :count, required: false
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# MyContract.validate!(id: "abc")
|
23
|
+
#
|
24
|
+
# @example Using Contract.build (preferred for dynamic generation)
|
25
|
+
# UserContract = Castkit::Contract.build(:user) do
|
26
|
+
# string :id
|
27
|
+
# string :email, required: false
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# UserContract.validate!(id: "123")
|
31
|
+
#
|
32
|
+
# @see Castkit::Contract.build
|
33
|
+
class Base
|
34
|
+
extend Castkit::Core::Config
|
35
|
+
extend Castkit::Core::AttributeTypes
|
36
|
+
extend Castkit::Core::Registerable
|
37
|
+
|
38
|
+
ATTRIBUTE_OPTIONS = %i[
|
39
|
+
required aliases min max format of validator unwrapped prefix force_type
|
40
|
+
].freeze
|
41
|
+
|
42
|
+
class << self
|
43
|
+
# Registers the current class under `Castkit::Contracts`.
|
44
|
+
#
|
45
|
+
# @param as [String, Symbol, nil] The constant name to use (PascalCase). Defaults to the name used when building
|
46
|
+
# the contract. If no name was provided, an error is raised.
|
47
|
+
# @return [Class] the registered contract class
|
48
|
+
# @raise [Castkit::Error] If a name cannot be resolved.
|
49
|
+
def register!(as: nil)
|
50
|
+
super(namespace: :contracts, as: as || definition[:name])
|
51
|
+
end
|
52
|
+
|
53
|
+
# Defines an attribute for the contract.
|
54
|
+
#
|
55
|
+
# Only a subset of options is allowed inside a contract.
|
56
|
+
#
|
57
|
+
# @param field [Symbol] the field name
|
58
|
+
# @param type [Symbol, Class, Array] the type or union of types
|
59
|
+
# @param options [Hash] allowed options like :required or :validator
|
60
|
+
# @return [void]
|
61
|
+
def attribute(field, type, **options)
|
62
|
+
options = options.slice(*ATTRIBUTE_OPTIONS)
|
63
|
+
attributes[field] = Castkit::Attribute.new(field, type, **options)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Validates input against the contract and returns a Result.
|
67
|
+
#
|
68
|
+
# @param input [Hash]
|
69
|
+
# @return [Castkit::Contract::Result]
|
70
|
+
def validate(input)
|
71
|
+
validate!(input)
|
72
|
+
rescue Castkit::ContractError => e
|
73
|
+
Castkit::Contract::Result.new(definition[:name].to_s, input, errors: e.errors)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Validates input and raises on failure.
|
77
|
+
#
|
78
|
+
# @param input [Hash]
|
79
|
+
# @raise [Castkit::ContractError]
|
80
|
+
# @return [void]
|
81
|
+
def validate!(input)
|
82
|
+
Castkit::Contract::Validator.call!(attributes.values, input, **validation_rules)
|
83
|
+
Castkit::Contract::Result.new(definition[:name].to_s, input)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns internal contract metadata.
|
87
|
+
#
|
88
|
+
# @return [Hash]
|
89
|
+
def definition
|
90
|
+
@definition ||= {
|
91
|
+
name: :ephemeral,
|
92
|
+
attributes: {}
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns the defined attributes.
|
97
|
+
#
|
98
|
+
# @return [Hash{Symbol => Castkit::Attribute}]
|
99
|
+
def attributes
|
100
|
+
definition[:attributes]
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
# Defines the contract from a source or block.
|
106
|
+
#
|
107
|
+
# @param name [Symbol, String]
|
108
|
+
# @param source [Castkit::DataObject, nil]
|
109
|
+
# @param block [Proc, nil]
|
110
|
+
# @return [Hash]
|
111
|
+
def define(name = :ephemeral, source = nil, validation_rules: {}, &block)
|
112
|
+
validate_definition!(source, &block)
|
113
|
+
|
114
|
+
if source
|
115
|
+
define_from_source(name, source)
|
116
|
+
else
|
117
|
+
define_from_block(name, &block)
|
118
|
+
end
|
119
|
+
|
120
|
+
validation_rules.each { |k, v| self.validation_rules[k] = v }
|
121
|
+
attributes
|
122
|
+
end
|
123
|
+
|
124
|
+
# Copies attributes from a DataObject.
|
125
|
+
#
|
126
|
+
# @param name [Symbol, String]
|
127
|
+
# @param source [Castkit::DataObject]
|
128
|
+
# @return [void]
|
129
|
+
def define_from_source(name, source)
|
130
|
+
source_attributes = source.attributes.dup
|
131
|
+
|
132
|
+
@definition = {
|
133
|
+
name: name,
|
134
|
+
attributes: source_attributes.transform_values do |attr|
|
135
|
+
Castkit::Attribute.new(attr.field, attr.type, **attr.options.slice(*ATTRIBUTE_OPTIONS))
|
136
|
+
end
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
140
|
+
# Executes DSL block in the contract context.
|
141
|
+
#
|
142
|
+
# @param name [Symbol, String]
|
143
|
+
# @yield [block]
|
144
|
+
# @return [void]
|
145
|
+
def define_from_block(name, &block)
|
146
|
+
definition[:name] = name
|
147
|
+
|
148
|
+
@__castkit_contract_dsl = true
|
149
|
+
instance_eval(&block)
|
150
|
+
ensure
|
151
|
+
@__castkit_contract_dsl = false
|
152
|
+
end
|
153
|
+
|
154
|
+
# Ensures a valid contract definition input.
|
155
|
+
#
|
156
|
+
# @param source [Object, nil]
|
157
|
+
# @raise [Castkit::ContractError]
|
158
|
+
# @return [void]
|
159
|
+
def validate_definition!(source)
|
160
|
+
raise Castkit::ContractError, "Received both source and block" if source && block_given?
|
161
|
+
return if block_given? || Castkit.dataobject?(source)
|
162
|
+
|
163
|
+
raise Castkit::ContractError, "Expected a Castkit::DataObject or contract block"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castkit
|
4
|
+
module Contract
|
5
|
+
# Extension module that adds `.to_dataobject` and `.dataobject` support to Castkit contracts.
|
6
|
+
#
|
7
|
+
# This allows any contract to be dynamically converted into a Castkit::DataObject class,
|
8
|
+
# enabling reuse of validation schemas for serialization, coercion, or API response modeling.
|
9
|
+
#
|
10
|
+
# This module is automatically included by Castkit contract classes and is not
|
11
|
+
# intended to be used manually.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# contract = Castkit::Contract.build(:user) do
|
15
|
+
# string :id
|
16
|
+
# string :email
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# UserDto = contract.to_dataobject
|
20
|
+
# UserDto.new(id: "123", email: "a@example.com")
|
21
|
+
module DataObject
|
22
|
+
# Returns or builds a Castkit::DataObject from the current contract.
|
23
|
+
#
|
24
|
+
# Memoizes the result to avoid repeated regeneration.
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# contract = Castkit::Contract.build(:user) do
|
28
|
+
# string :id
|
29
|
+
# string :name
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# dto_class = contract.dataobject
|
33
|
+
# dto = dto_class.new(id: "123", name: "Alice")
|
34
|
+
#
|
35
|
+
# @return [Class<Castkit::DataObject>] the generated DTO class
|
36
|
+
def dataobject
|
37
|
+
@dataobject ||= to_dataobject
|
38
|
+
end
|
39
|
+
|
40
|
+
# Constructs an ephemeral Castkit::DataObject class from the current contract.
|
41
|
+
#
|
42
|
+
# This creates a new anonymous class each time unless memoized via {#dataobject}.
|
43
|
+
#
|
44
|
+
# @example
|
45
|
+
# dto_class = contract.to_dataobject
|
46
|
+
#
|
47
|
+
# @return [Class<Castkit::DataObject>] the dynamically generated DTO
|
48
|
+
def to_dataobject
|
49
|
+
Class.new(Castkit::DataObject).tap do |klass|
|
50
|
+
attributes.each_value do |attr|
|
51
|
+
klass.attribute(attr.field, attr.type, **attr.options)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Alias for {#to_dataobject}
|
57
|
+
#
|
58
|
+
# @see #to_dataobject
|
59
|
+
alias to_dto to_dataobject
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castkit
|
4
|
+
module Contract
|
5
|
+
# Represents the result of a contract validation.
|
6
|
+
#
|
7
|
+
# Provides access to the validation outcome, including whether it succeeded or failed,
|
8
|
+
# and includes the full list of errors if any.
|
9
|
+
class Result
|
10
|
+
# @return [Symbol] the name of the contract
|
11
|
+
attr_reader :contract
|
12
|
+
|
13
|
+
# @return [Hash{Symbol => Object}] the validated input
|
14
|
+
attr_reader :input
|
15
|
+
|
16
|
+
# @return [Hash{Symbol => Object}] the validation error hash
|
17
|
+
attr_reader :errors
|
18
|
+
|
19
|
+
# Initializes a new result object.
|
20
|
+
#
|
21
|
+
# @param contract [Symbol, String] the name of the contract
|
22
|
+
# @param input [Hash{Symbol => Object}] the validated input
|
23
|
+
# @param errors [Hash{Symbol => Object}] the validation errors
|
24
|
+
def initialize(contract, input, errors: {})
|
25
|
+
@contract = contract.to_sym.freeze
|
26
|
+
@input = input.freeze
|
27
|
+
@errors = errors.freeze
|
28
|
+
end
|
29
|
+
|
30
|
+
# A debug-friendly representation of the validation result.
|
31
|
+
#
|
32
|
+
# @return [String]
|
33
|
+
def inspect
|
34
|
+
"#<#{self.class.name} contract=#{contract.inspect} success=#{success?} errors=#{errors.inspect}>"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Whether the validation passed with no errors.
|
38
|
+
#
|
39
|
+
# @return [Boolean]
|
40
|
+
def success?
|
41
|
+
errors.empty?
|
42
|
+
end
|
43
|
+
|
44
|
+
# Whether the validation failed with one or more errors.
|
45
|
+
#
|
46
|
+
# @return [Boolean]
|
47
|
+
def failure?
|
48
|
+
!success?
|
49
|
+
end
|
50
|
+
|
51
|
+
# A readable string representation of the validation result.
|
52
|
+
#
|
53
|
+
# @return [String]
|
54
|
+
def to_s
|
55
|
+
return "[Castkit] Contract validation passed for #{contract}" if success?
|
56
|
+
|
57
|
+
parsed_errors = errors.map { |k, v| " #{k}: #{v.inspect}" }.join("\n")
|
58
|
+
"[Castkit] Contract validation failed for #{contract}:\n#{parsed_errors}"
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [Hash{Symbol => Object}] the input validation and error hash
|
62
|
+
def to_hash
|
63
|
+
@to_hash ||= {
|
64
|
+
contract: contract,
|
65
|
+
input: input,
|
66
|
+
errors: errors
|
67
|
+
}.freeze
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Hash{Symbol => Object}] the input and validation error hash
|
71
|
+
alias to_h to_hash
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|