castkit 0.3.0 → 0.3.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/README.md +11 -3
  4. data/castkit.gemspec +2 -0
  5. data/lib/castkit/attribute.rb +82 -59
  6. data/lib/castkit/attributes/definition.rb +64 -0
  7. data/lib/castkit/attributes/options.rb +214 -0
  8. data/lib/castkit/castkit.rb +13 -3
  9. data/lib/castkit/cli/generate.rb +14 -0
  10. data/lib/castkit/contract/base.rb +0 -12
  11. data/lib/castkit/contract/validator.rb +5 -1
  12. data/lib/castkit/core/attributes.rb +87 -44
  13. data/lib/castkit/data_object.rb +2 -25
  14. data/lib/castkit/{ext → dsl}/attribute/access.rb +1 -1
  15. data/lib/castkit/{ext → dsl}/attribute/error_handling.rb +1 -1
  16. data/lib/castkit/{ext → dsl}/attribute/options.rb +1 -1
  17. data/lib/castkit/{ext → dsl}/attribute/validation.rb +3 -3
  18. data/lib/castkit/dsl/attribute.rb +47 -0
  19. data/lib/castkit/{ext → dsl}/data_object/contract.rb +1 -1
  20. data/lib/castkit/{ext → dsl}/data_object/deserialization.rb +6 -2
  21. data/lib/castkit/{ext → dsl}/data_object/plugins.rb +1 -1
  22. data/lib/castkit/{ext → dsl}/data_object/serialization.rb +1 -1
  23. data/lib/castkit/dsl/data_object.rb +61 -0
  24. data/lib/castkit/types/base.rb +24 -3
  25. data/lib/castkit/validators/boolean_validator.rb +3 -3
  26. data/lib/castkit/version.rb +1 -1
  27. data/lib/castkit.rb +1 -4
  28. data/lib/generators/attribute.rb +39 -0
  29. data/lib/generators/templates/attribute.rb.tt +21 -0
  30. data/lib/generators/templates/attribute_spec.rb.tt +41 -0
  31. data/lib/generators/templates/contract.rb.tt +2 -0
  32. data/lib/generators/templates/data_object.rb.tt +2 -0
  33. data/lib/generators/templates/type.rb.tt +2 -0
  34. data/lib/generators/templates/validator.rb.tt +1 -1
  35. metadata +45 -12
  36. data/.rspec_status +0 -195
  37. data/lib/castkit/core/registerable.rb +0 -59
@@ -188,7 +188,11 @@ module Castkit
188
188
  # @return [Object, nil]
189
189
  def resolve_input_value(input, attribute)
190
190
  attribute.key_path(with_aliases: true).each do |path|
191
- value = path.reduce(input) { |memo, key| memo.is_a?(Hash) ? memo[key] : nil }
191
+ value = path.reduce(input) do |memo, key|
192
+ next memo unless memo.is_a?(Hash)
193
+
194
+ memo.key?(key) ? memo[key] : memo[key.to_s]
195
+ end
192
196
  return value unless value.nil?
193
197
  end
194
198
 
@@ -4,45 +4,51 @@ module Castkit
4
4
  module Core
5
5
  # Provides DSL and implementation for declaring attributes within a Castkit::DataObject.
6
6
  #
7
- # Includes support for regular, composite, transient, readonly/writeonly, and grouped attribute definitions.
7
+ # Supports reusable attribute definitions, transient fields, composite readers, and
8
+ # grouped declarations such as `readonly`, `optional`, and `transient` blocks.
9
+ #
10
+ # This module is included into `Castkit::DataObject` and handles attribute registration,
11
+ # accessor generation, and typed writing behavior.
8
12
  module Attributes
9
- # Declares an attribute with the given type and options.
13
+ # Declares an attribute on the data object.
10
14
  #
15
+ # Accepts either inline options or a reusable attribute definition (`using` or `definition`).
11
16
  # If `:transient` is true, defines only standard accessors and skips serialization logic.
12
17
  #
13
- # @param field [Symbol]
14
- # @param type [Symbol, Class]
15
- # @param options [Hash]
18
+ # @param field [Symbol] the attribute name
19
+ # @param type [Symbol, Class] the attribute's declared type
20
+ # @param definition [Hash, nil] an optional pre-built definition object (`{ type:, options: }`)
21
+ # @param using [Castkit::Attributes::Base, nil] an optional class-based definition (`.definition`)
22
+ # @param options [Hash] additional options like `default`, `access`, `required`, etc.
16
23
  # @return [void]
17
- # @raise [Castkit::DataObjectError] if the attribute is already defined
18
- def attribute(field, type, **options)
24
+ # @raise [Castkit::DataObjectError] if attribute already defined or type mismatch
25
+ def attribute(field, type = nil, definition = nil, using: nil, **options)
19
26
  field = field.to_sym
20
27
  raise Castkit::DataObjectError, "Attribute '#{field}' already defined" if attributes.key?(field)
21
28
 
22
- options = build_options(options)
29
+ type, options = use_definition(field, definition || using&.definition, type, options)
23
30
  return define_attribute(field, type, **options) unless options[:transient]
24
31
 
25
32
  attr_accessor field
26
33
  end
27
34
 
28
- # Declares a computed (composite) attribute.
35
+ # Declares a composite (computed) attribute.
29
36
  #
30
- # The provided block defines the read behavior.
31
- #
32
- # @param field [Symbol]
33
- # @param type [Symbol, Class]
34
- # @param options [Hash]
35
- # @yieldreturn [Object] evaluated composite value
37
+ # @param field [Symbol] the name of the attribute
38
+ # @param type [Symbol, Class] the attribute type
39
+ # @param options [Hash] additional attribute options
40
+ # @yieldreturn [Object] the value to return when the reader is called
41
+ # @return [void]
36
42
  def composite(field, type, **options, &block)
37
43
  attribute(field, type, **options, composite: true)
38
44
  define_method(field, &block)
39
45
  end
40
46
 
41
- # Declares a group of transient attributes within the given block.
47
+ # Declares a group of transient attributes within a block.
42
48
  #
43
- # These attributes are not serialized or included in `to_h`.
49
+ # These attributes are excluded from serialization (`to_h`) and not stored.
44
50
  #
45
- # @yield defines one or more transient attributes via `attribute`
51
+ # @yield a block containing `attribute` calls
46
52
  # @return [void]
47
53
  def transient(&block)
48
54
  @__transient_context = true
@@ -51,61 +57,97 @@ module Castkit
51
57
  @__transient_context = nil
52
58
  end
53
59
 
54
- # Declares a group of readonly attributes within the given block.
60
+ # Declares a group of readonly attributes (accessible for read only).
55
61
  #
56
- # @param options [Hash] shared options for attributes inside the block
57
- # @yield defines attributes with `access: [:read]`
62
+ # @param options [Hash] shared options for all attributes inside the block
63
+ # @yield a block containing `attribute` calls
58
64
  # @return [void]
59
65
  def readonly(**options, &block)
60
66
  with_access([:read], options, &block)
61
67
  end
62
68
 
63
- # Declares a group of writeonly attributes within the given block.
69
+ # Declares a group of writeonly attributes (accessible for write only).
64
70
  #
65
- # @param options [Hash] shared options for attributes inside the block
66
- # @yield defines attributes with `access: [:write]`
71
+ # @param options [Hash] shared options for all attributes inside the block
72
+ # @yield a block containing `attribute` calls
67
73
  # @return [void]
68
74
  def writeonly(**options, &block)
69
75
  with_access([:write], options, &block)
70
76
  end
71
77
 
72
- # Declares a group of required attributes within the given block.
78
+ # Declares a group of required attributes.
73
79
  #
74
- # @param options [Hash] shared options for attributes inside the block
75
- # @yield defines attributes with `required: true`
80
+ # @param options [Hash] shared options for all attributes inside the block
81
+ # @yield a block containing `attribute` calls
76
82
  # @return [void]
77
83
  def required(**options, &block)
78
84
  with_required(true, options, &block)
79
85
  end
80
86
 
81
- # Declares a group of optional attributes within the given block.
87
+ # Declares a group of optional attributes.
82
88
  #
83
- # @param options [Hash] shared options for attributes inside the block
84
- # @yield defines attributes with `required: false`
89
+ # @param options [Hash] shared options for all attributes inside the block
90
+ # @yield a block containing `attribute` calls
85
91
  # @return [void]
86
92
  def optional(**options, &block)
87
93
  with_required(false, options, &block)
88
94
  end
89
95
 
90
- # Returns all declared non-transient attributes.
96
+ # Returns all non-transient attributes defined on the class.
91
97
  #
92
98
  # @return [Hash{Symbol => Castkit::Attribute}]
93
99
  def attributes
94
100
  @attributes ||= {}
95
101
  end
96
102
 
97
- # Alias for `composite`
103
+ def inherited(subclass)
104
+ super
105
+
106
+ parent_attributes = instance_variable_get(:@attributes)
107
+ subclass.instance_variable_set(:@attributes, parent_attributes.dup) if parent_attributes
108
+ end
109
+
110
+ # Alias for {#attribute}
111
+ #
112
+ # @see #attribute
113
+ alias attr attribute
114
+
115
+ # Alias for {#composite}
98
116
  #
99
117
  # @see #composite
100
118
  alias property composite
101
119
 
102
120
  private
103
121
 
104
- # Defines a full attribute, including accessor methods and type logic.
122
+ # Applies a reusable definition to the current attribute call.
123
+ #
124
+ # Ensures the declared type matches and merges options.
125
+ #
126
+ # @param field [Symbol] the attribute name
127
+ # @param definition [Hash{Symbol => Object}, nil]
128
+ # @param type [Symbol, Class]
129
+ # @param options [Hash]
130
+ # @return [Array<(Symbol, Hash)>] the final type and options
131
+ # @raise [Castkit::DataObjectError] if type mismatch occurs
132
+ def use_definition(field, definition, type, options)
133
+ type ||= definition&.fetch(:type, nil)
134
+ raise Castkit::AttributeError, "Attribute `#{field} has no type" if type.nil?
135
+
136
+ if definition && type != definition[:type]
137
+ raise Castkit::AttributeError,
138
+ "Attribute `#{field}` type mismatch: expected #{definition[:type].inspect}, got #{type.inspect}"
139
+ end
140
+
141
+ options = definition[:options].merge(options) if definition
142
+ [type, build_options(options)]
143
+ end
144
+
145
+ # Instantiates and stores a Castkit::Attribute, defining accessors as needed.
105
146
  #
106
147
  # @param field [Symbol]
107
148
  # @param type [Symbol, Class]
108
149
  # @param options [Hash]
150
+ # @return [void]
109
151
  def define_attribute(field, type, **options)
110
152
  attribute = Castkit::Attribute.new(field, type, **options)
111
153
  attributes[field] = attribute
@@ -121,13 +163,14 @@ module Castkit
121
163
  end
122
164
  end
123
165
 
124
- # Defines a type-aware writer method for the attribute.
166
+ # Defines a writer method that enforces type coercion.
125
167
  #
126
168
  # @param field [Symbol]
127
169
  # @param attribute [Castkit::Attribute]
170
+ # @return [void]
128
171
  def define_typed_writer(field, attribute)
129
172
  define_method("#{field}=") do |value|
130
- deserialized_value = Castkit.type_caster(attribute.type).call(
173
+ deserialized_value = Castkit.type_caster(attribute.type.to_sym).call(
131
174
  value,
132
175
  options: attribute.options,
133
176
  context: attribute.field
@@ -137,11 +180,12 @@ module Castkit
137
180
  end
138
181
  end
139
182
 
140
- # Applies scoped access control to all attributes declared in the given block.
183
+ # Applies a temporary `access` context to all attributes within a block.
141
184
  #
142
- # @param access [Array<Symbol>] e.g., [:read] or [:write]
185
+ # @param access [Array<Symbol>] e.g. `[:read]` or `[:write]`
143
186
  # @param options [Hash]
144
- # @yield the block containing one or more `attribute` calls
187
+ # @yield the block containing `attribute` calls
188
+ # @return [void]
145
189
  def with_access(access, options = {}, &block)
146
190
  @__access_context = access
147
191
  @__block_options = options
@@ -151,11 +195,12 @@ module Castkit
151
195
  @__block_options = nil
152
196
  end
153
197
 
154
- # Applies scoped required/optional flag to all attributes declared in the given block.
198
+ # Applies a temporary `required` context to all attributes within a block.
155
199
  #
156
200
  # @param flag [Boolean]
157
201
  # @param options [Hash]
158
- # @yield the block containing one or more `attribute` calls
202
+ # @yield the block containing `attribute` calls
203
+ # @return [void]
159
204
  def with_required(flag, options = {}, &block)
160
205
  @__required_context = flag
161
206
  @__block_options = options
@@ -165,12 +210,10 @@ module Castkit
165
210
  @__block_options = nil
166
211
  end
167
212
 
168
- # Builds effective options for the current attribute definition.
169
- #
170
- # Merges scoped flags like `required`, `access`, and `transient` if present.
213
+ # Merges any current context flags (e.g., required, access) into the options hash.
171
214
  #
172
215
  # @param options [Hash]
173
- # @return [Hash]
216
+ # @return [Hash] effective options for the attribute
174
217
  def build_options(options)
175
218
  base = @__block_options || {}
176
219
  base = base.merge(required: @__required_context) unless @__required_context.nil?
@@ -5,14 +5,7 @@ require_relative "error"
5
5
  require_relative "attribute"
6
6
  require_relative "serializers/default_serializer"
7
7
  require_relative "contract/validator"
8
- require_relative "core/config"
9
- require_relative "core/attributes"
10
- require_relative "core/attribute_types"
11
- require_relative "core/registerable"
12
- require_relative "ext/data_object/contract"
13
- require_relative "ext/data_object/deserialization"
14
- require_relative "ext/data_object/plugins"
15
- require_relative "ext/data_object/serialization"
8
+ require_relative "dsl/data_object"
16
9
 
17
10
  module Castkit
18
11
  # Base class for defining declarative, typed data transfer objects (DTOs).
@@ -30,25 +23,9 @@ module Castkit
30
23
  # user = UserDto.new(name: "Alice", age: 30)
31
24
  # user.to_json #=> '{"name":"Alice","age":30}'
32
25
  class DataObject
33
- extend Castkit::Core::Config
34
- extend Castkit::Core::Attributes
35
- extend Castkit::Core::AttributeTypes
36
- extend Castkit::Core::Registerable
37
- extend Castkit::Ext::DataObject::Contract
38
- extend Castkit::Ext::DataObject::Plugins
39
-
40
- include Castkit::Ext::DataObject::Serialization
41
- include Castkit::Ext::DataObject::Deserialization
26
+ include Castkit::DSL::DataObject
42
27
 
43
28
  class << self
44
- # Registers the current class under `Castkit::DataObjects`.
45
- #
46
- # @param as [String, Symbol, nil] The constant name to use (PascalCase). Defaults to class name or "Anonymous".
47
- # @return [Class] the registered dataobject class
48
- def register!(as: nil)
49
- super(namespace: :dataobjects, as: as)
50
- end
51
-
52
29
  def build(&block)
53
30
  klass = Class.new(self)
54
31
  klass.class_eval(&block) if block_given?
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castkit
4
- module Ext
4
+ module DSL
5
5
  module Attribute
6
6
  # Provides access control helpers for attributes.
7
7
  #
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castkit
4
- module Ext
4
+ module DSL
5
5
  module Attribute
6
6
  # Provides centralized handling of attribute casting and validation errors.
7
7
  #
@@ -3,7 +3,7 @@
3
3
  require_relative "../../data_object"
4
4
 
5
5
  module Castkit
6
- module Ext
6
+ module DSL
7
7
  module Attribute
8
8
  # Provides access to normalized attribute options and helper predicates.
9
9
  #
@@ -4,13 +4,13 @@ require_relative "error_handling"
4
4
  require_relative "options"
5
5
 
6
6
  module Castkit
7
- module Ext
7
+ module DSL
8
8
  module Attribute
9
9
  # Provides validation logic for attribute configuration.
10
10
  #
11
11
  # These checks are typically performed at attribute initialization to catch misconfigurations early.
12
12
  module Validation
13
- include Castkit::Ext::Attribute::ErrorHandling
13
+ include Castkit::DSL::Attribute::ErrorHandling
14
14
 
15
15
  private
16
16
 
@@ -58,7 +58,7 @@ module Castkit
58
58
  # @raise [Castkit::AttributeError] if any access mode is invalid and enforcement is enabled
59
59
  def validate_access!
60
60
  access.each do |mode|
61
- next if Castkit::Ext::Attribute::Options::DEFAULT_OPTIONS[:access].include?(mode)
61
+ next if Castkit::Attributes::Options::DEFAULTS[:access].include?(mode)
62
62
 
63
63
  handle_error(:access, mode: mode, context: to_h)
64
64
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "attribute/options"
4
+ require_relative "attribute/access"
5
+ require_relative "attribute/validation"
6
+
7
+ module Castkit
8
+ module DSL
9
+ # Provides a unified entry point for attribute-level DSL extensions.
10
+ #
11
+ # This module bundles together the core DSL modules for configuring attributes.
12
+ # It is included internally by systems that support Castkit-style attribute declarations,
13
+ # such as {Castkit::DataObject} and {Castkit::Contract::Base}.
14
+ #
15
+ # When included, it mixes in:
16
+ # - {Castkit::DSL::Attribute::Options} – option-setting methods (e.g., `required`, `default`, etc.)
17
+ # - {Castkit::DSL::Attribute::Access} – access control methods (e.g., `readonly`, `access`)
18
+ # - {Castkit::DSL::Attribute::Validation} – validation helpers (e.g., `format`, `validator`)
19
+ #
20
+ # @example Extending a custom DSL that uses Castkit-style attributes
21
+ # class MyCustomSchema
22
+ # include Castkit::DSL::Attribute
23
+ #
24
+ # def self.required(value)
25
+ # # interpret DSL options
26
+ # end
27
+ # end
28
+ #
29
+ # class MyString < MyCustomSchema
30
+ # type :string
31
+ # required true
32
+ # access [:read]
33
+ # end
34
+ #
35
+ # @note This module is not intended to be mixed into {Castkit::Attributes::Definition}.
36
+ module Attribute
37
+ # Hook called when this module is included.
38
+ #
39
+ # @param base [Class, Module] the including class or module
40
+ def self.included(base)
41
+ base.include(Castkit::DSL::Attribute::Options)
42
+ base.include(Castkit::DSL::Attribute::Access)
43
+ base.include(Castkit::DSL::Attribute::Validation)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -3,7 +3,7 @@
3
3
  require_relative "../../contract"
4
4
 
5
5
  module Castkit
6
- module Ext
6
+ module DSL
7
7
  module DataObject
8
8
  # Extension module that adds contract support to Castkit::DataObject classes.
9
9
  #
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castkit
4
- module Ext
4
+ module DSL
5
5
  module DataObject
6
6
  # Adds deserialization support for Castkit::DataObject instances.
7
7
  #
@@ -104,7 +104,11 @@ module Castkit
104
104
  # @return [Object, nil]
105
105
  def resolve_input_value(input, attribute)
106
106
  attribute.key_path(with_aliases: true).each do |path|
107
- value = path.reduce(input) { |memo, key| memo.is_a?(Hash) ? memo[key] : nil }
107
+ value = path.reduce(input) do |memo, key|
108
+ next memo unless memo.is_a?(Hash)
109
+
110
+ memo.key?(key) ? memo[key] : memo[key.to_s]
111
+ end
108
112
  return value unless value.nil?
109
113
  end
110
114
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castkit
4
- module Ext
4
+ module DSL
5
5
  module DataObject
6
6
  # Provides plugin support for DataObject classes.
7
7
  #
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castkit
4
- module Ext
4
+ module DSL
5
5
  module DataObject
6
6
  # Provides per-class serialization configuration for Castkit::Dataobject, including
7
7
  # root key handling and ignore rules.
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../core/config"
4
+ require_relative "../core/attributes"
5
+ require_relative "../core/attribute_types"
6
+ require_relative "data_object/contract"
7
+ require_relative "data_object/plugins"
8
+ require_relative "data_object/serialization"
9
+ require_relative "data_object/deserialization"
10
+
11
+ module Castkit
12
+ module DSL
13
+ # Provides the complete DSL used by Castkit data objects.
14
+ #
15
+ # This module can be included into any class to make it behave like a `Castkit::DataObject`
16
+ # without requiring subclassing. It wires in the full attribute DSL, type system, contract support,
17
+ # plugin lifecycle, and (de)serialization logic.
18
+ #
19
+ # This is what powers `Castkit::DataObject` internally, and is intended for advanced use
20
+ # cases where composition is preferred over inheritance.
21
+ #
22
+ # When included, this module:
23
+ #
24
+ # - `extend`s:
25
+ # - {Castkit::Core::Config} – configuration and context behavior
26
+ # - {Castkit::Core::Attributes} – the DSL for declaring attributes
27
+ # - {Castkit::Core::AttributeTypes} – support for custom type resolution
28
+ # - {Castkit::DSL::DataObject::Contract} – validation contract hooks
29
+ # - {Castkit::DSL::DataObject::Plugins} – plugin hooks and lifecycle events
30
+ #
31
+ # - `include`s:
32
+ # - {Castkit::DSL::DataObject::Serialization} – `#to_h`, `#as_json`, etc.
33
+ # - {Castkit::DSL::DataObject::Deserialization} – `from_h`, `from_json`, etc.
34
+ #
35
+ # @example Including in a custom data object
36
+ # class MyObject
37
+ # include Castkit::DSL::DataObject
38
+ #
39
+ # string :id
40
+ # boolean :active, default: true
41
+ # end
42
+ #
43
+ # @see Castkit::DataObject for the default implementation
44
+ module DataObject
45
+ # Hook triggered when the module is included.
46
+ #
47
+ # @param base [Class] the including class
48
+ # @return [void]
49
+ def self.included(base)
50
+ base.extend(Castkit::Core::Config)
51
+ base.extend(Castkit::Core::Attributes)
52
+ base.extend(Castkit::Core::AttributeTypes)
53
+ base.extend(Castkit::DSL::DataObject::Contract)
54
+ base.extend(Castkit::DSL::DataObject::Plugins)
55
+
56
+ base.include(Castkit::DSL::DataObject::Serialization)
57
+ base.include(Castkit::DSL::DataObject::Deserialization)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -24,18 +24,19 @@ module Castkit
24
24
  # @param options [Hash] options passed to `validate!`, e.g., `min`, `max`, `force_type`
25
25
  # @param context [Symbol, String, nil] context label for error messages
26
26
  # @return [Object] the deserialized and validated value
27
- def cast!(value, validator: nil, options: {}, context: {})
27
+ def cast!(value, validator: nil, options: {}, context: {}, **extra_options)
28
+ options = options.merge(extra_options)
28
29
  instance = new
29
30
  validator ||= options.delete(:validator)
30
31
  validator ||= default_validator(instance)
31
32
 
32
33
  if options[:force_type]
33
34
  deserialized_value = instance.deserialize(value)
34
- validator.call(deserialized_value, options: options, context: context)
35
+ invoke_validator(validator, deserialized_value, options: options, context: context)
35
36
  return deserialized_value
36
37
  end
37
38
 
38
- validator.call(value, options: options, context: context)
39
+ invoke_validator(validator, value, options: options, context: context)
39
40
  instance.deserialize(value)
40
41
  end
41
42
 
@@ -76,6 +77,26 @@ module Castkit
76
77
  instance.validate!(value, options: options, context: context)
77
78
  end
78
79
  end
80
+
81
+ # Dispatches validation to support callable validators with different arities.
82
+ #
83
+ # @param validator [#call, Proc] the validator to invoke
84
+ # @param value [Object] the value being validated
85
+ # @param options [Hash] validation options
86
+ # @param context [Symbol, String, nil] context for error messages
87
+ # @return [void]
88
+ def invoke_validator(validator, value, options:, context:)
89
+ return validator.call(value, options: options, context: context) unless validator.is_a?(Proc)
90
+
91
+ case validator.arity
92
+ when 1
93
+ validator.call(value)
94
+ when 2
95
+ validator.call(value, options)
96
+ else
97
+ validator.call(value, options: options, context: context)
98
+ end
99
+ end
79
100
  end
80
101
 
81
102
  # Deserializes the value. Override in subclasses to coerce input (e.g., `"123"` → `123`).
@@ -16,15 +16,15 @@ module Castkit
16
16
  # validator.call("true", _options: {}, context: :enabled) # => true
17
17
  # validator.call("0", _options: {}, context: :enabled) # => false
18
18
  # validator.call("nope", _options: {}, context: :enabled) # raises Castkit::AttributeError
19
- class BooleanValidator
19
+ class BooleanValidator < Castkit::Validators::Base
20
20
  # Validates the Boolean value.
21
21
  #
22
22
  # @param value [Object] the input to validate
23
- # @param _options [Hash] unused, provided for consistency with other validators
23
+ # @param options [Hash] unused, provided for consistency with other validators
24
24
  # @param context [Symbol, String] the attribute name or path for error messages
25
25
  # @return [Boolean]
26
26
  # @raise [Castkit::AttributeError] if the value is not a recognizable boolean
27
- def call(value, _options:, context:)
27
+ def call(value, options:, context:) # rubocop:disable Lint/UnusedMethodArgument
28
28
  case value.to_s.downcase
29
29
  when "true", "1"
30
30
  true
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castkit
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
  end
data/lib/castkit.rb CHANGED
@@ -1,9 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "castkit/version"
4
- require_relative "castkit/attribute"
5
- require_relative "castkit/contract"
6
- require_relative "castkit/data_object"
3
+ require_relative "castkit/castkit"
7
4
 
8
5
  # Castkit is a lightweight, type-safe data object system for Ruby.
9
6
  #
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor/group"
4
+ require "castkit/inflector"
5
+ require_relative "base"
6
+
7
+ module Castkit
8
+ module Generators
9
+ # Generator for creating Castkit attribute definitions.
10
+ #
11
+ # Generates a class inheriting from `Castkit::Attributes::Definition`
12
+ # and an optional spec file.
13
+ #
14
+ # Example:
15
+ # $ castkit generate attribute OptionalString required:false default:"N/A"
16
+ #
17
+ # This will generate:
18
+ # - lib/castkit/attributes/optional_string.rb
19
+ # - spec/castkit/attributes/optional_string_spec.rb
20
+ #
21
+ # @see Castkit::Generators::Base
22
+ class Attribute < Castkit::Generators::Base
23
+ component :attribute
24
+
25
+ argument :type,
26
+ type: :string,
27
+ desc: "The base type (e.g., string, integer)"
28
+
29
+ private
30
+
31
+ # @return [Hash] configuration passed into templates
32
+ def config
33
+ super.merge(
34
+ type: type.first.gsub(/^:/, "")
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end