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
|
@@ -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
|
|
|
@@ -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
|
|
@@ -33,22 +32,13 @@ module Castkit
|
|
|
33
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
|
-
|
|
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
|
|
41
|
+
include Cattri
|
|
52
42
|
|
|
53
43
|
# Defines an attribute for the contract.
|
|
54
44
|
#
|
|
@@ -83,15 +73,9 @@ module Castkit
|
|
|
83
73
|
Castkit::Contract::Result.new(definition[:name].to_s, input)
|
|
84
74
|
end
|
|
85
75
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def definition
|
|
90
|
-
@definition ||= {
|
|
91
|
-
name: :ephemeral,
|
|
92
|
-
attributes: {}
|
|
93
|
-
}
|
|
94
|
-
end
|
|
76
|
+
cattri :definition,
|
|
77
|
+
-> { { name: :ephemeral, attributes: {} } },
|
|
78
|
+
scope: :class, expose: :read_write
|
|
95
79
|
|
|
96
80
|
# Returns the defined attributes.
|
|
97
81
|
#
|
|
@@ -129,8 +113,8 @@ module Castkit
|
|
|
129
113
|
def define_from_source(name, source)
|
|
130
114
|
source_attributes = source.attributes.dup
|
|
131
115
|
|
|
132
|
-
|
|
133
|
-
name: name,
|
|
116
|
+
self.definition = {
|
|
117
|
+
name: name.to_sym,
|
|
134
118
|
attributes: source_attributes.transform_values do |attr|
|
|
135
119
|
Castkit::Attribute.new(attr.field, attr.type, **attr.options.slice(*ATTRIBUTE_OPTIONS))
|
|
136
120
|
end
|
|
@@ -143,7 +127,8 @@ module Castkit
|
|
|
143
127
|
# @yield [block]
|
|
144
128
|
# @return [void]
|
|
145
129
|
def define_from_block(name, &block)
|
|
146
|
-
|
|
130
|
+
contract_name = name ? name.to_sym : :ephemeral
|
|
131
|
+
self.definition = { name: contract_name, attributes: {} }
|
|
147
132
|
|
|
148
133
|
@__castkit_contract_dsl = true
|
|
149
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.
|
|
@@ -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
|
|
|
@@ -1,48 +1,60 @@
|
|
|
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.
|
|
6
8
|
#
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
# Supports reusable attribute definitions, transient fields, composite readers, and
|
|
10
|
+
# grouped declarations such as `readonly`, `optional`, and `transient` blocks.
|
|
11
|
+
#
|
|
12
|
+
# This module is included into `Castkit::DataObject` and handles attribute registration,
|
|
13
|
+
# accessor generation, and typed writing behavior.
|
|
14
|
+
module Attributes # rubocop:disable Metrics/ModuleLength
|
|
15
|
+
def self.extended(base)
|
|
16
|
+
base.include(Cattri)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Declares an attribute on the data object.
|
|
10
20
|
#
|
|
21
|
+
# Accepts either inline options or a reusable attribute definition (`using` or `definition`).
|
|
11
22
|
# If `:transient` is true, defines only standard accessors and skips serialization logic.
|
|
12
23
|
#
|
|
13
|
-
# @param field [Symbol]
|
|
14
|
-
# @param type [Symbol, Class]
|
|
15
|
-
# @param
|
|
24
|
+
# @param field [Symbol] the attribute name
|
|
25
|
+
# @param type [Symbol, Class] the attribute's declared type
|
|
26
|
+
# @param definition [Hash, nil] an optional pre-built definition object (`{ type:, options: }`)
|
|
27
|
+
# @param using [Castkit::Attributes::Base, nil] an optional class-based definition (`.definition`)
|
|
28
|
+
# @param options [Hash] additional options like `default`, `access`, `required`, etc.
|
|
16
29
|
# @return [void]
|
|
17
|
-
# @raise [Castkit::DataObjectError] if
|
|
18
|
-
def attribute(field, type, **options)
|
|
30
|
+
# @raise [Castkit::DataObjectError] if attribute already defined or type mismatch
|
|
31
|
+
def attribute(field, type = nil, definition = nil, using: nil, **options)
|
|
19
32
|
field = field.to_sym
|
|
20
33
|
raise Castkit::DataObjectError, "Attribute '#{field}' already defined" if attributes.key?(field)
|
|
21
34
|
|
|
22
|
-
options =
|
|
35
|
+
type, options = use_definition(field, definition || using&.definition, type, options)
|
|
23
36
|
return define_attribute(field, type, **options) unless options[:transient]
|
|
24
37
|
|
|
25
|
-
|
|
38
|
+
define_transient_accessor(field)
|
|
26
39
|
end
|
|
27
40
|
|
|
28
|
-
# Declares a
|
|
29
|
-
#
|
|
30
|
-
# The provided block defines the read behavior.
|
|
41
|
+
# Declares a composite (computed) attribute.
|
|
31
42
|
#
|
|
32
|
-
# @param field [Symbol]
|
|
33
|
-
# @param type [Symbol, Class]
|
|
34
|
-
# @param options [Hash]
|
|
35
|
-
# @yieldreturn [Object]
|
|
43
|
+
# @param field [Symbol] the name of the attribute
|
|
44
|
+
# @param type [Symbol, Class] the attribute type
|
|
45
|
+
# @param options [Hash] additional attribute options
|
|
46
|
+
# @yieldreturn [Object] the value to return when the reader is called
|
|
47
|
+
# @return [void]
|
|
36
48
|
def composite(field, type, **options, &block)
|
|
37
49
|
attribute(field, type, **options, composite: true)
|
|
38
50
|
define_method(field, &block)
|
|
39
51
|
end
|
|
40
52
|
|
|
41
|
-
# Declares a group of transient attributes within
|
|
53
|
+
# Declares a group of transient attributes within a block.
|
|
42
54
|
#
|
|
43
|
-
# These attributes are
|
|
55
|
+
# These attributes are excluded from serialization (`to_h`) and not stored.
|
|
44
56
|
#
|
|
45
|
-
# @yield
|
|
57
|
+
# @yield a block containing `attribute` calls
|
|
46
58
|
# @return [void]
|
|
47
59
|
def transient(&block)
|
|
48
60
|
@__transient_context = true
|
|
@@ -51,126 +63,161 @@ module Castkit
|
|
|
51
63
|
@__transient_context = nil
|
|
52
64
|
end
|
|
53
65
|
|
|
54
|
-
# Declares a group of readonly attributes
|
|
66
|
+
# Declares a group of readonly attributes (accessible for read only).
|
|
55
67
|
#
|
|
56
|
-
# @param options [Hash] shared options for attributes inside the block
|
|
57
|
-
# @yield
|
|
68
|
+
# @param options [Hash] shared options for all attributes inside the block
|
|
69
|
+
# @yield a block containing `attribute` calls
|
|
58
70
|
# @return [void]
|
|
59
71
|
def readonly(**options, &block)
|
|
60
72
|
with_access([:read], options, &block)
|
|
61
73
|
end
|
|
62
74
|
|
|
63
|
-
# Declares a group of writeonly attributes
|
|
75
|
+
# Declares a group of writeonly attributes (accessible for write only).
|
|
64
76
|
#
|
|
65
|
-
# @param options [Hash] shared options for attributes inside the block
|
|
66
|
-
# @yield
|
|
77
|
+
# @param options [Hash] shared options for all attributes inside the block
|
|
78
|
+
# @yield a block containing `attribute` calls
|
|
67
79
|
# @return [void]
|
|
68
80
|
def writeonly(**options, &block)
|
|
69
81
|
with_access([:write], options, &block)
|
|
70
82
|
end
|
|
71
83
|
|
|
72
|
-
# Declares a group of required attributes
|
|
84
|
+
# Declares a group of required attributes.
|
|
73
85
|
#
|
|
74
|
-
# @param options [Hash] shared options for attributes inside the block
|
|
75
|
-
# @yield
|
|
86
|
+
# @param options [Hash] shared options for all attributes inside the block
|
|
87
|
+
# @yield a block containing `attribute` calls
|
|
76
88
|
# @return [void]
|
|
77
89
|
def required(**options, &block)
|
|
78
90
|
with_required(true, options, &block)
|
|
79
91
|
end
|
|
80
92
|
|
|
81
|
-
# Declares a group of optional attributes
|
|
93
|
+
# Declares a group of optional attributes.
|
|
82
94
|
#
|
|
83
|
-
# @param options [Hash] shared options for attributes inside the block
|
|
84
|
-
# @yield
|
|
95
|
+
# @param options [Hash] shared options for all attributes inside the block
|
|
96
|
+
# @yield a block containing `attribute` calls
|
|
85
97
|
# @return [void]
|
|
86
98
|
def optional(**options, &block)
|
|
87
99
|
with_required(false, options, &block)
|
|
88
100
|
end
|
|
89
101
|
|
|
90
|
-
# Returns all
|
|
102
|
+
# Returns all non-transient attributes defined on the class.
|
|
91
103
|
#
|
|
92
104
|
# @return [Hash{Symbol => Castkit::Attribute}]
|
|
93
105
|
def attributes
|
|
94
|
-
|
|
106
|
+
cattri_variable_memoize(:__castkit_attributes_registry) { {} }
|
|
95
107
|
end
|
|
96
108
|
|
|
97
|
-
|
|
109
|
+
def inherited(subclass)
|
|
110
|
+
super
|
|
111
|
+
|
|
112
|
+
parent_attributes = cattri_variable_get(:__castkit_attributes_registry)
|
|
113
|
+
subclass.cattri_variable_set(:__castkit_attributes_registry, parent_attributes.dup) if parent_attributes
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Alias for {#attribute}
|
|
117
|
+
#
|
|
118
|
+
# @see #attribute
|
|
119
|
+
alias attr attribute
|
|
120
|
+
|
|
121
|
+
# Alias for {#composite}
|
|
98
122
|
#
|
|
99
123
|
# @see #composite
|
|
100
124
|
alias property composite
|
|
101
125
|
|
|
102
126
|
private
|
|
103
127
|
|
|
104
|
-
#
|
|
128
|
+
# Applies a reusable definition to the current attribute call.
|
|
129
|
+
#
|
|
130
|
+
# Ensures the declared type matches and merges options.
|
|
131
|
+
#
|
|
132
|
+
# @param field [Symbol] the attribute name
|
|
133
|
+
# @param definition [Hash{Symbol => Object}, nil]
|
|
134
|
+
# @param type [Symbol, Class]
|
|
135
|
+
# @param options [Hash]
|
|
136
|
+
# @return [Array<(Symbol, Hash)>] the final type and options
|
|
137
|
+
# @raise [Castkit::DataObjectError] if type mismatch occurs
|
|
138
|
+
def use_definition(field, definition, type, options)
|
|
139
|
+
type ||= definition&.fetch(:type, nil)
|
|
140
|
+
raise Castkit::AttributeError, "Attribute `#{field} has no type" if type.nil?
|
|
141
|
+
|
|
142
|
+
if definition && type != definition[:type]
|
|
143
|
+
raise Castkit::AttributeError,
|
|
144
|
+
"Attribute `#{field}` type mismatch: expected #{definition[:type].inspect}, got #{type.inspect}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
options = definition[:options].merge(options) if definition
|
|
148
|
+
[type, build_options(options)]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Instantiates and stores a Castkit::Attribute, defining accessors as needed.
|
|
105
152
|
#
|
|
106
153
|
# @param field [Symbol]
|
|
107
154
|
# @param type [Symbol, Class]
|
|
108
155
|
# @param options [Hash]
|
|
156
|
+
# @return [void]
|
|
109
157
|
def define_attribute(field, type, **options)
|
|
110
158
|
attribute = Castkit::Attribute.new(field, type, **options)
|
|
111
159
|
attributes[field] = attribute
|
|
112
160
|
|
|
113
|
-
|
|
114
|
-
attr_reader field
|
|
115
|
-
|
|
116
|
-
define_typed_writer(field, attribute)
|
|
117
|
-
elsif attribute.writeable?
|
|
118
|
-
define_typed_writer(field, attribute)
|
|
119
|
-
elsif attribute.readable?
|
|
120
|
-
attr_reader field
|
|
121
|
-
end
|
|
161
|
+
define_accessors(attribute)
|
|
122
162
|
end
|
|
123
163
|
|
|
124
|
-
#
|
|
164
|
+
# Creates readers/writers for a defined attribute using Cattri.
|
|
125
165
|
#
|
|
126
|
-
# @param field [Symbol]
|
|
127
166
|
# @param attribute [Castkit::Attribute]
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
|
|
167
|
+
# @return [void]
|
|
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)
|
|
137
182
|
end
|
|
138
183
|
end
|
|
139
184
|
|
|
140
|
-
# Applies
|
|
185
|
+
# Applies a temporary `access` context to all attributes within a block.
|
|
141
186
|
#
|
|
142
|
-
# @param access [Array<Symbol>] e.g
|
|
187
|
+
# @param access [Array<Symbol>] e.g. `[:read]` or `[:write]`
|
|
143
188
|
# @param options [Hash]
|
|
144
|
-
# @yield the block containing
|
|
189
|
+
# @yield the block containing `attribute` calls
|
|
190
|
+
# @return [void]
|
|
145
191
|
def with_access(access, options = {}, &block)
|
|
146
192
|
@__access_context = access
|
|
147
193
|
@__block_options = options
|
|
194
|
+
|
|
148
195
|
instance_eval(&block)
|
|
149
196
|
ensure
|
|
150
197
|
@__access_context = nil
|
|
151
198
|
@__block_options = nil
|
|
152
199
|
end
|
|
153
200
|
|
|
154
|
-
# Applies
|
|
201
|
+
# Applies a temporary `required` context to all attributes within a block.
|
|
155
202
|
#
|
|
156
203
|
# @param flag [Boolean]
|
|
157
204
|
# @param options [Hash]
|
|
158
|
-
# @yield the block containing
|
|
205
|
+
# @yield the block containing `attribute` calls
|
|
206
|
+
# @return [void]
|
|
159
207
|
def with_required(flag, options = {}, &block)
|
|
160
208
|
@__required_context = flag
|
|
161
209
|
@__block_options = options
|
|
210
|
+
|
|
162
211
|
instance_eval(&block)
|
|
163
212
|
ensure
|
|
164
213
|
@__required_context = nil
|
|
165
214
|
@__block_options = nil
|
|
166
215
|
end
|
|
167
216
|
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
# Merges scoped flags like `required`, `access`, and `transient` if present.
|
|
217
|
+
# Merges any current context flags (e.g., required, access) into the options hash.
|
|
171
218
|
#
|
|
172
219
|
# @param options [Hash]
|
|
173
|
-
# @return [Hash]
|
|
220
|
+
# @return [Hash] effective options for the attribute
|
|
174
221
|
def build_options(options)
|
|
175
222
|
base = @__block_options || {}
|
|
176
223
|
base = base.merge(required: @__required_context) unless @__required_context.nil?
|
|
@@ -179,6 +226,26 @@ module Castkit
|
|
|
179
226
|
|
|
180
227
|
base.merge(options)
|
|
181
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
|
|
182
249
|
end
|
|
183
250
|
end
|
|
184
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
|