castkit 0.3.0 → 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 +44 -0
- data/README.md +19 -11
- data/castkit.gemspec +4 -0
- data/lib/castkit/attribute.rb +87 -65
- data/lib/castkit/attributes/definition.rb +64 -0
- data/lib/castkit/attributes/options.rb +214 -0
- data/lib/castkit/castkit.rb +14 -3
- data/lib/castkit/cli/generate.rb +14 -0
- data/lib/castkit/configuration.rb +25 -48
- data/lib/castkit/contract/base.rb +8 -23
- data/lib/castkit/contract/result.rb +10 -6
- data/lib/castkit/contract/validator.rb +5 -1
- data/lib/castkit/core/attribute_types.rb +3 -1
- data/lib/castkit/core/attributes.rb +132 -65
- data/lib/castkit/core/config.rb +23 -13
- data/lib/castkit/data_object.rb +9 -29
- 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 +24 -3
- data/lib/castkit/dsl/data_object/introspection.rb +52 -0
- data/lib/castkit/{ext → dsl}/data_object/plugins.rb +1 -1
- data/lib/castkit/{ext → dsl}/data_object/serialization.rb +5 -2
- data/lib/castkit/dsl/data_object.rb +65 -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/types/base.rb +24 -3
- data/lib/castkit/validators/boolean_validator.rb +3 -3
- data/lib/castkit/validators/collection_validator.rb +2 -2
- 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 +74 -12
- data/.rspec_status +0 -195
- data/lib/castkit/core/registerable.rb +0 -59
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?
|
|
@@ -99,20 +76,23 @@ module Castkit
|
|
|
99
76
|
end
|
|
100
77
|
|
|
101
78
|
# @return [Hash{Symbol => Object}] The raw data provided during instantiation.
|
|
102
|
-
|
|
79
|
+
cattri :__raw, nil, expose: :read
|
|
103
80
|
|
|
104
81
|
# @return [Hash{Symbol => Object}] Undefined attributes provided during instantiation.
|
|
105
|
-
|
|
82
|
+
cattri :unknown_attributes, nil, expose: :read
|
|
106
83
|
|
|
107
84
|
# Initializes the DTO from a hash of attributes.
|
|
108
85
|
#
|
|
109
86
|
# @param data [Hash] raw input hash
|
|
110
87
|
# @raise [Castkit::DataObjectError] if strict mode is enabled and unknown keys are present
|
|
111
88
|
def initialize(data = {})
|
|
112
|
-
|
|
89
|
+
super()
|
|
90
|
+
|
|
91
|
+
cattri_variable_set(:__raw, data.dup.freeze)
|
|
113
92
|
data = unwrap_root(data)
|
|
114
93
|
|
|
115
|
-
|
|
94
|
+
cattri_variable_set(:unknown_attributes,
|
|
95
|
+
data.reject { |key, _| self.class.attributes.key?(key.to_sym) }.freeze)
|
|
116
96
|
|
|
117
97
|
validate_data!(data)
|
|
118
98
|
deserialize_attributes!(data)
|
|
@@ -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
|
#
|
|
@@ -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
|
|
|
@@ -104,13 +104,34 @@ 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
|
|
|
111
115
|
nil
|
|
112
116
|
end
|
|
113
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
|
+
|
|
114
135
|
# Resolves root-wrapped and unwrapped data.
|
|
115
136
|
#
|
|
116
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
|
|
@@ -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
|
# Provides per-class serialization configuration for Castkit::Dataobject, including
|
|
7
7
|
# root key handling and ignore rules.
|
|
@@ -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
|
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
require_relative "data_object/introspection"
|
|
11
|
+
|
|
12
|
+
module Castkit
|
|
13
|
+
module DSL
|
|
14
|
+
# Provides the complete DSL used by Castkit data objects.
|
|
15
|
+
#
|
|
16
|
+
# This module can be included into any class to make it behave like a `Castkit::DataObject`
|
|
17
|
+
# without requiring subclassing. It wires in the full attribute DSL, type system, contract support,
|
|
18
|
+
# plugin lifecycle, and (de)serialization logic.
|
|
19
|
+
#
|
|
20
|
+
# This is what powers `Castkit::DataObject` internally, and is intended for advanced use
|
|
21
|
+
# cases where composition is preferred over inheritance.
|
|
22
|
+
#
|
|
23
|
+
# When included, this module:
|
|
24
|
+
#
|
|
25
|
+
# - `extend`s:
|
|
26
|
+
# - {Castkit::Core::Config} – configuration and context behavior
|
|
27
|
+
# - {Castkit::Core::Attributes} – the DSL for declaring attributes
|
|
28
|
+
# - {Castkit::Core::AttributeTypes} – support for custom type resolution
|
|
29
|
+
# - {Castkit::DSL::DataObject::Contract} – validation contract hooks
|
|
30
|
+
# - {Castkit::DSL::DataObject::Plugins} – plugin hooks and lifecycle events
|
|
31
|
+
#
|
|
32
|
+
# - `include`s:
|
|
33
|
+
# - {Castkit::DSL::DataObject::Serialization} – `#to_h`, `#as_json`, etc.
|
|
34
|
+
# - {Castkit::DSL::DataObject::Deserialization} – `from_h`, `from_json`, etc.
|
|
35
|
+
#
|
|
36
|
+
# @example Including in a custom data object
|
|
37
|
+
# class MyObject
|
|
38
|
+
# include Castkit::DSL::DataObject
|
|
39
|
+
#
|
|
40
|
+
# string :id
|
|
41
|
+
# boolean :active, default: true
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# @see Castkit::DataObject for the default implementation
|
|
45
|
+
module DataObject
|
|
46
|
+
# Hook triggered when the module is included.
|
|
47
|
+
#
|
|
48
|
+
# @param base [Class] the including class
|
|
49
|
+
# @return [void]
|
|
50
|
+
def self.included(base)
|
|
51
|
+
base.include(Cattri)
|
|
52
|
+
|
|
53
|
+
base.extend(Castkit::Core::Config)
|
|
54
|
+
base.extend(Castkit::Core::Attributes)
|
|
55
|
+
base.extend(Castkit::Core::AttributeTypes)
|
|
56
|
+
base.extend(Castkit::DSL::DataObject::Contract)
|
|
57
|
+
base.extend(Castkit::DSL::DataObject::Plugins)
|
|
58
|
+
base.extend(Castkit::DSL::DataObject::Introspection)
|
|
59
|
+
|
|
60
|
+
base.include(Castkit::DSL::DataObject::Serialization)
|
|
61
|
+
base.include(Castkit::DSL::DataObject::Deserialization)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
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.
|
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
|
|
@@ -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
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
|
#
|