castkit 0.2.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 +40 -1
- data/README.md +297 -13
- data/castkit.gemspec +3 -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 +18 -5
- data/lib/castkit/cli/generate.rb +112 -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 -17
- data/lib/castkit/contract/result.rb +2 -2
- data/lib/castkit/contract/validator.rb +5 -1
- data/lib/castkit/contract.rb +5 -5
- data/lib/castkit/core/attributes.rb +87 -44
- data/lib/castkit/data_object.rb +11 -30
- 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 +2 -2
- data/lib/castkit/{ext → dsl}/data_object/deserialization.rb +6 -2
- data/lib/castkit/dsl/data_object/plugins.rb +86 -0
- data/lib/castkit/{ext → dsl}/data_object/serialization.rb +1 -1
- data/lib/castkit/dsl/data_object.rb +61 -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} +30 -10
- 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/castkit.rb +1 -4
- data/lib/generators/attribute.rb +39 -0
- 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/attribute.rb.tt +21 -0
- data/lib/generators/templates/attribute_spec.rb.tt +41 -0
- data/lib/generators/templates/contract.rb.tt +26 -0
- data/lib/generators/templates/contract_spec.rb.tt +76 -0
- data/lib/generators/templates/data_object.rb.tt +17 -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 +57 -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 +92 -16
- data/.rspec_status +0 -196
- data/lib/castkit/core/registerable.rb +0 -59
- 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
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../core/config"
|
|
4
4
|
require_relative "../core/attribute_types"
|
|
5
|
-
require_relative "../core/registerable"
|
|
6
5
|
require_relative "result"
|
|
7
6
|
|
|
8
7
|
module Castkit
|
|
@@ -14,7 +13,7 @@ module Castkit
|
|
|
14
13
|
# ephemeral or reusable contract classes.
|
|
15
14
|
#
|
|
16
15
|
# @example Subclassing directly
|
|
17
|
-
# class MyContract < Castkit::Contract::
|
|
16
|
+
# class MyContract < Castkit::Contract::Base
|
|
18
17
|
# string :id
|
|
19
18
|
# integer :count, required: false
|
|
20
19
|
# end
|
|
@@ -30,26 +29,15 @@ module Castkit
|
|
|
30
29
|
# UserContract.validate!(id: "123")
|
|
31
30
|
#
|
|
32
31
|
# @see Castkit::Contract.build
|
|
33
|
-
class
|
|
32
|
+
class Base
|
|
34
33
|
extend Castkit::Core::Config
|
|
35
34
|
extend Castkit::Core::AttributeTypes
|
|
36
|
-
extend Castkit::Core::Registerable
|
|
37
35
|
|
|
38
36
|
ATTRIBUTE_OPTIONS = %i[
|
|
39
37
|
required aliases min max format of validator unwrapped prefix force_type
|
|
40
38
|
].freeze
|
|
41
39
|
|
|
42
40
|
class << self
|
|
43
|
-
# Registers the current class under `Castkit::Contracts`.
|
|
44
|
-
#
|
|
45
|
-
# @param as [String, Symbol, nil] The constant name to use (PascalCase). Defaults to the name used when building
|
|
46
|
-
# the contract. If no name was provided, an error is raised.
|
|
47
|
-
# @return [Class] the registered contract class
|
|
48
|
-
# @raise [Castkit::Error] If a name cannot be resolved.
|
|
49
|
-
def register!(as: nil)
|
|
50
|
-
super(namespace: :contracts, as: as || definition[:name])
|
|
51
|
-
end
|
|
52
|
-
|
|
53
41
|
# Defines an attribute for the contract.
|
|
54
42
|
#
|
|
55
43
|
# Only a subset of options is allowed inside a contract.
|
|
@@ -69,7 +57,6 @@ module Castkit
|
|
|
69
57
|
# @return [Castkit::Contract::Result]
|
|
70
58
|
def validate(input)
|
|
71
59
|
validate!(input)
|
|
72
|
-
Castkit::Contract::Result.new(definition[:name].to_s, input)
|
|
73
60
|
rescue Castkit::ContractError => e
|
|
74
61
|
Castkit::Contract::Result.new(definition[:name].to_s, input, errors: e.errors)
|
|
75
62
|
end
|
|
@@ -81,6 +68,7 @@ module Castkit
|
|
|
81
68
|
# @return [void]
|
|
82
69
|
def validate!(input)
|
|
83
70
|
Castkit::Contract::Validator.call!(attributes.values, input, **validation_rules)
|
|
71
|
+
Castkit::Contract::Result.new(definition[:name].to_s, input)
|
|
84
72
|
end
|
|
85
73
|
|
|
86
74
|
# Returns internal contract metadata.
|
|
@@ -88,7 +76,7 @@ module Castkit
|
|
|
88
76
|
# @return [Hash]
|
|
89
77
|
def definition
|
|
90
78
|
@definition ||= {
|
|
91
|
-
name: :
|
|
79
|
+
name: :ephemeral,
|
|
92
80
|
attributes: {}
|
|
93
81
|
}
|
|
94
82
|
end
|
|
@@ -108,7 +96,7 @@ module Castkit
|
|
|
108
96
|
# @param source [Castkit::DataObject, nil]
|
|
109
97
|
# @param block [Proc, nil]
|
|
110
98
|
# @return [Hash]
|
|
111
|
-
def define(name = :
|
|
99
|
+
def define(name = :ephemeral, source = nil, validation_rules: {}, &block)
|
|
112
100
|
validate_definition!(source, &block)
|
|
113
101
|
|
|
114
102
|
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
|
|
@@ -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
|
|
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
|
|
@@ -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
|
@@ -3,15 +3,9 @@
|
|
|
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
|
-
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/serialization"
|
|
8
|
+
require_relative "dsl/data_object"
|
|
15
9
|
|
|
16
10
|
module Castkit
|
|
17
11
|
# Base class for defining declarative, typed data transfer objects (DTOs).
|
|
@@ -29,24 +23,9 @@ module Castkit
|
|
|
29
23
|
# user = UserDto.new(name: "Alice", age: 30)
|
|
30
24
|
# user.to_json #=> '{"name":"Alice","age":30}'
|
|
31
25
|
class DataObject
|
|
32
|
-
|
|
33
|
-
extend Castkit::Core::Attributes
|
|
34
|
-
extend Castkit::Core::AttributeTypes
|
|
35
|
-
extend Castkit::Core::Registerable
|
|
36
|
-
extend Castkit::Ext::DataObject::Contract
|
|
37
|
-
|
|
38
|
-
include Castkit::Ext::DataObject::Serialization
|
|
39
|
-
include Castkit::Ext::DataObject::Deserialization
|
|
26
|
+
include Castkit::DSL::DataObject
|
|
40
27
|
|
|
41
28
|
class << self
|
|
42
|
-
# Registers the current class under `Castkit::DataObjects`.
|
|
43
|
-
#
|
|
44
|
-
# @param as [String, Symbol, nil] The constant name to use (PascalCase). Defaults to class name or "Anonymous".
|
|
45
|
-
# @return [Class] the registered dataobject class
|
|
46
|
-
def register!(as: nil)
|
|
47
|
-
super(namespace: :dataobjects, as: as)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
29
|
def build(&block)
|
|
51
30
|
klass = Class.new(self)
|
|
52
31
|
klass.class_eval(&block) if block_given?
|
|
@@ -56,12 +35,14 @@ module Castkit
|
|
|
56
35
|
|
|
57
36
|
# Gets or sets the serializer class to use for instances of this object.
|
|
58
37
|
#
|
|
59
|
-
# @param value [Class<Castkit::
|
|
60
|
-
# @return [Class<Castkit::
|
|
61
|
-
# @raise [ArgumentError] if value does not inherit from Castkit::
|
|
38
|
+
# @param value [Class<Castkit::Serializers::Base>, nil]
|
|
39
|
+
# @return [Class<Castkit::Serializers::Base>, nil]
|
|
40
|
+
# @raise [ArgumentError] if value does not inherit from Castkit::Serializers::Base
|
|
62
41
|
def serializer(value = nil)
|
|
63
42
|
if value
|
|
64
|
-
|
|
43
|
+
unless value < Castkit::Serializers::Base
|
|
44
|
+
raise ArgumentError, "Serializer must inherit from Castkit::Serializers::Base"
|
|
45
|
+
end
|
|
65
46
|
|
|
66
47
|
@serializer = value
|
|
67
48
|
else
|
|
@@ -154,9 +135,9 @@ module Castkit
|
|
|
154
135
|
|
|
155
136
|
# Returns the serializer instance or default for this object.
|
|
156
137
|
#
|
|
157
|
-
# @return [Class<Castkit::
|
|
138
|
+
# @return [Class<Castkit::Serializers::Base>]
|
|
158
139
|
def serializer
|
|
159
|
-
@serializer ||= self.class.serializer || Castkit::DefaultSerializer
|
|
140
|
+
@serializer ||= self.class.serializer || Castkit::Serializers::DefaultSerializer
|
|
160
141
|
end
|
|
161
142
|
|
|
162
143
|
# Returns false if self.class.allow_unknown == true, otherwise the value of self.class.strict.
|
|
@@ -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
|