castkit 0.2.0 → 0.3.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/.rspec_status +118 -119
- data/CHANGELOG.md +1 -1
- data/README.md +287 -11
- data/castkit.gemspec +1 -0
- data/lib/castkit/castkit.rb +5 -2
- data/lib/castkit/cli/generate.rb +98 -0
- data/lib/castkit/cli/list.rb +200 -0
- data/lib/castkit/cli/main.rb +43 -0
- data/lib/castkit/cli.rb +24 -0
- data/lib/castkit/configuration.rb +31 -8
- data/lib/castkit/contract/{generic.rb → base.rb} +5 -5
- data/lib/castkit/contract/result.rb +2 -2
- data/lib/castkit/contract.rb +5 -5
- data/lib/castkit/data_object.rb +11 -7
- data/lib/castkit/ext/data_object/contract.rb +1 -1
- data/lib/castkit/ext/data_object/plugins.rb +86 -0
- data/lib/castkit/inflector.rb +1 -1
- data/lib/castkit/plugins.rb +82 -0
- data/lib/castkit/serializers/base.rb +94 -0
- data/lib/castkit/serializers/default_serializer.rb +156 -0
- data/lib/castkit/types/{generic.rb → base.rb} +6 -7
- data/lib/castkit/types/boolean.rb +14 -10
- data/lib/castkit/types/collection.rb +13 -2
- data/lib/castkit/types/date.rb +2 -2
- data/lib/castkit/types/date_time.rb +2 -2
- data/lib/castkit/types/float.rb +5 -5
- data/lib/castkit/types/integer.rb +5 -5
- data/lib/castkit/types/string.rb +2 -2
- data/lib/castkit/types.rb +1 -1
- data/lib/castkit/validators/base.rb +59 -0
- data/lib/castkit/validators/boolean_validator.rb +39 -0
- data/lib/castkit/validators/collection_validator.rb +29 -0
- data/lib/castkit/validators/float_validator.rb +31 -0
- data/lib/castkit/validators/integer_validator.rb +31 -0
- data/lib/castkit/validators/numeric_validator.rb +2 -2
- data/lib/castkit/validators/string_validator.rb +3 -4
- data/lib/castkit/version.rb +1 -1
- data/lib/generators/base.rb +97 -0
- data/lib/generators/contract.rb +68 -0
- data/lib/generators/data_object.rb +48 -0
- data/lib/generators/plugin.rb +25 -0
- data/lib/generators/serializer.rb +28 -0
- data/lib/generators/templates/contract.rb.tt +24 -0
- data/lib/generators/templates/contract_spec.rb.tt +76 -0
- data/lib/generators/templates/data_object.rb.tt +15 -0
- data/lib/generators/templates/data_object_spec.rb.tt +36 -0
- data/lib/generators/templates/plugin.rb.tt +37 -0
- data/lib/generators/templates/plugin_spec.rb.tt +18 -0
- data/lib/generators/templates/serializer.rb.tt +24 -0
- data/lib/generators/templates/serializer_spec.rb.tt +14 -0
- data/lib/generators/templates/type.rb.tt +55 -0
- data/lib/generators/templates/type_spec.rb.tt +42 -0
- data/lib/generators/templates/validator.rb.tt +26 -0
- data/lib/generators/templates/validator_spec.rb.tt +23 -0
- data/lib/generators/type.rb +29 -0
- data/lib/generators/validator.rb +41 -0
- metadata +50 -7
- data/lib/castkit/default_serializer.rb +0 -154
- data/lib/castkit/serializer.rb +0 -92
- data/lib/castkit/validators/base_validator.rb +0 -39
data/lib/castkit/cli.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
require_relative "cli/main"
|
5
|
+
|
6
|
+
module Castkit
|
7
|
+
# Entrypoint for Castkit’s command-line interface.
|
8
|
+
#
|
9
|
+
# Delegates to the `Castkit::CLI::Main` Thor class, which defines all CLI commands.
|
10
|
+
#
|
11
|
+
# @example Executing from a binstub
|
12
|
+
# Castkit::CLI.start(ARGV)
|
13
|
+
#
|
14
|
+
module CLI
|
15
|
+
# Starts the Castkit CLI.
|
16
|
+
#
|
17
|
+
# @param args [Array<String>] the command-line arguments
|
18
|
+
# @param kwargs [Hash] additional keyword arguments passed to Thor
|
19
|
+
# @return [void]
|
20
|
+
def self.start(*args, **kwargs)
|
21
|
+
Castkit::CLI::Main.start(*args, **kwargs)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -10,14 +10,14 @@ module Castkit
|
|
10
10
|
class Configuration
|
11
11
|
# Default mapping of primitive type definitions.
|
12
12
|
#
|
13
|
-
# @return [Hash{Symbol => Castkit::Types::
|
13
|
+
# @return [Hash{Symbol => Castkit::Types::Base}]
|
14
14
|
DEFAULT_TYPES = {
|
15
15
|
array: Castkit::Types::Collection.new,
|
16
16
|
boolean: Castkit::Types::Boolean.new,
|
17
17
|
date: Castkit::Types::Date.new,
|
18
18
|
datetime: Castkit::Types::DateTime.new,
|
19
19
|
float: Castkit::Types::Float.new,
|
20
|
-
hash: Castkit::Types::
|
20
|
+
hash: Castkit::Types::Base.new,
|
21
21
|
integer: Castkit::Types::Integer.new,
|
22
22
|
string: Castkit::Types::String.new
|
23
23
|
}.freeze
|
@@ -36,9 +36,15 @@ module Castkit
|
|
36
36
|
uuid: :string
|
37
37
|
}.freeze
|
38
38
|
|
39
|
-
# @return [Hash{Symbol => Castkit::Types::
|
39
|
+
# @return [Hash{Symbol => Castkit::Types::Base}] registered types
|
40
40
|
attr_reader :types
|
41
41
|
|
42
|
+
# Set default plugins that will be used globally in all Castkit::DataObject subclasses.
|
43
|
+
# This is equivalent to calling `enable_plugins` in every class.
|
44
|
+
#
|
45
|
+
# @return [Array<Symbol>] default plugin names to be applied to all DataObject subclasses
|
46
|
+
attr_accessor :default_plugins
|
47
|
+
|
42
48
|
# Whether to raise an error if values should be validated before deserializing, e.g. true -> "true"
|
43
49
|
# @return [Boolean]
|
44
50
|
attr_accessor :enforce_typing
|
@@ -79,6 +85,7 @@ module Castkit
|
|
79
85
|
@raise_type_errors = true
|
80
86
|
@enable_warnings = true
|
81
87
|
@strict_by_default = true
|
88
|
+
@default_plugins = []
|
82
89
|
|
83
90
|
apply_type_aliases!
|
84
91
|
end
|
@@ -86,17 +93,17 @@ module Castkit
|
|
86
93
|
# Registers a new type definition.
|
87
94
|
#
|
88
95
|
# @param type [Symbol] the symbolic type name (e.g., :uuid)
|
89
|
-
# @param klass [Class<Castkit::Types::
|
96
|
+
# @param klass [Class<Castkit::Types::Base>] the class to register
|
90
97
|
# @param override [Boolean] whether to allow overwriting existing registration
|
91
|
-
# @raise [Castkit::TypeError] if the type class is invalid or not a subclass of
|
98
|
+
# @raise [Castkit::TypeError] if the type class is invalid or not a subclass of Castkit::Types::Base
|
92
99
|
# @return [void]
|
93
100
|
def register_type(type, klass, aliases: [], override: false)
|
94
101
|
type = type.to_sym
|
95
102
|
return if types.key?(type) && !override
|
96
103
|
|
97
104
|
instance = klass.new
|
98
|
-
unless instance.is_a?(Castkit::Types::
|
99
|
-
raise Castkit::TypeError, "Expected subclass of Castkit::Types::
|
105
|
+
unless instance.is_a?(Castkit::Types::Base)
|
106
|
+
raise Castkit::TypeError, "Expected subclass of Castkit::Types::Base for `#{type}`"
|
100
107
|
end
|
101
108
|
|
102
109
|
types[type] = instance
|
@@ -107,10 +114,26 @@ module Castkit
|
|
107
114
|
aliases.each { |alias_type| register_type(alias_type, klass, override: override) }
|
108
115
|
end
|
109
116
|
|
117
|
+
# Register a custom plugin for use with Castkit::DataObject.
|
118
|
+
#
|
119
|
+
# @example Loading as a default plugin
|
120
|
+
# Castkit.configure do |config|
|
121
|
+
# config.register_plugin(:custom, CustomPlugin)
|
122
|
+
# config.default_plugins [:custom]
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# @example Loading it directly in a Castkit::DataObject
|
126
|
+
# class UserDto < Castkit::DataObject
|
127
|
+
# enable_plugins :custom
|
128
|
+
# end
|
129
|
+
def register_plugin(name, plugin)
|
130
|
+
Castkit::Plugins.register(name, plugin)
|
131
|
+
end
|
132
|
+
|
110
133
|
# Returns the type handler for a given type symbol.
|
111
134
|
#
|
112
135
|
# @param type [Symbol]
|
113
|
-
# @return [Castkit::Types::
|
136
|
+
# @return [Castkit::Types::Base]
|
114
137
|
# @raise [Castkit::TypeError] if the type is not registered
|
115
138
|
def fetch_type(type)
|
116
139
|
@types.fetch(type.to_sym) do
|
@@ -14,7 +14,7 @@ module Castkit
|
|
14
14
|
# ephemeral or reusable contract classes.
|
15
15
|
#
|
16
16
|
# @example Subclassing directly
|
17
|
-
# class MyContract < Castkit::Contract::
|
17
|
+
# class MyContract < Castkit::Contract::Base
|
18
18
|
# string :id
|
19
19
|
# integer :count, required: false
|
20
20
|
# end
|
@@ -30,7 +30,7 @@ module Castkit
|
|
30
30
|
# UserContract.validate!(id: "123")
|
31
31
|
#
|
32
32
|
# @see Castkit::Contract.build
|
33
|
-
class
|
33
|
+
class Base
|
34
34
|
extend Castkit::Core::Config
|
35
35
|
extend Castkit::Core::AttributeTypes
|
36
36
|
extend Castkit::Core::Registerable
|
@@ -69,7 +69,6 @@ module Castkit
|
|
69
69
|
# @return [Castkit::Contract::Result]
|
70
70
|
def validate(input)
|
71
71
|
validate!(input)
|
72
|
-
Castkit::Contract::Result.new(definition[:name].to_s, input)
|
73
72
|
rescue Castkit::ContractError => e
|
74
73
|
Castkit::Contract::Result.new(definition[:name].to_s, input, errors: e.errors)
|
75
74
|
end
|
@@ -81,6 +80,7 @@ module Castkit
|
|
81
80
|
# @return [void]
|
82
81
|
def validate!(input)
|
83
82
|
Castkit::Contract::Validator.call!(attributes.values, input, **validation_rules)
|
83
|
+
Castkit::Contract::Result.new(definition[:name].to_s, input)
|
84
84
|
end
|
85
85
|
|
86
86
|
# Returns internal contract metadata.
|
@@ -88,7 +88,7 @@ module Castkit
|
|
88
88
|
# @return [Hash]
|
89
89
|
def definition
|
90
90
|
@definition ||= {
|
91
|
-
name: :
|
91
|
+
name: :ephemeral,
|
92
92
|
attributes: {}
|
93
93
|
}
|
94
94
|
end
|
@@ -108,7 +108,7 @@ module Castkit
|
|
108
108
|
# @param source [Castkit::DataObject, nil]
|
109
109
|
# @param block [Proc, nil]
|
110
110
|
# @return [Hash]
|
111
|
-
def define(name = :
|
111
|
+
def define(name = :ephemeral, source = nil, validation_rules: {}, &block)
|
112
112
|
validate_definition!(source, &block)
|
113
113
|
|
114
114
|
if source
|
@@ -52,10 +52,10 @@ module Castkit
|
|
52
52
|
#
|
53
53
|
# @return [String]
|
54
54
|
def to_s
|
55
|
-
return "[Castkit] Contract validation passed for #{contract
|
55
|
+
return "[Castkit] Contract validation passed for #{contract}" if success?
|
56
56
|
|
57
57
|
parsed_errors = errors.map { |k, v| " #{k}: #{v.inspect}" }.join("\n")
|
58
|
-
"[Castkit] Contract validation failed for #{contract
|
58
|
+
"[Castkit] Contract validation failed for #{contract}:\n#{parsed_errors}"
|
59
59
|
end
|
60
60
|
|
61
61
|
# @return [Hash{Symbol => Object}] the input validation and error hash
|
data/lib/castkit/contract.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "contract/
|
3
|
+
require_relative "contract/base"
|
4
4
|
|
5
5
|
module Castkit
|
6
6
|
# Castkit::Contract provides a lightweight mechanism for defining and validating
|
@@ -31,9 +31,9 @@ module Castkit
|
|
31
31
|
# @param name [String, Symbol, nil] Optional name for the contract.
|
32
32
|
# @param validation_rules [Hash] Optional validation rules (e.g., `strict: true`).
|
33
33
|
# @yield Optional DSL block to define attributes.
|
34
|
-
# @return [Class<Castkit::Contract::
|
34
|
+
# @return [Class<Castkit::Contract::Base>]
|
35
35
|
def build(name = nil, **validation_rules, &block)
|
36
|
-
klass = Class.new(Castkit::Contract::
|
36
|
+
klass = Class.new(Castkit::Contract::Base)
|
37
37
|
klass.send(:define, name, nil, validation_rules: validation_rules, &block)
|
38
38
|
|
39
39
|
klass
|
@@ -52,12 +52,12 @@ module Castkit
|
|
52
52
|
#
|
53
53
|
# @param source [Class<Castkit::DataObject>] the DataObject to generate the contract from
|
54
54
|
# @param as [String, Symbol, nil] Optional custom name to use for the contract
|
55
|
-
# @return [Class<Castkit::Contract::
|
55
|
+
# @return [Class<Castkit::Contract::Base>]
|
56
56
|
def from_dataobject(source, as: nil)
|
57
57
|
name = as || Castkit::Inflector.unqualified_name(source)
|
58
58
|
name = Castkit::Inflector.underscore(name).to_sym
|
59
59
|
|
60
|
-
klass = Class.new(Castkit::Contract::
|
60
|
+
klass = Class.new(Castkit::Contract::Base)
|
61
61
|
klass.send(:define, name, source, validation_rules: source.validation_rules)
|
62
62
|
|
63
63
|
klass
|
data/lib/castkit/data_object.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
require "json"
|
4
4
|
require_relative "error"
|
5
5
|
require_relative "attribute"
|
6
|
-
require_relative "default_serializer"
|
6
|
+
require_relative "serializers/default_serializer"
|
7
7
|
require_relative "contract/validator"
|
8
8
|
require_relative "core/config"
|
9
9
|
require_relative "core/attributes"
|
@@ -11,6 +11,7 @@ require_relative "core/attribute_types"
|
|
11
11
|
require_relative "core/registerable"
|
12
12
|
require_relative "ext/data_object/contract"
|
13
13
|
require_relative "ext/data_object/deserialization"
|
14
|
+
require_relative "ext/data_object/plugins"
|
14
15
|
require_relative "ext/data_object/serialization"
|
15
16
|
|
16
17
|
module Castkit
|
@@ -34,6 +35,7 @@ module Castkit
|
|
34
35
|
extend Castkit::Core::AttributeTypes
|
35
36
|
extend Castkit::Core::Registerable
|
36
37
|
extend Castkit::Ext::DataObject::Contract
|
38
|
+
extend Castkit::Ext::DataObject::Plugins
|
37
39
|
|
38
40
|
include Castkit::Ext::DataObject::Serialization
|
39
41
|
include Castkit::Ext::DataObject::Deserialization
|
@@ -56,12 +58,14 @@ module Castkit
|
|
56
58
|
|
57
59
|
# Gets or sets the serializer class to use for instances of this object.
|
58
60
|
#
|
59
|
-
# @param value [Class<Castkit::
|
60
|
-
# @return [Class<Castkit::
|
61
|
-
# @raise [ArgumentError] if value does not inherit from Castkit::
|
61
|
+
# @param value [Class<Castkit::Serializers::Base>, nil]
|
62
|
+
# @return [Class<Castkit::Serializers::Base>, nil]
|
63
|
+
# @raise [ArgumentError] if value does not inherit from Castkit::Serializers::Base
|
62
64
|
def serializer(value = nil)
|
63
65
|
if value
|
64
|
-
|
66
|
+
unless value < Castkit::Serializers::Base
|
67
|
+
raise ArgumentError, "Serializer must inherit from Castkit::Serializers::Base"
|
68
|
+
end
|
65
69
|
|
66
70
|
@serializer = value
|
67
71
|
else
|
@@ -154,9 +158,9 @@ module Castkit
|
|
154
158
|
|
155
159
|
# Returns the serializer instance or default for this object.
|
156
160
|
#
|
157
|
-
# @return [Class<Castkit::
|
161
|
+
# @return [Class<Castkit::Serializers::Base>]
|
158
162
|
def serializer
|
159
|
-
@serializer ||= self.class.serializer || Castkit::DefaultSerializer
|
163
|
+
@serializer ||= self.class.serializer || Castkit::Serializers::DefaultSerializer
|
160
164
|
end
|
161
165
|
|
162
166
|
# Returns false if self.class.allow_unknown == true, otherwise the value of self.class.strict.
|
@@ -63,7 +63,7 @@ module Castkit
|
|
63
63
|
# UserDto = Castkit::DataObject.from_contract(UserContract)
|
64
64
|
# dto = UserDto.new(id: "abc", email: "a@example.com")
|
65
65
|
#
|
66
|
-
# @param contract [Class<Castkit::Contract::
|
66
|
+
# @param contract [Class<Castkit::Contract::Base>] the contract to convert
|
67
67
|
# @return [Class<Castkit::DataObject>] a new anonymous DataObject class
|
68
68
|
|
69
69
|
def from_contract(contract)
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castkit
|
4
|
+
module Ext
|
5
|
+
module DataObject
|
6
|
+
# Provides plugin support for DataObject classes.
|
7
|
+
#
|
8
|
+
# This module allows a Castkit::DataObject to explicitly declare supported plugins,
|
9
|
+
# and ensures all default plugins are enabled on subclassing.
|
10
|
+
#
|
11
|
+
# Plugins should be defined under `Castkit::Plugins::<Name>` and can be registered
|
12
|
+
# globally via `Castkit.configure { |c| c.register_plugin(:name, MyPlugin) }`.
|
13
|
+
#
|
14
|
+
# Example:
|
15
|
+
# class MyDto < Castkit::DataObject
|
16
|
+
# enable_plugins :oj, :msgpack
|
17
|
+
# disable_plugins :yaml
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
module Plugins
|
21
|
+
# Returns the set of plugins explicitly enabled on the class.
|
22
|
+
#
|
23
|
+
# @return [Set<Symbol>] enabled plugin names
|
24
|
+
def enabled_plugins
|
25
|
+
@enabled_plugins ||= Set.new
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the set of default plugins explicitly disabled on the class.
|
29
|
+
#
|
30
|
+
# @return [Set<Symbol>] disabled plugin names
|
31
|
+
def disabled_plugins
|
32
|
+
@disabled_plugins ||= Set.new
|
33
|
+
end
|
34
|
+
|
35
|
+
# Enables one or more plugins on the calling class.
|
36
|
+
#
|
37
|
+
# @param plugins [Array<Symbol>] plugin identifiers (e.g., :oj, :yaml)
|
38
|
+
# @return [void]
|
39
|
+
def enable_plugins(*plugins)
|
40
|
+
return if plugins.empty?
|
41
|
+
|
42
|
+
@enabled_plugins ||= Set.new
|
43
|
+
@enabled_plugins.merge(plugins)
|
44
|
+
|
45
|
+
Castkit::Plugins.activate(self, *plugins)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Disables one or more default plugins on the calling class.
|
49
|
+
#
|
50
|
+
# @example
|
51
|
+
# Castkit.configure do |config|
|
52
|
+
# config.default_plugins [:oj, :activerecord]
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# class UserDto < Castkit::DataObject
|
56
|
+
# disable_plugin :activerecord
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# @param plugins [Array<Symbol>] plugin identifiers (e.g., :oj, :yaml)
|
60
|
+
# @return [void]
|
61
|
+
def disable_plugins(*plugins)
|
62
|
+
return if plugins.empty?
|
63
|
+
|
64
|
+
@disabled_plugins ||= Set.new
|
65
|
+
@disabled_plugins.merge(plugins)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Hook that is called when a DataObject subclass is created.
|
69
|
+
#
|
70
|
+
# Automatically applies `Castkit.configuration.default_plugins`
|
71
|
+
# to the subclass.
|
72
|
+
#
|
73
|
+
# @param subclass [Class] the inheriting subclass
|
74
|
+
# @return [void]
|
75
|
+
def inherited(subclass)
|
76
|
+
super
|
77
|
+
|
78
|
+
disabled = instance_variable_get(:@disabled_plugins) || Set.new
|
79
|
+
plugins = Castkit.configuration.default_plugins - disabled.to_a
|
80
|
+
|
81
|
+
subclass.enable_plugins(*plugins)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/castkit/inflector.rb
CHANGED
@@ -24,7 +24,7 @@ module Castkit
|
|
24
24
|
# @param string [String, Symbol] the input to convert
|
25
25
|
# @return [String] the PascalCase representation
|
26
26
|
def pascalize(string)
|
27
|
-
string.to_s.split("_").map(&:capitalize).join
|
27
|
+
underscore(string).to_s.split("_").map(&:capitalize).join
|
28
28
|
end
|
29
29
|
|
30
30
|
# Converts a PascalCase or camelCase string to snake_case.
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castkit
|
4
|
+
# Internal registry for Castkit plugin modules.
|
5
|
+
#
|
6
|
+
# This module supports registering, activating, and looking up Castkit plugins.
|
7
|
+
# Plugins are typically included in Castkit::DataObject subclasses via `.enable_plugins`.
|
8
|
+
#
|
9
|
+
# Plugins can be defined under `Castkit::Plugins::<Name>` or manually registered
|
10
|
+
# through the configuration API.
|
11
|
+
#
|
12
|
+
# @example Registering a custom plugin
|
13
|
+
# Castkit.configure do |config|
|
14
|
+
# config.register_plugin(:custom, MyCustomPlugin)
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @example Enabling plugins on a DataObject
|
18
|
+
# class MyDto < Castkit::DataObject
|
19
|
+
# enable_plugins :custom, :oj
|
20
|
+
# end
|
21
|
+
module Plugins
|
22
|
+
@registered_plugins = {}
|
23
|
+
|
24
|
+
class << self
|
25
|
+
# Activates one or more plugins on the given class.
|
26
|
+
#
|
27
|
+
# Each plugin module is included into the class. If the module responds to `setup!`,
|
28
|
+
# it will be called with the class as the argument.
|
29
|
+
#
|
30
|
+
# @param klass [Class] the target class (usually a Castkit::DataObject subclass)
|
31
|
+
# @param names [Array<Symbol>] plugin names (e.g., :oj, :yaml)
|
32
|
+
# @return [void]
|
33
|
+
def activate(klass, *names)
|
34
|
+
names.each do |name|
|
35
|
+
plugin = lookup!(name)
|
36
|
+
klass.include(plugin) if plugin
|
37
|
+
plugin.setup!(klass) if plugin.respond_to?(:setup!)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# (Placeholder) Deactivates plugins by name.
|
42
|
+
#
|
43
|
+
# Currently not implemented, included for future API completeness.
|
44
|
+
#
|
45
|
+
# @param _klass [Class] the class to deactivate plugins on
|
46
|
+
# @param names [Array<Symbol>] plugin names to deactivate
|
47
|
+
# @return [void]
|
48
|
+
def deactivate(_klass, *names)
|
49
|
+
@deactivate_plugins = names
|
50
|
+
end
|
51
|
+
|
52
|
+
# Looks up a plugin module by name.
|
53
|
+
#
|
54
|
+
# This will first check the internal registry, then fall back to
|
55
|
+
# resolving a constant under `Castkit::Plugins::<Name>`.
|
56
|
+
#
|
57
|
+
# @param name [Symbol, String] the plugin name (e.g., :oj)
|
58
|
+
# @return [Module] the plugin module
|
59
|
+
# @raise [Castkit::Error] if no plugin is found
|
60
|
+
def lookup!(name)
|
61
|
+
@registered_plugins[name.to_sym] ||
|
62
|
+
const_get(Castkit::Inflector.pascalize(name.to_s), false)
|
63
|
+
rescue NameError
|
64
|
+
raise Castkit::Error,
|
65
|
+
"Castkit plugin `#{name}` could not be found. Make sure it is " \
|
66
|
+
"defined under Castkit::Plugins or registered using " \
|
67
|
+
"`Castkit.configure { |c| c.register_plugin(:#{name}, MyPlugin) }`."
|
68
|
+
end
|
69
|
+
|
70
|
+
# Registers a plugin module under a custom name.
|
71
|
+
#
|
72
|
+
# This allows developers to register modules not defined under Castkit::Plugins.
|
73
|
+
#
|
74
|
+
# @param name [Symbol] the plugin name (e.g., :custom_plugin)
|
75
|
+
# @param plugin [Module] the plugin module to register
|
76
|
+
# @return [void]
|
77
|
+
def register(name, plugin)
|
78
|
+
@registered_plugins[name] = plugin
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module Castkit
|
6
|
+
module Serializers
|
7
|
+
# Abstract base class for defining custom serializers for Castkit::DataObject instances.
|
8
|
+
#
|
9
|
+
# Handles circular reference detection and provides a consistent `call` API.
|
10
|
+
#
|
11
|
+
# Subclasses must implement an instance method `#call` that returns a hash-like representation.
|
12
|
+
#
|
13
|
+
# @example Usage
|
14
|
+
# class CustomSerializer < Castkit::Serializers::Base
|
15
|
+
# private
|
16
|
+
#
|
17
|
+
# def call
|
18
|
+
# { type: object.class.name, data: object.to_h }
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# CustomSerializer.call(user_dto)
|
23
|
+
class Base
|
24
|
+
class << self
|
25
|
+
# Entrypoint for serializing an object.
|
26
|
+
#
|
27
|
+
# @param object [Castkit::DataObject] the object to serialize
|
28
|
+
# @param visited [Set, nil] used to track visited object IDs
|
29
|
+
# @return [Object] result of custom serialization
|
30
|
+
def call(object, visited: nil)
|
31
|
+
new(object, visited: visited).send(:serialize)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [Castkit::DataObject] the object being serialized
|
36
|
+
attr_reader :object
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
# Fallback to the default serializer.
|
41
|
+
#
|
42
|
+
# @return [Hash]
|
43
|
+
def serialize_with_default
|
44
|
+
Castkit::Serializers::DefaultSerializer.call(object, visited: visited)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# @return [Set<Integer>] a set of visited object IDs to detect circular references
|
50
|
+
attr_reader :visited
|
51
|
+
|
52
|
+
# Initializes the serializer instance.
|
53
|
+
#
|
54
|
+
# @param object [Castkit::DataObject]
|
55
|
+
# @param visited [Set, nil]
|
56
|
+
def initialize(object, visited: nil)
|
57
|
+
@object = object
|
58
|
+
@visited = visited || Set.new
|
59
|
+
end
|
60
|
+
|
61
|
+
# Subclasses must override this method to implement serialization logic.
|
62
|
+
#
|
63
|
+
# @raise [NotImplementedError]
|
64
|
+
# @return [Object]
|
65
|
+
def call
|
66
|
+
raise NotImplementedError, "#{self.class.name} must implement `#call`"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Wraps the actual serialization logic with circular reference detection.
|
70
|
+
#
|
71
|
+
# @return [Object]
|
72
|
+
# @raise [Castkit::SerializationError] if a circular reference is detected
|
73
|
+
def serialize
|
74
|
+
check_circular_reference!
|
75
|
+
visited << object.object_id
|
76
|
+
|
77
|
+
result = call
|
78
|
+
visited.delete(object.object_id)
|
79
|
+
|
80
|
+
result
|
81
|
+
end
|
82
|
+
|
83
|
+
# Raises if the object has already been visited (circular reference).
|
84
|
+
#
|
85
|
+
# @raise [Castkit::SerializationError]
|
86
|
+
# @return [void]
|
87
|
+
def check_circular_reference!
|
88
|
+
return unless visited.include?(object.object_id)
|
89
|
+
|
90
|
+
raise Castkit::SerializationError, "Circular reference detected for #{object.class}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|