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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -0
- data/README.md +11 -3
- data/castkit.gemspec +2 -0
- data/lib/castkit/attribute.rb +82 -59
- data/lib/castkit/attributes/definition.rb +64 -0
- data/lib/castkit/attributes/options.rb +214 -0
- data/lib/castkit/castkit.rb +13 -3
- data/lib/castkit/cli/generate.rb +14 -0
- data/lib/castkit/contract/base.rb +0 -12
- data/lib/castkit/contract/validator.rb +5 -1
- data/lib/castkit/core/attributes.rb +87 -44
- data/lib/castkit/data_object.rb +2 -25
- data/lib/castkit/{ext → dsl}/attribute/access.rb +1 -1
- data/lib/castkit/{ext → dsl}/attribute/error_handling.rb +1 -1
- data/lib/castkit/{ext → dsl}/attribute/options.rb +1 -1
- data/lib/castkit/{ext → dsl}/attribute/validation.rb +3 -3
- data/lib/castkit/dsl/attribute.rb +47 -0
- data/lib/castkit/{ext → dsl}/data_object/contract.rb +1 -1
- data/lib/castkit/{ext → dsl}/data_object/deserialization.rb +6 -2
- data/lib/castkit/{ext → dsl}/data_object/plugins.rb +1 -1
- data/lib/castkit/{ext → dsl}/data_object/serialization.rb +1 -1
- data/lib/castkit/dsl/data_object.rb +61 -0
- data/lib/castkit/types/base.rb +24 -3
- data/lib/castkit/validators/boolean_validator.rb +3 -3
- data/lib/castkit/version.rb +1 -1
- data/lib/castkit.rb +1 -4
- data/lib/generators/attribute.rb +39 -0
- data/lib/generators/templates/attribute.rb.tt +21 -0
- data/lib/generators/templates/attribute_spec.rb.tt +41 -0
- data/lib/generators/templates/contract.rb.tt +2 -0
- data/lib/generators/templates/data_object.rb.tt +2 -0
- data/lib/generators/templates/type.rb.tt +2 -0
- data/lib/generators/templates/validator.rb.tt +1 -1
- metadata +45 -12
- data/.rspec_status +0 -195
- 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)
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
35
|
+
# Declares a composite (computed) attribute.
|
|
29
36
|
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
# @param
|
|
33
|
-
# @
|
|
34
|
-
# @
|
|
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
|
|
47
|
+
# Declares a group of transient attributes within a block.
|
|
42
48
|
#
|
|
43
|
-
# These attributes are
|
|
49
|
+
# These attributes are excluded from serialization (`to_h`) and not stored.
|
|
44
50
|
#
|
|
45
|
-
# @yield
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
78
|
+
# Declares a group of required attributes.
|
|
73
79
|
#
|
|
74
|
-
# @param options [Hash] shared options for attributes inside the block
|
|
75
|
-
# @yield
|
|
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
|
|
87
|
+
# Declares a group of optional attributes.
|
|
82
88
|
#
|
|
83
|
-
# @param options [Hash] shared options for attributes inside the block
|
|
84
|
-
# @yield
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
|
183
|
+
# Applies a temporary `access` context to all attributes within a block.
|
|
141
184
|
#
|
|
142
|
-
# @param access [Array<Symbol>] e.g
|
|
185
|
+
# @param access [Array<Symbol>] e.g. `[:read]` or `[:write]`
|
|
143
186
|
# @param options [Hash]
|
|
144
|
-
# @yield the block containing
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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?
|
data/lib/castkit/data_object.rb
CHANGED
|
@@ -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 "
|
|
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
|
-
|
|
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?
|
|
@@ -4,13 +4,13 @@ require_relative "error_handling"
|
|
|
4
4
|
require_relative "options"
|
|
5
5
|
|
|
6
6
|
module Castkit
|
|
7
|
-
module
|
|
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::
|
|
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::
|
|
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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Castkit
|
|
4
|
-
module
|
|
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)
|
|
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
|
|
|
@@ -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
|
data/lib/castkit/types/base.rb
CHANGED
|
@@ -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
|
|
35
|
+
invoke_validator(validator, deserialized_value, options: options, context: context)
|
|
35
36
|
return deserialized_value
|
|
36
37
|
end
|
|
37
38
|
|
|
38
|
-
validator
|
|
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
|
|
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,
|
|
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
|
data/lib/castkit/version.rb
CHANGED
data/lib/castkit.rb
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "castkit/
|
|
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
|