castkit 0.1.2 → 0.2.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec_status +196 -219
  3. data/CHANGELOG.md +42 -0
  4. data/README.md +469 -84
  5. data/lib/castkit/attribute.rb +6 -24
  6. data/lib/castkit/castkit.rb +58 -10
  7. data/lib/castkit/configuration.rb +94 -47
  8. data/lib/castkit/contract/data_object.rb +62 -0
  9. data/lib/castkit/contract/generic.rb +168 -0
  10. data/lib/castkit/contract/result.rb +74 -0
  11. data/lib/castkit/contract/validator.rb +248 -0
  12. data/lib/castkit/contract.rb +67 -0
  13. data/lib/castkit/{data_object_extensions → core}/attribute_types.rb +21 -7
  14. data/lib/castkit/{data_object_extensions → core}/attributes.rb +8 -3
  15. data/lib/castkit/core/config.rb +74 -0
  16. data/lib/castkit/core/registerable.rb +59 -0
  17. data/lib/castkit/data_object.rb +45 -60
  18. data/lib/castkit/default_serializer.rb +85 -54
  19. data/lib/castkit/error.rb +15 -3
  20. data/lib/castkit/ext/attribute/access.rb +67 -0
  21. data/lib/castkit/ext/attribute/error_handling.rb +63 -0
  22. data/lib/castkit/ext/attribute/options.rb +142 -0
  23. data/lib/castkit/ext/attribute/validation.rb +85 -0
  24. data/lib/castkit/ext/data_object/contract.rb +96 -0
  25. data/lib/castkit/ext/data_object/deserialization.rb +167 -0
  26. data/lib/castkit/ext/data_object/serialization.rb +61 -0
  27. data/lib/castkit/inflector.rb +47 -0
  28. data/lib/castkit/types/boolean.rb +43 -0
  29. data/lib/castkit/types/collection.rb +24 -0
  30. data/lib/castkit/types/date.rb +34 -0
  31. data/lib/castkit/types/date_time.rb +34 -0
  32. data/lib/castkit/types/float.rb +46 -0
  33. data/lib/castkit/types/generic.rb +123 -0
  34. data/lib/castkit/types/integer.rb +46 -0
  35. data/lib/castkit/types/string.rb +44 -0
  36. data/lib/castkit/types.rb +15 -0
  37. data/lib/castkit/validators/base_validator.rb +39 -0
  38. data/lib/castkit/validators/numeric_validator.rb +2 -2
  39. data/lib/castkit/validators/string_validator.rb +3 -3
  40. data/lib/castkit/version.rb +1 -1
  41. data/lib/castkit.rb +2 -0
  42. metadata +29 -13
  43. data/lib/castkit/attribute_extensions/access.rb +0 -65
  44. data/lib/castkit/attribute_extensions/casting.rb +0 -147
  45. data/lib/castkit/attribute_extensions/error_handling.rb +0 -83
  46. data/lib/castkit/attribute_extensions/options.rb +0 -131
  47. data/lib/castkit/attribute_extensions/serialization.rb +0 -89
  48. data/lib/castkit/attribute_extensions/validation.rb +0 -72
  49. data/lib/castkit/data_object_extensions/config.rb +0 -113
  50. data/lib/castkit/data_object_extensions/deserialization.rb +0 -110
  51. data/lib/castkit/validators.rb +0 -4
@@ -1,23 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "error"
4
- require_relative "data_object"
5
- require_relative "attribute_extensions/options"
6
- require_relative "attribute_extensions/casting"
7
- require_relative "attribute_extensions/access"
8
- require_relative "attribute_extensions/validation"
9
- require_relative "attribute_extensions/serialization"
4
+ require_relative "ext/attribute/options"
5
+ require_relative "ext/attribute/access"
6
+ require_relative "ext/attribute/validation"
10
7
 
11
8
  module Castkit
12
9
  # Represents a typed attribute on a Castkit::DataObject.
13
10
  #
14
11
  # Provides casting, validation, access control, and serialization behavior.
15
12
  class Attribute
16
- include Castkit::AttributeExtensions::Options
17
- include Castkit::AttributeExtensions::Casting
18
- include Castkit::AttributeExtensions::Access
19
- include Castkit::AttributeExtensions::Validation
20
- include Castkit::AttributeExtensions::Serialization
13
+ include Castkit::Ext::Attribute::Options
14
+ include Castkit::Ext::Attribute::Access
15
+ include Castkit::Ext::Attribute::Validation
21
16
 
22
17
  # @return [Symbol] the attribute name
23
18
  attr_reader :field
@@ -106,19 +101,6 @@ module Castkit
106
101
  end
107
102
  end
108
103
 
109
- # Validates the final value against a validator if required.
110
- #
111
- # @param value [Object]
112
- # @param context [Symbol, String]
113
- # @return [void]
114
- def validate_value!(value, context:)
115
- return if value.nil? && optional?
116
- return if type.is_a?(Array) || dataobject?
117
-
118
- validator = options[:validator] || Castkit.configuration.validator_for(type)
119
- validator&.call(value, options: options, context: context)
120
- end
121
-
122
104
  # Raises a Castkit::AttributeError with optional context.
123
105
  #
124
106
  # @param message [String]
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "configuration"
4
+ require_relative "inflector"
4
5
 
5
6
  # Castkit is a lightweight, type-safe data object system for Ruby.
6
7
  #
@@ -15,42 +16,89 @@ require_relative "configuration"
15
16
  #
16
17
  # user = UserDto.new(name: "Alice", age: 30)
17
18
  # user.to_h #=> { name: "Alice", age: 30 }
19
+ #
20
+ # @see Castkit::Contract
21
+ # @see Castkit::DataObject
18
22
  module Castkit
23
+ # Namespace used for registering DataObjects generated from contracts.
24
+ module DataObjects; end
25
+
26
+ # Namespace used for registering contracts generated from DataObjects.
27
+ module Contracts; end
28
+
19
29
  class << self
20
30
  # Yields the global configuration object for customization.
21
31
  #
22
- # @example
32
+ # @example Disabling array enforcement
23
33
  # Castkit.configure do |config|
24
- # config.enforce_boolean_casting = false
34
+ # config.enforce_typing = false
25
35
  # end
26
36
  #
27
- # @yieldparam config [Castkit::Configuration]
37
+ # @yieldparam config [Castkit::Configuration] the mutable config object
28
38
  # @return [void]
29
39
  def configure
30
40
  yield(configuration)
31
41
  end
32
42
 
33
- # Retrieves the global Castkit configuration.
43
+ # Retrieves the global Castkit configuration instance.
34
44
  #
35
- # @return [Castkit::Configuration] the configuration instance
45
+ # @return [Castkit::Configuration] the configuration object
36
46
  def configuration
37
47
  @configuration ||= Configuration.new
38
48
  end
39
49
 
40
- # Generates a warning message if configuration.enable_warnings == true.
50
+ # Emits a warning to STDERR if `enable_warnings` is enabled in config.
41
51
  #
42
- # @param message [String] The warning message
52
+ # @param message [String] the warning message
43
53
  # @return [void]
44
54
  def warning(message)
45
55
  warn message if configuration.enable_warnings
46
56
  end
47
57
 
48
- # Determine if an object is a subclass of Castkit::DataObject.
58
+ # Checks whether a given object is a subclass of Castkit::DataObject.
49
59
  #
50
- # @param obj [Object] The object to check
51
- # @return [Boolean]
60
+ # @param obj [Object] the object to test
61
+ # @return [Boolean] true if obj is a Castkit::DataObject class
52
62
  def dataobject?(obj)
53
63
  obj.is_a?(Class) && obj.ancestors.include?(Castkit::DataObject)
54
64
  end
65
+
66
+ # Returns a type caster lambda for the given type.
67
+ #
68
+ # Type casting performs both validation and deserialization on the provided value.
69
+ #
70
+ # @param type [Symbol] the registered type (e.g. :string)
71
+ # @return [Proc] a lambda that accepts a value and options and returns a casted result
72
+ def type_caster(type)
73
+ type_definition = configuration.fetch_type(type)
74
+
75
+ lambda do |value, validator: nil, options: {}, context: nil|
76
+ type_definition.class.cast!(value, validator: validator, options: options, context: context)
77
+ end
78
+ end
79
+
80
+ # Returns a serializer lambda for the given type.
81
+ #
82
+ # @param type [Symbol] the registered type (e.g. :string)
83
+ # @return [Proc] a lambda that calls `.serialize` on the type
84
+ def type_serializer(type)
85
+ ->(value) { configuration.fetch_type(type).serialize(value) }
86
+ end
87
+
88
+ # Returns a deserializer lambda for the given type.
89
+ #
90
+ # @param type [Symbol] the registered type (e.g. :string)
91
+ # @return [Proc] a lambda that calls `.deserialize` on the type
92
+ def type_deserializer(type)
93
+ ->(value) { configuration.fetch_type(type).deserialize(value) }
94
+ end
95
+
96
+ # Returns a validator lambda for the given type.
97
+ #
98
+ # @param type [Symbol] the registered type (e.g. :string)
99
+ # @return [Proc] a lambda that calls `.validate!` on the type
100
+ def type_validator(type)
101
+ ->(value) { configuration.fetch_type(type).validate!(value) }
102
+ end
55
103
  end
56
104
  end
@@ -1,39 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "validators"
3
+ require_relative "types"
4
4
 
5
5
  module Castkit
6
6
  # Configuration container for global Castkit settings.
7
7
  #
8
- # This includes validator registration and enforcement flags for various runtime checks.
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 types to validators.
11
+ # Default mapping of primitive type definitions.
11
12
  #
12
- # @return [Hash<Symbol, Class>]
13
- DEFAULT_VALIDATORS = {
14
- string: Castkit::Validators::StringValidator,
15
- integer: Castkit::Validators::NumericValidator,
16
- float: Castkit::Validators::NumericValidator
13
+ # @return [Hash{Symbol => Castkit::Types::Generic}]
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::Generic.new,
21
+ integer: Castkit::Types::Integer.new,
22
+ string: Castkit::Types::String.new
17
23
  }.freeze
18
24
 
19
- # @return [Hash<Symbol, #call>] registered validators by type
20
- attr_reader :validators
21
-
22
- # Whether to raise an error if `of:` is missing for array types.
23
- # @return [Boolean]
24
- attr_accessor :enforce_array_of_type
25
-
26
- # Whether to raise an error for unrecognized primitive types.
27
- # @return [Boolean]
28
- attr_accessor :enforce_known_primitive_type
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
29
38
 
30
- # Whether to raise an error on invalid boolean coercion.
31
- # @return [Boolean]
32
- attr_accessor :enforce_boolean_casting
39
+ # @return [Hash{Symbol => Castkit::Types::Generic}] registered types
40
+ attr_reader :types
33
41
 
34
- # Whether to raise an error if a union type has no matching candidate.
42
+ # Whether to raise an error if values should be validated before deserializing, e.g. true -> "true"
35
43
  # @return [Boolean]
36
- attr_accessor :enforce_union_match
44
+ attr_accessor :enforce_typing
37
45
 
38
46
  # Whether to raise an error if access mode is not recognized.
39
47
  # @return [Boolean]
@@ -47,55 +55,94 @@ module Castkit
47
55
  # @return [Boolean]
48
56
  attr_accessor :enforce_array_options
49
57
 
50
- # Whether to generating warnings or not, defaults to `true`.
58
+ # Whether to raise an error for unknown and invalid type definitions.
59
+ # @return [Boolean]
60
+ attr_accessor :raise_type_errors
61
+
62
+ # Whether to emit warnings when Castkit detects misconfigurations.
51
63
  # @return [Boolean]
52
64
  attr_accessor :enable_warnings
53
65
 
54
- # Initializes the configuration with default validators and enforcement settings.
66
+ # Whether the strict flag is enabled by default for all DataObjects and Contracts.
67
+ # @return [Boolean]
68
+ attr_accessor :strict_by_default
69
+
70
+ # Initializes the configuration with default types and enforcement flags.
55
71
  #
56
72
  # @return [void]
57
73
  def initialize
58
- @validators = DEFAULT_VALIDATORS.dup
59
- @enforce_array_of_type = true
60
- @enforce_known_primitive_type = true
61
- @enforce_boolean_casting = true
62
- @enforce_union_match = true
74
+ @types = DEFAULT_TYPES.dup
75
+ @enforce_typing = true
63
76
  @enforce_attribute_access = true
64
77
  @enforce_unwrapped_prefix = true
65
78
  @enforce_array_options = true
79
+ @raise_type_errors = true
66
80
  @enable_warnings = true
81
+ @strict_by_default = true
82
+
83
+ apply_type_aliases!
67
84
  end
68
85
 
69
- # Registers a custom validator for a given type.
86
+ # Registers a new type definition.
70
87
  #
71
- # @param type [Symbol] the type symbol (e.g., :string, :integer)
72
- # @param validator [#call] a callable object that implements `call(value, options:, context:)`
73
- # @param override [Boolean] whether to override an existing validator
74
- # @raise [Castkit::Error] if validator does not respond to `.call`
88
+ # @param type [Symbol] the symbolic type name (e.g., :uuid)
89
+ # @param klass [Class<Castkit::Types::Generic>] the class to register
90
+ # @param override [Boolean] whether to allow overwriting existing registration
91
+ # @raise [Castkit::TypeError] if the type class is invalid or not a subclass of Generic
75
92
  # @return [void]
76
- def register_validator(type, validator, override: false)
77
- return if @validators.key?(type.to_sym) && !override
93
+ def register_type(type, klass, aliases: [], override: false)
94
+ type = type.to_sym
95
+ return if types.key?(type) && !override
78
96
 
79
- unless validator.respond_to?(:call)
80
- raise Castkit::Error, "Validator for `#{type}` must respond to `.call(value, options:, context:)`"
97
+ instance = klass.new
98
+ unless instance.is_a?(Castkit::Types::Generic)
99
+ raise Castkit::TypeError, "Expected subclass of Castkit::Types::Generic for `#{type}`"
81
100
  end
82
101
 
83
- @validators[type.to_sym] = validator
102
+ types[type] = instance
103
+
104
+ Castkit::Core::AttributeTypes.define_type_dsl(type) if Castkit::Core::AttributeTypes.respond_to?(:define_type_dsl)
105
+ return unless aliases.any?
106
+
107
+ aliases.each { |alias_type| register_type(alias_type, klass, override: override) }
84
108
  end
85
109
 
86
- # Returns the registered validator for the given type.
110
+ # Returns the type handler for a given type symbol.
87
111
  #
88
112
  # @param type [Symbol]
89
- # @return [#call, nil]
90
- def validator_for(type)
91
- validators[type.to_sym]
113
+ # @return [Castkit::Types::Generic]
114
+ # @raise [Castkit::TypeError] if the type is not registered
115
+ def fetch_type(type)
116
+ @types.fetch(type.to_sym) do
117
+ raise Castkit::TypeError, "Unknown type `#{type.inspect}`" if raise_type_errors
118
+ end
119
+ end
120
+
121
+ # Returns whether a type is currently registered.
122
+ #
123
+ # @param type [Symbol]
124
+ # @return [Boolean]
125
+ def type_registered?(type)
126
+ @types.key?(type.to_sym)
92
127
  end
93
128
 
94
- # Resets all validators to their default mappings.
129
+ # Restores the type registry to its default state.
95
130
  #
96
131
  # @return [void]
97
- def reset_validators!
98
- @validators = DEFAULT_VALIDATORS.dup
132
+ def reset_types!
133
+ @types = DEFAULT_TYPES.dup
134
+ apply_type_aliases!
135
+ end
136
+
137
+ private
138
+
139
+ # Registers aliases for primitive type definitions.
140
+ #
141
+ # @return [void]
142
+ def apply_type_aliases!
143
+ TYPE_ALIASES.each do |alias_key, canonical|
144
+ register_type(alias_key, DEFAULT_TYPES[canonical].class)
145
+ end
99
146
  end
100
147
  end
101
148
  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,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::Generic
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 Generic
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
+ Castkit::Contract::Result.new(definition[:name].to_s, input)
73
+ rescue Castkit::ContractError => e
74
+ Castkit::Contract::Result.new(definition[:name].to_s, input, errors: e.errors)
75
+ end
76
+
77
+ # Validates input and raises on failure.
78
+ #
79
+ # @param input [Hash]
80
+ # @raise [Castkit::ContractError]
81
+ # @return [void]
82
+ def validate!(input)
83
+ Castkit::Contract::Validator.call!(attributes.values, input, **validation_rules)
84
+ end
85
+
86
+ # Returns internal contract metadata.
87
+ #
88
+ # @return [Hash]
89
+ def definition
90
+ @definition ||= {
91
+ name: :ephemeral_contract,
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_contract, 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,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.inspect}" if success?
56
+
57
+ parsed_errors = errors.map { |k, v| " #{k}: #{v.inspect}" }.join("\n")
58
+ "[Castkit] Contract validation failed for #{contract.inspect}:\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