castkit 0.3.1 → 0.4.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/CHANGELOG.md +5 -0
- data/README.md +8 -8
- data/castkit.gemspec +2 -0
- data/lib/castkit/attribute.rb +11 -12
- data/lib/castkit/castkit.rb +1 -0
- data/lib/castkit/configuration.rb +25 -48
- data/lib/castkit/contract/base.rb +9 -12
- data/lib/castkit/contract/result.rb +10 -6
- data/lib/castkit/core/attribute_types.rb +3 -1
- data/lib/castkit/core/attributes.rb +49 -25
- data/lib/castkit/core/config.rb +23 -13
- data/lib/castkit/data_object.rb +7 -4
- data/lib/castkit/dsl/data_object/deserialization.rb +18 -1
- data/lib/castkit/dsl/data_object/introspection.rb +52 -0
- data/lib/castkit/dsl/data_object/serialization.rb +4 -1
- data/lib/castkit/dsl/data_object.rb +4 -0
- data/lib/castkit/error.rb +8 -4
- data/lib/castkit/plugins.rb +12 -3
- data/lib/castkit/serializers/base.rb +9 -4
- data/lib/castkit/serializers/default_serializer.rb +10 -10
- data/lib/castkit/validators/collection_validator.rb +2 -2
- data/lib/castkit/version.rb +1 -1
- metadata +31 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1202666103d91222c67462e90710b701128129cdbdb2b1363ed5be3603b04cc9
|
|
4
|
+
data.tar.gz: a64630cb20e6cca047cb3b5c0a68a251b33fdd92265d077aaf9c04e7e501ab81
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b8d1f485109363ccab35aa46cf605d0c06d96b31257168878422e89bf19c4e44c315fb5481fef72b45192cc133258621657b82a98fc173da2f755d541eabc3a8
|
|
7
|
+
data.tar.gz: 841d82b75c79858208296e300e86b9e4405546d90df7e7cee1cf6071b3811f7c3e05abcb43e78962ae07a01f681ff82817a40b98631590ff960abb2d4e30c5a8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
# v0.4.0
|
|
4
|
+
|
|
5
|
+
- Refactored Castkit internals to build on Cattri registries for attributes, configuration, contracts, and attribute definitions
|
|
6
|
+
- Added Cattri-powered introspection helpers, serialization toggle behavior and enhanced type/validator DSLs, plus coverage for the new plugin helpers
|
|
7
|
+
|
|
3
8
|
## [0.3.0] - 2025-04-16
|
|
4
9
|
|
|
5
10
|
### Added
|
data/README.md
CHANGED
|
@@ -60,14 +60,14 @@ Castkit comes with built-in support for primitive types and allows registration
|
|
|
60
60
|
|
|
61
61
|
```ruby
|
|
62
62
|
{
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
array: Castkit::Types::Collection,
|
|
64
|
+
boolean: Castkit::Types::Boolean,
|
|
65
|
+
date: Castkit::Types::Date,
|
|
66
|
+
datetime: Castkit::Types::DateTime,
|
|
67
|
+
float: Castkit::Types::Float,
|
|
68
|
+
hash: Castkit::Types::Base,
|
|
69
|
+
integer: Castkit::Types::Integer,
|
|
70
|
+
string: Castkit::Types::String
|
|
71
71
|
}
|
|
72
72
|
```
|
|
73
73
|
|
data/castkit.gemspec
CHANGED
|
@@ -33,6 +33,7 @@ Gem::Specification.new do |spec|
|
|
|
33
33
|
spec.require_paths = ["lib"]
|
|
34
34
|
|
|
35
35
|
# Runtime dependencies
|
|
36
|
+
spec.add_dependency "cattri", ">=0.2.3"
|
|
36
37
|
spec.add_dependency "thor"
|
|
37
38
|
|
|
38
39
|
# Development dependencies
|
|
@@ -40,5 +41,6 @@ Gem::Specification.new do |spec|
|
|
|
40
41
|
spec.add_development_dependency "rubocop"
|
|
41
42
|
spec.add_development_dependency "simplecov"
|
|
42
43
|
spec.add_development_dependency "simplecov-cobertura"
|
|
44
|
+
spec.add_development_dependency "simplecov-html"
|
|
43
45
|
spec.add_development_dependency "yard"
|
|
44
46
|
end
|
data/lib/castkit/attribute.rb
CHANGED
|
@@ -23,6 +23,11 @@ module Castkit
|
|
|
23
23
|
# @see Castkit::DSL::Attribute::Validation
|
|
24
24
|
class Attribute
|
|
25
25
|
include Castkit::DSL::Attribute
|
|
26
|
+
include Cattri
|
|
27
|
+
|
|
28
|
+
cattri :field, nil, expose: :read, final: true
|
|
29
|
+
cattri :type, nil, expose: :read, final: true
|
|
30
|
+
cattri :options, nil, expose: :read, final: true
|
|
26
31
|
|
|
27
32
|
class << self
|
|
28
33
|
# Defines a reusable attribute definition via a DSL wrapper.
|
|
@@ -71,15 +76,6 @@ module Castkit
|
|
|
71
76
|
end
|
|
72
77
|
end
|
|
73
78
|
|
|
74
|
-
# @return [Symbol] the attribute name
|
|
75
|
-
attr_reader :field
|
|
76
|
-
|
|
77
|
-
# @return [Symbol, Class, Array] the declared or normalized type
|
|
78
|
-
attr_reader :type
|
|
79
|
-
|
|
80
|
-
# @return [Hash] full option hash, including merged defaults
|
|
81
|
-
attr_reader :options
|
|
82
|
-
|
|
83
79
|
# Initializes a new attribute definition.
|
|
84
80
|
#
|
|
85
81
|
# @param field [Symbol] the attribute name
|
|
@@ -87,10 +83,13 @@ module Castkit
|
|
|
87
83
|
# @param default [Object, Proc, nil] optional static or callable default
|
|
88
84
|
# @param options [Hash] additional attribute options
|
|
89
85
|
def initialize(field, type, default: nil, **options)
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
super()
|
|
87
|
+
|
|
88
|
+
cattri_variable_set(:field, field, final: true)
|
|
89
|
+
cattri_variable_set(:type, self.class.normalize_type(type), final: true)
|
|
90
|
+
|
|
92
91
|
@default = default
|
|
93
|
-
|
|
92
|
+
cattri_variable_set(:options, populate_options(options), final: true)
|
|
94
93
|
|
|
95
94
|
validate!
|
|
96
95
|
end
|
data/lib/castkit/castkit.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "types"
|
|
4
|
+
require "cattri"
|
|
4
5
|
|
|
5
6
|
module Castkit
|
|
6
7
|
# Configuration container for global Castkit settings.
|
|
@@ -8,6 +9,8 @@ module Castkit
|
|
|
8
9
|
# This includes type registration, validation, and enforcement flags
|
|
9
10
|
# used throughout Castkit's attribute system.
|
|
10
11
|
class Configuration
|
|
12
|
+
include Cattri
|
|
13
|
+
|
|
11
14
|
# Default mapping of primitive type definitions.
|
|
12
15
|
#
|
|
13
16
|
# @return [Hash{Symbol => Castkit::Types::Base}]
|
|
@@ -36,56 +39,30 @@ module Castkit
|
|
|
36
39
|
uuid: :string
|
|
37
40
|
}.freeze
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# Whether to raise an error if values should be validated before deserializing, e.g. true -> "true"
|
|
49
|
-
# @return [Boolean]
|
|
50
|
-
attr_accessor :enforce_typing
|
|
51
|
-
|
|
52
|
-
# Whether to raise an error if access mode is not recognized.
|
|
53
|
-
# @return [Boolean]
|
|
54
|
-
attr_accessor :enforce_attribute_access
|
|
55
|
-
|
|
56
|
-
# Whether to raise an error if a prefix is defined without `unwrapped: true`.
|
|
57
|
-
# @return [Boolean]
|
|
58
|
-
attr_accessor :enforce_unwrapped_prefix
|
|
59
|
-
|
|
60
|
-
# Whether to raise an error if an array attribute is missing the `of:` type.
|
|
61
|
-
# @return [Boolean]
|
|
62
|
-
attr_accessor :enforce_array_options
|
|
63
|
-
|
|
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.
|
|
69
|
-
# @return [Boolean]
|
|
70
|
-
attr_accessor :enable_warnings
|
|
71
|
-
|
|
72
|
-
# Whether the strict flag is enabled by default for all DataObjects and Contracts.
|
|
73
|
-
# @return [Boolean]
|
|
74
|
-
attr_accessor :strict_by_default
|
|
42
|
+
cattri :types, -> { DEFAULT_TYPES.dup }, expose: :read_write
|
|
43
|
+
cattri :default_plugins, [], expose: :read_write
|
|
44
|
+
cattri :enforce_typing, true, expose: :read_write
|
|
45
|
+
cattri :enforce_attribute_access, true, expose: :read_write
|
|
46
|
+
cattri :enforce_unwrapped_prefix, true, expose: :read_write
|
|
47
|
+
cattri :enforce_array_options, true, expose: :read_write
|
|
48
|
+
cattri :raise_type_errors, true, expose: :read_write
|
|
49
|
+
cattri :enable_warnings, true, expose: :read_write
|
|
50
|
+
cattri :strict_by_default, true, expose: :read_write
|
|
75
51
|
|
|
76
52
|
# Initializes the configuration with default types and enforcement flags.
|
|
77
53
|
#
|
|
78
54
|
# @return [void]
|
|
79
55
|
def initialize
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
56
|
+
super
|
|
57
|
+
self.types = DEFAULT_TYPES.dup
|
|
58
|
+
self.enforce_typing = true
|
|
59
|
+
self.enforce_attribute_access = true
|
|
60
|
+
self.enforce_unwrapped_prefix = true
|
|
61
|
+
self.enforce_array_options = true
|
|
62
|
+
self.raise_type_errors = true
|
|
63
|
+
self.enable_warnings = true
|
|
64
|
+
self.strict_by_default = true
|
|
65
|
+
self.default_plugins = []
|
|
89
66
|
|
|
90
67
|
apply_type_aliases!
|
|
91
68
|
end
|
|
@@ -136,7 +113,7 @@ module Castkit
|
|
|
136
113
|
# @return [Castkit::Types::Base]
|
|
137
114
|
# @raise [Castkit::TypeError] if the type is not registered
|
|
138
115
|
def fetch_type(type)
|
|
139
|
-
|
|
116
|
+
types.fetch(type.to_sym) do
|
|
140
117
|
raise Castkit::TypeError, "Unknown type `#{type.inspect}`" if raise_type_errors
|
|
141
118
|
end
|
|
142
119
|
end
|
|
@@ -146,14 +123,14 @@ module Castkit
|
|
|
146
123
|
# @param type [Symbol]
|
|
147
124
|
# @return [Boolean]
|
|
148
125
|
def type_registered?(type)
|
|
149
|
-
|
|
126
|
+
types.key?(type.to_sym)
|
|
150
127
|
end
|
|
151
128
|
|
|
152
129
|
# Restores the type registry to its default state.
|
|
153
130
|
#
|
|
154
131
|
# @return [void]
|
|
155
132
|
def reset_types!
|
|
156
|
-
|
|
133
|
+
self.types = DEFAULT_TYPES.dup
|
|
157
134
|
apply_type_aliases!
|
|
158
135
|
end
|
|
159
136
|
|
|
@@ -38,6 +38,8 @@ module Castkit
|
|
|
38
38
|
].freeze
|
|
39
39
|
|
|
40
40
|
class << self
|
|
41
|
+
include Cattri
|
|
42
|
+
|
|
41
43
|
# Defines an attribute for the contract.
|
|
42
44
|
#
|
|
43
45
|
# Only a subset of options is allowed inside a contract.
|
|
@@ -71,15 +73,9 @@ module Castkit
|
|
|
71
73
|
Castkit::Contract::Result.new(definition[:name].to_s, input)
|
|
72
74
|
end
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def definition
|
|
78
|
-
@definition ||= {
|
|
79
|
-
name: :ephemeral,
|
|
80
|
-
attributes: {}
|
|
81
|
-
}
|
|
82
|
-
end
|
|
76
|
+
cattri :definition,
|
|
77
|
+
-> { { name: :ephemeral, attributes: {} } },
|
|
78
|
+
scope: :class, expose: :read_write
|
|
83
79
|
|
|
84
80
|
# Returns the defined attributes.
|
|
85
81
|
#
|
|
@@ -117,8 +113,8 @@ module Castkit
|
|
|
117
113
|
def define_from_source(name, source)
|
|
118
114
|
source_attributes = source.attributes.dup
|
|
119
115
|
|
|
120
|
-
|
|
121
|
-
name: name,
|
|
116
|
+
self.definition = {
|
|
117
|
+
name: name.to_sym,
|
|
122
118
|
attributes: source_attributes.transform_values do |attr|
|
|
123
119
|
Castkit::Attribute.new(attr.field, attr.type, **attr.options.slice(*ATTRIBUTE_OPTIONS))
|
|
124
120
|
end
|
|
@@ -131,7 +127,8 @@ module Castkit
|
|
|
131
127
|
# @yield [block]
|
|
132
128
|
# @return [void]
|
|
133
129
|
def define_from_block(name, &block)
|
|
134
|
-
|
|
130
|
+
contract_name = name ? name.to_sym : :ephemeral
|
|
131
|
+
self.definition = { name: contract_name, attributes: {} }
|
|
135
132
|
|
|
136
133
|
@__castkit_contract_dsl = true
|
|
137
134
|
instance_eval(&block)
|
|
@@ -7,14 +7,16 @@ module Castkit
|
|
|
7
7
|
# Provides access to the validation outcome, including whether it succeeded or failed,
|
|
8
8
|
# and includes the full list of errors if any.
|
|
9
9
|
class Result
|
|
10
|
+
include Cattri
|
|
11
|
+
|
|
10
12
|
# @return [Symbol] the name of the contract
|
|
11
|
-
|
|
13
|
+
cattri :contract, nil, expose: :read
|
|
12
14
|
|
|
13
15
|
# @return [Hash{Symbol => Object}] the validated input
|
|
14
|
-
|
|
16
|
+
cattri :input, nil, expose: :read
|
|
15
17
|
|
|
16
18
|
# @return [Hash{Symbol => Object}] the validation error hash
|
|
17
|
-
|
|
19
|
+
cattri :errors, {}, expose: :read
|
|
18
20
|
|
|
19
21
|
# Initializes a new result object.
|
|
20
22
|
#
|
|
@@ -22,9 +24,11 @@ module Castkit
|
|
|
22
24
|
# @param input [Hash{Symbol => Object}] the validated input
|
|
23
25
|
# @param errors [Hash{Symbol => Object}] the validation errors
|
|
24
26
|
def initialize(contract, input, errors: {})
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
super()
|
|
28
|
+
|
|
29
|
+
cattri_variable_set(:contract, contract.to_sym.freeze)
|
|
30
|
+
cattri_variable_set(:input, input.freeze)
|
|
31
|
+
cattri_variable_set(:errors, errors.freeze)
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
# A debug-friendly representation of the validation result.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "cattri"
|
|
4
|
+
|
|
3
5
|
module Castkit
|
|
4
6
|
module Core
|
|
5
7
|
# Provides DSL and implementation for declaring attributes within a Castkit::DataObject.
|
|
@@ -9,7 +11,11 @@ module Castkit
|
|
|
9
11
|
#
|
|
10
12
|
# This module is included into `Castkit::DataObject` and handles attribute registration,
|
|
11
13
|
# accessor generation, and typed writing behavior.
|
|
12
|
-
module Attributes
|
|
14
|
+
module Attributes # rubocop:disable Metrics/ModuleLength
|
|
15
|
+
def self.extended(base)
|
|
16
|
+
base.include(Cattri)
|
|
17
|
+
end
|
|
18
|
+
|
|
13
19
|
# Declares an attribute on the data object.
|
|
14
20
|
#
|
|
15
21
|
# Accepts either inline options or a reusable attribute definition (`using` or `definition`).
|
|
@@ -29,7 +35,7 @@ module Castkit
|
|
|
29
35
|
type, options = use_definition(field, definition || using&.definition, type, options)
|
|
30
36
|
return define_attribute(field, type, **options) unless options[:transient]
|
|
31
37
|
|
|
32
|
-
|
|
38
|
+
define_transient_accessor(field)
|
|
33
39
|
end
|
|
34
40
|
|
|
35
41
|
# Declares a composite (computed) attribute.
|
|
@@ -97,14 +103,14 @@ module Castkit
|
|
|
97
103
|
#
|
|
98
104
|
# @return [Hash{Symbol => Castkit::Attribute}]
|
|
99
105
|
def attributes
|
|
100
|
-
|
|
106
|
+
cattri_variable_memoize(:__castkit_attributes_registry) { {} }
|
|
101
107
|
end
|
|
102
108
|
|
|
103
109
|
def inherited(subclass)
|
|
104
110
|
super
|
|
105
111
|
|
|
106
|
-
parent_attributes =
|
|
107
|
-
subclass.
|
|
112
|
+
parent_attributes = cattri_variable_get(:__castkit_attributes_registry)
|
|
113
|
+
subclass.cattri_variable_set(:__castkit_attributes_registry, parent_attributes.dup) if parent_attributes
|
|
108
114
|
end
|
|
109
115
|
|
|
110
116
|
# Alias for {#attribute}
|
|
@@ -152,31 +158,27 @@ module Castkit
|
|
|
152
158
|
attribute = Castkit::Attribute.new(field, type, **options)
|
|
153
159
|
attributes[field] = attribute
|
|
154
160
|
|
|
155
|
-
|
|
156
|
-
attr_reader field
|
|
157
|
-
|
|
158
|
-
define_typed_writer(field, attribute)
|
|
159
|
-
elsif attribute.writeable?
|
|
160
|
-
define_typed_writer(field, attribute)
|
|
161
|
-
elsif attribute.readable?
|
|
162
|
-
attr_reader field
|
|
163
|
-
end
|
|
161
|
+
define_accessors(attribute)
|
|
164
162
|
end
|
|
165
163
|
|
|
166
|
-
#
|
|
164
|
+
# Creates readers/writers for a defined attribute using Cattri.
|
|
167
165
|
#
|
|
168
|
-
# @param field [Symbol]
|
|
169
166
|
# @param attribute [Castkit::Attribute]
|
|
170
167
|
# @return [void]
|
|
171
|
-
def
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
168
|
+
def define_accessors(attribute)
|
|
169
|
+
expose = exposure_for(attribute)
|
|
170
|
+
return if expose == :none
|
|
171
|
+
|
|
172
|
+
if attribute.writeable?
|
|
173
|
+
cattri(attribute.field, nil, expose: expose) do |value|
|
|
174
|
+
Castkit.type_caster(attribute.type.to_sym).call(
|
|
175
|
+
value,
|
|
176
|
+
options: attribute.options,
|
|
177
|
+
context: attribute.field
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
else
|
|
181
|
+
cattri(attribute.field, nil, expose: expose)
|
|
180
182
|
end
|
|
181
183
|
end
|
|
182
184
|
|
|
@@ -189,6 +191,7 @@ module Castkit
|
|
|
189
191
|
def with_access(access, options = {}, &block)
|
|
190
192
|
@__access_context = access
|
|
191
193
|
@__block_options = options
|
|
194
|
+
|
|
192
195
|
instance_eval(&block)
|
|
193
196
|
ensure
|
|
194
197
|
@__access_context = nil
|
|
@@ -204,6 +207,7 @@ module Castkit
|
|
|
204
207
|
def with_required(flag, options = {}, &block)
|
|
205
208
|
@__required_context = flag
|
|
206
209
|
@__block_options = options
|
|
210
|
+
|
|
207
211
|
instance_eval(&block)
|
|
208
212
|
ensure
|
|
209
213
|
@__required_context = nil
|
|
@@ -222,6 +226,26 @@ module Castkit
|
|
|
222
226
|
|
|
223
227
|
base.merge(options)
|
|
224
228
|
end
|
|
229
|
+
|
|
230
|
+
# Maps Castkit access flags onto Cattri's expose option.
|
|
231
|
+
#
|
|
232
|
+
# @param attribute [Castkit::Attribute]
|
|
233
|
+
# @return [Symbol]
|
|
234
|
+
def exposure_for(attribute)
|
|
235
|
+
return :read_write if attribute.full_access?
|
|
236
|
+
return :write if attribute.writeable?
|
|
237
|
+
return :read if attribute.readable?
|
|
238
|
+
|
|
239
|
+
:none
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Defines read/write accessors for transient attributes.
|
|
243
|
+
#
|
|
244
|
+
# @param field [Symbol]
|
|
245
|
+
# @return [void]
|
|
246
|
+
def define_transient_accessor(field)
|
|
247
|
+
cattri(field, nil, expose: :read_write)
|
|
248
|
+
end
|
|
225
249
|
end
|
|
226
250
|
end
|
|
227
251
|
end
|
data/lib/castkit/core/config.rb
CHANGED
|
@@ -5,6 +5,17 @@ module Castkit
|
|
|
5
5
|
# Provides per-class configuration for a Castkit::DataObject,
|
|
6
6
|
# including root key handling, strict mode, and unknown key behavior.
|
|
7
7
|
module Config
|
|
8
|
+
def self.extended(base)
|
|
9
|
+
super
|
|
10
|
+
|
|
11
|
+
base.include(Cattri) unless base.is_a?(Class) && base < Cattri
|
|
12
|
+
return unless base.respond_to?(:cattri)
|
|
13
|
+
|
|
14
|
+
base.cattri :strict_flag, nil, scope: :class, expose: :read_write
|
|
15
|
+
base.cattri :warn_on_unknown_flag, nil, scope: :class, expose: :read_write
|
|
16
|
+
base.cattri :allow_unknown_flag, nil, scope: :class, expose: :read_write
|
|
17
|
+
end
|
|
18
|
+
|
|
8
19
|
# Sets or retrieves strict mode behavior.
|
|
9
20
|
#
|
|
10
21
|
# In strict mode, unknown keys during deserialization raise errors. If unset, falls back
|
|
@@ -13,11 +24,9 @@ module Castkit
|
|
|
13
24
|
# @param value [Boolean, nil]
|
|
14
25
|
# @return [Boolean]
|
|
15
26
|
def strict(value = nil)
|
|
16
|
-
if value.nil?
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@strict = !!value
|
|
20
|
-
end
|
|
27
|
+
return (strict_flag.nil? ? Castkit.configuration.strict_by_default : strict_flag) if value.nil?
|
|
28
|
+
|
|
29
|
+
self.strict_flag = !!value
|
|
21
30
|
end
|
|
22
31
|
|
|
23
32
|
# Enables or disables ignoring unknown keys during deserialization.
|
|
@@ -27,7 +36,7 @@ module Castkit
|
|
|
27
36
|
# @param value [Boolean]
|
|
28
37
|
# @return [void]
|
|
29
38
|
def ignore_unknown(value = nil)
|
|
30
|
-
|
|
39
|
+
self.strict_flag = !value
|
|
31
40
|
end
|
|
32
41
|
|
|
33
42
|
# Sets or retrieves whether to emit warnings when unknown keys are encountered.
|
|
@@ -35,7 +44,7 @@ module Castkit
|
|
|
35
44
|
# @param value [Boolean, nil]
|
|
36
45
|
# @return [Boolean, nil]
|
|
37
46
|
def warn_on_unknown(value = nil)
|
|
38
|
-
value.nil? ?
|
|
47
|
+
value.nil? ? warn_on_unknown_flag : (self.warn_on_unknown_flag = value)
|
|
39
48
|
end
|
|
40
49
|
|
|
41
50
|
# Sets or retrieves whether to allow unknown keys when they are encountered.
|
|
@@ -43,7 +52,7 @@ module Castkit
|
|
|
43
52
|
# @param value [Boolean, nil]
|
|
44
53
|
# @return [Boolean, nil]
|
|
45
54
|
def allow_unknown(value = nil)
|
|
46
|
-
value.nil? ?
|
|
55
|
+
value.nil? ? allow_unknown_flag : (self.allow_unknown_flag = value)
|
|
47
56
|
end
|
|
48
57
|
|
|
49
58
|
# Returns a relaxed version of the current class with strict mode off.
|
|
@@ -63,11 +72,12 @@ module Castkit
|
|
|
63
72
|
#
|
|
64
73
|
# @return [Hash{Symbol => Boolean}]
|
|
65
74
|
def validation_rules
|
|
66
|
-
@validation_rules ||= {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
@validation_rules ||= {}
|
|
76
|
+
@validation_rules[:strict] = strict
|
|
77
|
+
@validation_rules[:allow_unknown] = allow_unknown
|
|
78
|
+
@validation_rules[:warn_on_unknown] = warn_on_unknown
|
|
79
|
+
|
|
80
|
+
@validation_rules
|
|
71
81
|
end
|
|
72
82
|
end
|
|
73
83
|
end
|
data/lib/castkit/data_object.rb
CHANGED
|
@@ -76,20 +76,23 @@ module Castkit
|
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
# @return [Hash{Symbol => Object}] The raw data provided during instantiation.
|
|
79
|
-
|
|
79
|
+
cattri :__raw, nil, expose: :read
|
|
80
80
|
|
|
81
81
|
# @return [Hash{Symbol => Object}] Undefined attributes provided during instantiation.
|
|
82
|
-
|
|
82
|
+
cattri :unknown_attributes, nil, expose: :read
|
|
83
83
|
|
|
84
84
|
# Initializes the DTO from a hash of attributes.
|
|
85
85
|
#
|
|
86
86
|
# @param data [Hash] raw input hash
|
|
87
87
|
# @raise [Castkit::DataObjectError] if strict mode is enabled and unknown keys are present
|
|
88
88
|
def initialize(data = {})
|
|
89
|
-
|
|
89
|
+
super()
|
|
90
|
+
|
|
91
|
+
cattri_variable_set(:__raw, data.dup.freeze)
|
|
90
92
|
data = unwrap_root(data)
|
|
91
93
|
|
|
92
|
-
|
|
94
|
+
cattri_variable_set(:unknown_attributes,
|
|
95
|
+
data.reject { |key, _| self.class.attributes.key?(key.to_sym) }.freeze)
|
|
93
96
|
|
|
94
97
|
validate_data!(data)
|
|
95
98
|
deserialize_attributes!(data)
|
|
@@ -49,7 +49,7 @@ module Castkit
|
|
|
49
49
|
next if value.nil? && attribute.optional?
|
|
50
50
|
|
|
51
51
|
value = deserialize_attribute_value!(attribute, value)
|
|
52
|
-
|
|
52
|
+
assign_attribute_value!(attribute, value)
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
|
|
@@ -115,6 +115,23 @@ module Castkit
|
|
|
115
115
|
nil
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
+
# Stores a deserialized value using Cattri's internal store when available.
|
|
119
|
+
#
|
|
120
|
+
# @param attribute [Castkit::Attribute]
|
|
121
|
+
# @param value [Object]
|
|
122
|
+
# @return [void]
|
|
123
|
+
def assign_attribute_value!(attribute, value)
|
|
124
|
+
if respond_to?(:cattri_variable_set, true)
|
|
125
|
+
cattri_variable_set(
|
|
126
|
+
attribute.field,
|
|
127
|
+
value,
|
|
128
|
+
final: attribute.options[:final]
|
|
129
|
+
)
|
|
130
|
+
else
|
|
131
|
+
instance_variable_set("@#{attribute.field}", value)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
118
135
|
# Resolves root-wrapped and unwrapped data.
|
|
119
136
|
#
|
|
120
137
|
# @param data [Hash]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Castkit
|
|
4
|
+
module DSL
|
|
5
|
+
module DataObject
|
|
6
|
+
# Provides opt-in attribute introspection for data objects using Cattri's registry
|
|
7
|
+
# without overriding Castkit's attribute DSL.
|
|
8
|
+
module Introspection
|
|
9
|
+
# Enables introspection helpers on the including class.
|
|
10
|
+
#
|
|
11
|
+
# @return [void]
|
|
12
|
+
def enable_cattri_introspection!
|
|
13
|
+
extend IntrospectionHelpers
|
|
14
|
+
|
|
15
|
+
@cattri_attribute_registry = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Class-level helpers that read from Cattri's attribute registry but do not
|
|
19
|
+
# override Castkit's attribute builder.
|
|
20
|
+
module IntrospectionHelpers
|
|
21
|
+
def attribute_defined?(name)
|
|
22
|
+
!!cattri_attribute(name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def attribute_definitions(with_ancestors: false)
|
|
26
|
+
cattri_attribute_registry.defined_attributes(with_ancestors: with_ancestors)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def attribute_methods
|
|
30
|
+
cattri_attribute_registry.defined_attributes(with_ancestors: true).transform_values do |attribute|
|
|
31
|
+
Set.new(attribute.allowed_methods)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def attribute_source(name)
|
|
36
|
+
cattri_attribute(name)&.defined_in
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def cattri_attribute_registry
|
|
42
|
+
@cattri_attribute_registry ||= attribute_registry
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def cattri_attribute(name)
|
|
46
|
+
cattri_attribute_registry.defined_attributes(with_ancestors: true)[name.to_sym]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -38,7 +38,10 @@ module Castkit
|
|
|
38
38
|
# @param value [Boolean, nil]
|
|
39
39
|
# @return [Boolean]
|
|
40
40
|
def ignore_blank(value = nil)
|
|
41
|
-
@ignore_blank = value.nil?
|
|
41
|
+
return (@ignore_blank = true) if value.nil? && !defined?(@ignore_blank)
|
|
42
|
+
return @ignore_blank if value.nil?
|
|
43
|
+
|
|
44
|
+
@ignore_blank = value
|
|
42
45
|
end
|
|
43
46
|
end
|
|
44
47
|
|
|
@@ -7,6 +7,7 @@ require_relative "data_object/contract"
|
|
|
7
7
|
require_relative "data_object/plugins"
|
|
8
8
|
require_relative "data_object/serialization"
|
|
9
9
|
require_relative "data_object/deserialization"
|
|
10
|
+
require_relative "data_object/introspection"
|
|
10
11
|
|
|
11
12
|
module Castkit
|
|
12
13
|
module DSL
|
|
@@ -47,11 +48,14 @@ module Castkit
|
|
|
47
48
|
# @param base [Class] the including class
|
|
48
49
|
# @return [void]
|
|
49
50
|
def self.included(base)
|
|
51
|
+
base.include(Cattri)
|
|
52
|
+
|
|
50
53
|
base.extend(Castkit::Core::Config)
|
|
51
54
|
base.extend(Castkit::Core::Attributes)
|
|
52
55
|
base.extend(Castkit::Core::AttributeTypes)
|
|
53
56
|
base.extend(Castkit::DSL::DataObject::Contract)
|
|
54
57
|
base.extend(Castkit::DSL::DataObject::Plugins)
|
|
58
|
+
base.extend(Castkit::DSL::DataObject::Introspection)
|
|
55
59
|
|
|
56
60
|
base.include(Castkit::DSL::DataObject::Serialization)
|
|
57
61
|
base.include(Castkit::DSL::DataObject::Deserialization)
|
data/lib/castkit/error.rb
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
module Castkit
|
|
4
4
|
# Base error class for all Castkit-related exceptions.
|
|
5
5
|
class Error < StandardError
|
|
6
|
+
include Cattri
|
|
7
|
+
|
|
6
8
|
# @return [Hash, Object, nil] contextual data to aid in debugging
|
|
7
|
-
|
|
9
|
+
cattri :context, nil, expose: :read
|
|
8
10
|
|
|
9
11
|
# Initializes a Castkit error.
|
|
10
12
|
#
|
|
@@ -12,7 +14,8 @@ module Castkit
|
|
|
12
14
|
# @param context [Object, String, nil] optional data object or hash for context
|
|
13
15
|
def initialize(msg, context: nil)
|
|
14
16
|
super(msg)
|
|
15
|
-
|
|
17
|
+
|
|
18
|
+
cattri_variable_set(:context, context, final: true)
|
|
16
19
|
end
|
|
17
20
|
end
|
|
18
21
|
|
|
@@ -44,11 +47,12 @@ module Castkit
|
|
|
44
47
|
|
|
45
48
|
# Raised during contract validation.
|
|
46
49
|
class ContractError < Error
|
|
47
|
-
|
|
50
|
+
cattri :errors, {}, expose: :read
|
|
48
51
|
|
|
49
52
|
def initialize(msg, context: nil, errors: nil)
|
|
50
53
|
super(msg, context: context)
|
|
51
|
-
|
|
54
|
+
|
|
55
|
+
cattri_variable_set(:errors, errors || {})
|
|
52
56
|
end
|
|
53
57
|
end
|
|
54
58
|
end
|
data/lib/castkit/plugins.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "cattri"
|
|
4
|
+
|
|
3
5
|
module Castkit
|
|
4
6
|
# Internal registry for Castkit plugin modules.
|
|
5
7
|
#
|
|
@@ -19,9 +21,16 @@ module Castkit
|
|
|
19
21
|
# enable_plugins :custom, :oj
|
|
20
22
|
# end
|
|
21
23
|
module Plugins
|
|
22
|
-
|
|
24
|
+
include Cattri
|
|
23
25
|
|
|
24
26
|
class << self
|
|
27
|
+
include Cattri
|
|
28
|
+
extend Cattri::Dsl
|
|
29
|
+
extend Cattri::ClassMethods
|
|
30
|
+
extend Cattri::Visibility
|
|
31
|
+
|
|
32
|
+
cattri :registered_plugins, {}, expose: :read_write
|
|
33
|
+
|
|
25
34
|
# Activates one or more plugins on the given class.
|
|
26
35
|
#
|
|
27
36
|
# Each plugin module is included into the class. If the module responds to `setup!`,
|
|
@@ -58,7 +67,7 @@ module Castkit
|
|
|
58
67
|
# @return [Module] the plugin module
|
|
59
68
|
# @raise [Castkit::Error] if no plugin is found
|
|
60
69
|
def lookup!(name)
|
|
61
|
-
|
|
70
|
+
registered_plugins[name.to_sym] ||
|
|
62
71
|
const_get(Castkit::Inflector.pascalize(name.to_s), false)
|
|
63
72
|
rescue NameError
|
|
64
73
|
raise Castkit::Error,
|
|
@@ -75,7 +84,7 @@ module Castkit
|
|
|
75
84
|
# @param plugin [Module] the plugin module to register
|
|
76
85
|
# @return [void]
|
|
77
86
|
def register(name, plugin)
|
|
78
|
-
|
|
87
|
+
registered_plugins[name.to_sym] = plugin
|
|
79
88
|
end
|
|
80
89
|
end
|
|
81
90
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "set"
|
|
4
|
+
require "cattri"
|
|
4
5
|
|
|
5
6
|
module Castkit
|
|
6
7
|
module Serializers
|
|
@@ -21,6 +22,8 @@ module Castkit
|
|
|
21
22
|
#
|
|
22
23
|
# CustomSerializer.call(user_dto)
|
|
23
24
|
class Base
|
|
25
|
+
include Cattri
|
|
26
|
+
|
|
24
27
|
class << self
|
|
25
28
|
# Entrypoint for serializing an object.
|
|
26
29
|
#
|
|
@@ -33,7 +36,7 @@ module Castkit
|
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
# @return [Castkit::DataObject] the object being serialized
|
|
36
|
-
|
|
39
|
+
cattri :object, nil, expose: :read
|
|
37
40
|
|
|
38
41
|
protected
|
|
39
42
|
|
|
@@ -47,15 +50,17 @@ module Castkit
|
|
|
47
50
|
private
|
|
48
51
|
|
|
49
52
|
# @return [Set<Integer>] a set of visited object IDs to detect circular references
|
|
50
|
-
|
|
53
|
+
cattri :visited, nil, expose: :read
|
|
51
54
|
|
|
52
55
|
# Initializes the serializer instance.
|
|
53
56
|
#
|
|
54
57
|
# @param object [Castkit::DataObject]
|
|
55
58
|
# @param visited [Set, nil]
|
|
56
59
|
def initialize(object, visited: nil)
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
super()
|
|
61
|
+
|
|
62
|
+
cattri_variable_set(:object, object)
|
|
63
|
+
cattri_variable_set(:visited, visited || Set.new)
|
|
59
64
|
end
|
|
60
65
|
|
|
61
66
|
# Subclasses must override this method to implement serialization logic.
|
|
@@ -11,13 +11,13 @@ module Castkit
|
|
|
11
11
|
# and respects the class-level serialization configuration.
|
|
12
12
|
class DefaultSerializer < Castkit::Serializers::Base
|
|
13
13
|
# @return [Hash{Symbol => Castkit::Attribute}] the attributes to serialize
|
|
14
|
-
|
|
14
|
+
cattri :attributes, nil, expose: :read
|
|
15
15
|
|
|
16
16
|
# @return [Hash{Symbol => Object}] unrecognized attributes captured during deserialization
|
|
17
|
-
|
|
17
|
+
cattri :unknown_attributes, nil, expose: :read
|
|
18
18
|
|
|
19
19
|
# @return [Hash] serialization config flags like :root, :ignore_nil, :allow_unknown
|
|
20
|
-
|
|
20
|
+
cattri :options, nil, expose: :read
|
|
21
21
|
|
|
22
22
|
# Serializes the object to a hash.
|
|
23
23
|
#
|
|
@@ -41,13 +41,13 @@ module Castkit
|
|
|
41
41
|
super
|
|
42
42
|
|
|
43
43
|
@skip_flag = "__castkit_#{object.object_id}"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
cattri_variable_set(:attributes, object.class.attributes.freeze)
|
|
45
|
+
cattri_variable_set(:unknown_attributes, object.unknown_attributes.freeze)
|
|
46
|
+
cattri_variable_set(:options, {
|
|
47
|
+
root: object.class.root,
|
|
48
|
+
ignore_nil: object.class.ignore_nil || false,
|
|
49
|
+
allow_unknown: object.class.allow_unknown || false
|
|
50
|
+
})
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# Serializes all defined attributes.
|
|
@@ -17,11 +17,11 @@ module Castkit
|
|
|
17
17
|
# Validates that the value is an Array.
|
|
18
18
|
#
|
|
19
19
|
# @param value [Object] the value to validate
|
|
20
|
-
# @param
|
|
20
|
+
# @param options [Hash] unused, for interface consistency
|
|
21
21
|
# @param context [Symbol, String] the field or context for error messaging
|
|
22
22
|
# @return [void]
|
|
23
23
|
# @raise [Castkit::AttributeError] if value is not an Array
|
|
24
|
-
def call(value,
|
|
24
|
+
def call(value, options:, context:) # rubocop:disable Lint/UnusedMethodArgument
|
|
25
25
|
type_error!(:array, value, context: context) unless value.is_a?(::Array)
|
|
26
26
|
end
|
|
27
27
|
end
|
data/lib/castkit/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: castkit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nathan Lucas
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-12-
|
|
11
|
+
date: 2025-12-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: cattri
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 0.2.3
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 0.2.3
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: thor
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -80,6 +94,20 @@ dependencies:
|
|
|
80
94
|
- - ">="
|
|
81
95
|
- !ruby/object:Gem::Version
|
|
82
96
|
version: '0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: simplecov-html
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
83
111
|
- !ruby/object:Gem::Dependency
|
|
84
112
|
name: yard
|
|
85
113
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -138,6 +166,7 @@ files:
|
|
|
138
166
|
- lib/castkit/dsl/data_object.rb
|
|
139
167
|
- lib/castkit/dsl/data_object/contract.rb
|
|
140
168
|
- lib/castkit/dsl/data_object/deserialization.rb
|
|
169
|
+
- lib/castkit/dsl/data_object/introspection.rb
|
|
141
170
|
- lib/castkit/dsl/data_object/plugins.rb
|
|
142
171
|
- lib/castkit/dsl/data_object/serialization.rb
|
|
143
172
|
- lib/castkit/error.rb
|