structure 4.1.0 → 4.2.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/lib/structure/builder.rb +24 -6
- data/lib/structure/rbs.rb +165 -30
- data/lib/structure/types.rb +8 -3
- data/lib/structure/version.rb +1 -1
- data/lib/structure.rb +62 -10
- data/sig/structure/builder.rbs +9 -4
- data/sig/structure/rbs.rbs +5 -1
- data/sig/structure/types.rbs +1 -1
- data/sig/structure.rbs +5 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0d49fdb007e1c1690e686dc7c17498fd905b8e100c8dcf8fe3b374c99ec18c14
|
4
|
+
data.tar.gz: 3dbdc1a4e9ac4e6e70332084bda5612b4b17dc74a63a28e0eb084a2f49d440bb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9d7c579f4e71000c4cabeaa4bd95deb3c01763b0ab5e426fb7544da65e7617a5e8ac7d32c7a3079f690a55c0021a57d97646732d516a25796cbc8999dbb6630a
|
7
|
+
data.tar.gz: '02791f8b7a9372c583839e35c7c20c6bb7ec03626256f92f9cda2ef11c5188d541f0f3b073a053d7ccf152da4a47c4af32eeb65b2e68a92ecc00e8494174ba7e'
|
data/lib/structure/builder.rb
CHANGED
@@ -14,6 +14,7 @@ module Structure
|
|
14
14
|
@defaults = {}
|
15
15
|
@types = {}
|
16
16
|
@optional = Set.new
|
17
|
+
@non_nullable = Set.new
|
17
18
|
end
|
18
19
|
|
19
20
|
# DSL method for defining attributes with optional type coercion
|
@@ -22,6 +23,7 @@ module Structure
|
|
22
23
|
# @param type [Class, Symbol, Array, nil] Type for coercion (e.g., String, :boolean, [String])
|
23
24
|
# @param from [String, nil] Source key in the data hash (defaults to name.to_s)
|
24
25
|
# @param default [Object, nil] Default value if attribute is missing
|
26
|
+
# @param null [Boolean] Whether nil values are allowed (default: true)
|
25
27
|
# @yield [value] Block for custom transformation
|
26
28
|
# @raise [ArgumentError] If both type and block are provided
|
27
29
|
#
|
@@ -35,12 +37,16 @@ module Structure
|
|
35
37
|
# attribute :price do |value|
|
36
38
|
# Money.new(value["amount"], value["currency"])
|
37
39
|
# end
|
38
|
-
|
39
|
-
|
40
|
+
#
|
41
|
+
# @example Non-nullable attribute
|
42
|
+
# attribute :id, String, null: false
|
43
|
+
def attribute(name, type = nil, from: nil, default: nil, null: true, &block)
|
44
|
+
mappings[name] = (from || name).to_s
|
40
45
|
defaults[name] = default unless default.nil?
|
46
|
+
@non_nullable.add(name) unless null
|
41
47
|
|
42
48
|
if type && block
|
43
|
-
raise ArgumentError, "
|
49
|
+
raise ArgumentError, "cannot specify both type and block for :#{name}"
|
44
50
|
else
|
45
51
|
types[name] = type || block
|
46
52
|
end
|
@@ -52,6 +58,7 @@ module Structure
|
|
52
58
|
# @param type [Class, Symbol, Array, nil] Type for coercion (e.g., String, :boolean, [String])
|
53
59
|
# @param from [String, nil] Source key in the data hash (defaults to name.to_s)
|
54
60
|
# @param default [Object, nil] Default value if attribute is missing
|
61
|
+
# @param null [Boolean] Whether nil values are allowed (default: true)
|
55
62
|
# @yield [value] Block for custom transformation
|
56
63
|
# @raise [ArgumentError] If both type and block are provided
|
57
64
|
#
|
@@ -60,8 +67,11 @@ module Structure
|
|
60
67
|
#
|
61
68
|
# @example Optional with default
|
62
69
|
# attribute? :status, String, default: "pending"
|
63
|
-
|
64
|
-
|
70
|
+
#
|
71
|
+
# @example Optional but non-nullable when present
|
72
|
+
# attribute? :name, String, null: false
|
73
|
+
def attribute?(name, type = nil, from: nil, default: nil, null: true, &block)
|
74
|
+
attribute(name, type, from: from, default: default, null: null, &block)
|
65
75
|
@optional.add(name)
|
66
76
|
end
|
67
77
|
|
@@ -87,9 +97,17 @@ module Structure
|
|
87
97
|
# @api private
|
88
98
|
def required = attributes - optional
|
89
99
|
|
100
|
+
# @api private
|
101
|
+
def non_nullable = @non_nullable.to_a
|
102
|
+
|
90
103
|
# @api private
|
91
104
|
def coercions(context = nil)
|
92
|
-
@types.
|
105
|
+
@types.to_h do |attr, type|
|
106
|
+
coercion = Types.coerce(type, context)
|
107
|
+
[attr, coercion]
|
108
|
+
rescue ArgumentError => e
|
109
|
+
raise ArgumentError, "#{e.message} for :#{attr}"
|
110
|
+
end
|
93
111
|
end
|
94
112
|
|
95
113
|
# @api private
|
data/lib/structure/rbs.rb
CHANGED
@@ -5,10 +5,13 @@ require "pathname"
|
|
5
5
|
|
6
6
|
module Structure
|
7
7
|
# Generates RBS type signatures for Structure classes
|
8
|
-
#
|
9
|
-
# Note: Custom methods defined in Structure blocks are not included and must be manually added to RBS files. This is
|
10
|
-
# consistent with how Ruby's RBS tooling handles Data classes.
|
11
8
|
module RBS
|
9
|
+
# @type const EMPTY_CUSTOM_METHODS: { instance: Array[untyped], singleton: Array[untyped] }
|
10
|
+
empty_instance = _ = [] #: Array[untyped]
|
11
|
+
empty_singleton = _ = [] #: Array[untyped]
|
12
|
+
EMPTY_CUSTOM_METHODS = { instance: empty_instance, singleton: empty_singleton }.freeze
|
13
|
+
private_constant :EMPTY_CUSTOM_METHODS
|
14
|
+
|
12
15
|
class << self
|
13
16
|
def emit(klass)
|
14
17
|
return unless klass < Data
|
@@ -20,8 +23,12 @@ module Structure
|
|
20
23
|
meta = klass.respond_to?(:__structure_meta__) ? klass.__structure_meta__ : {}
|
21
24
|
|
22
25
|
attributes = meta[:mappings] ? meta[:mappings].keys : klass.members
|
23
|
-
|
24
|
-
|
26
|
+
# @type var types: Hash[Symbol, untyped]
|
27
|
+
default_types = _ = {} #: Hash[Symbol, untyped]
|
28
|
+
types = meta.fetch(:types, default_types)
|
29
|
+
# @type var required: Array[Symbol]
|
30
|
+
required = meta.fetch(:required, attributes)
|
31
|
+
custom_methods = meta.fetch(:custom_methods, EMPTY_CUSTOM_METHODS)
|
25
32
|
|
26
33
|
emit_rbs_content(
|
27
34
|
class_name:,
|
@@ -29,6 +36,7 @@ module Structure
|
|
29
36
|
types:,
|
30
37
|
required:,
|
31
38
|
has_structure_modules: meta.any?,
|
39
|
+
custom_methods:,
|
32
40
|
)
|
33
41
|
end
|
34
42
|
|
@@ -53,7 +61,7 @@ module Structure
|
|
53
61
|
|
54
62
|
private
|
55
63
|
|
56
|
-
def emit_rbs_content(class_name:, attributes:, types:, required:, has_structure_modules:)
|
64
|
+
def emit_rbs_content(class_name:, attributes:, types:, required:, has_structure_modules:, custom_methods:)
|
57
65
|
# @type var lines: Array[String]
|
58
66
|
lines = []
|
59
67
|
lines << "class #{class_name} < Data"
|
@@ -87,48 +95,127 @@ module Structure
|
|
87
95
|
}.join(", ") + " }"
|
88
96
|
end
|
89
97
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
98
|
+
# Build singleton methods list
|
99
|
+
# Note: `new` and `[]` are kept at top (RBS::Sorter convention)
|
100
|
+
special_singleton_methods = [
|
101
|
+
{
|
102
|
+
name: "new",
|
103
|
+
lines: [
|
104
|
+
" def self.new: (#{keyword_params}) -> #{class_name}",
|
105
|
+
" | (#{positional_params}) -> #{class_name}",
|
106
|
+
],
|
107
|
+
},
|
108
|
+
{
|
109
|
+
name: "[]",
|
110
|
+
lines: [
|
111
|
+
" def self.[]: (#{keyword_params}) -> #{class_name}",
|
112
|
+
" | (#{positional_params}) -> #{class_name}",
|
113
|
+
],
|
114
|
+
},
|
115
|
+
]
|
96
116
|
|
97
|
-
#
|
117
|
+
# Regular singleton methods (to be sorted)
|
118
|
+
# @type var singleton_methods_list: Array[{ name: String, lines: Array[String] }]
|
119
|
+
singleton_methods_list = []
|
120
|
+
|
121
|
+
# Add custom singleton methods
|
122
|
+
# @type var custom_singleton: Array[untyped]
|
123
|
+
default_singleton = _ = [] #: Array[untyped]
|
124
|
+
custom_singleton = custom_methods.fetch(:singleton, default_singleton)
|
125
|
+
custom_singleton.each do |method_meta|
|
126
|
+
singleton_methods_list << {
|
127
|
+
name: method_meta[:name].to_s,
|
128
|
+
lines: [format_method_signature(method_meta, singleton: true)],
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
132
|
+
# Add standard singleton methods
|
98
133
|
members_tuple = attributes.map { |attr| ":#{attr}" }.join(", ")
|
99
|
-
|
100
|
-
|
134
|
+
singleton_methods_list << {
|
135
|
+
name: "members",
|
136
|
+
lines: [" def self.members: () -> [ #{members_tuple} ]"],
|
137
|
+
}
|
101
138
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
139
|
+
singleton_methods_list << if needs_parse_data
|
140
|
+
{
|
141
|
+
name: "parse",
|
142
|
+
lines: [
|
143
|
+
" def self.parse: (?parse_data data) -> #{class_name}",
|
144
|
+
" | (?Hash[String, untyped] data) -> #{class_name}",
|
145
|
+
],
|
146
|
+
}
|
106
147
|
else
|
107
|
-
|
108
|
-
|
148
|
+
{
|
149
|
+
name: "parse",
|
150
|
+
lines: [" def self.parse: (?Hash[String | Symbol, untyped], **untyped) -> #{class_name}"],
|
151
|
+
}
|
109
152
|
end
|
153
|
+
|
154
|
+
# Emit special singleton methods first (new, [])
|
155
|
+
special_singleton_methods.each do |method|
|
156
|
+
# @type var method_lines: Array[String]
|
157
|
+
method_lines = method[:lines]
|
158
|
+
method_lines.each { |line| lines << line }
|
159
|
+
end
|
160
|
+
|
161
|
+
# Sort and emit other singleton methods
|
162
|
+
singleton_methods_list.sort_by { |m| m[:name] }.each do |method|
|
163
|
+
lines << "" # Blank line before each method group
|
164
|
+
# @type var method_lines: Array[String]
|
165
|
+
method_lines = method[:lines]
|
166
|
+
method_lines.each { |line| lines << line }
|
167
|
+
end
|
168
|
+
|
110
169
|
lines << ""
|
111
170
|
|
112
171
|
# Sort attr_reader lines alphabetically (RBS::Sorter does this)
|
113
172
|
attributes.sort.each do |attr|
|
114
173
|
lines << " attr_reader #{attr}: #{rbs_types[attr]}"
|
115
174
|
end
|
175
|
+
lines << ""
|
176
|
+
|
177
|
+
# Build instance methods list (standard + custom), then sort
|
178
|
+
# @type var instance_methods_list: Array[{ name: String, lines: Array[String] }]
|
179
|
+
instance_methods_list = []
|
116
180
|
|
117
181
|
# Add boolean predicates
|
118
182
|
boolean_predicates = types.sort.select { |attr, type| type == :boolean && !attr.to_s.end_with?("?") }
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
lines
|
123
|
-
|
183
|
+
boolean_predicates.each do |attr, _type|
|
184
|
+
instance_methods_list << {
|
185
|
+
name: "#{attr}?",
|
186
|
+
lines: [" def #{attr}?: () -> bool"],
|
187
|
+
}
|
124
188
|
end
|
125
189
|
|
126
|
-
#
|
127
|
-
|
128
|
-
|
190
|
+
# Add custom instance methods
|
191
|
+
# @type var custom_instance: Array[untyped]
|
192
|
+
default_instance = _ = [] #: Array[untyped]
|
193
|
+
custom_instance = custom_methods.fetch(:instance, default_instance)
|
194
|
+
custom_instance.each do |method_meta|
|
195
|
+
instance_methods_list << {
|
196
|
+
name: method_meta[:name].to_s,
|
197
|
+
lines: [format_method_signature(method_meta, singleton: false)],
|
198
|
+
}
|
199
|
+
end
|
200
|
+
|
201
|
+
# Add standard instance methods
|
202
|
+
instance_methods_list << {
|
203
|
+
name: "members",
|
204
|
+
lines: [" def members: () -> [ #{members_tuple} ]"],
|
205
|
+
}
|
129
206
|
|
130
207
|
hash_type = attributes.map { |attr| "#{attr}: #{rbs_types[attr]}" }.join(", ")
|
131
|
-
|
208
|
+
instance_methods_list << {
|
209
|
+
name: "to_h",
|
210
|
+
lines: [" def to_h: () -> { #{hash_type} }"],
|
211
|
+
}
|
212
|
+
|
213
|
+
# Sort and emit instance methods
|
214
|
+
instance_methods_list.sort_by { |m| m[:name] }.each do |method|
|
215
|
+
# @type var method_lines: Array[String]
|
216
|
+
method_lines = method[:lines]
|
217
|
+
method_lines.each { |line| lines << line }
|
218
|
+
end
|
132
219
|
end
|
133
220
|
|
134
221
|
lines << "end"
|
@@ -183,6 +270,54 @@ module Structure
|
|
183
270
|
"untyped"
|
184
271
|
end
|
185
272
|
end
|
273
|
+
|
274
|
+
def format_method_signature(method_meta, singleton:)
|
275
|
+
name = method_meta.fetch(:name)
|
276
|
+
parameters = method_meta.fetch(:parameters, [])
|
277
|
+
|
278
|
+
params_str, block_str = build_parameters_fragment(parameters)
|
279
|
+
|
280
|
+
method_prefix = singleton ? "self." : ""
|
281
|
+
signature = " def #{method_prefix}#{name}: #{params_str}"
|
282
|
+
signature += " #{block_str}" if block_str
|
283
|
+
signature + " -> untyped"
|
284
|
+
end
|
285
|
+
|
286
|
+
def build_parameters_fragment(parameters)
|
287
|
+
# @type var parts: Array[String]
|
288
|
+
parts = []
|
289
|
+
block_required = false
|
290
|
+
|
291
|
+
parameters.each do |kind, param_name|
|
292
|
+
case kind
|
293
|
+
when :req
|
294
|
+
parts << "untyped"
|
295
|
+
when :opt
|
296
|
+
parts << "?untyped"
|
297
|
+
when :rest
|
298
|
+
parts << "*untyped"
|
299
|
+
when :keyreq
|
300
|
+
key_name = param_name || :arg
|
301
|
+
parts << "#{key_name}: untyped"
|
302
|
+
when :key
|
303
|
+
key_name = param_name || :arg
|
304
|
+
parts << "?#{key_name}: untyped"
|
305
|
+
when :keyrest
|
306
|
+
parts << "**untyped"
|
307
|
+
when :block
|
308
|
+
block_required = true
|
309
|
+
else
|
310
|
+
parts << "untyped"
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
params_str = "(#{parts.join(", ")})"
|
315
|
+
params_str = "()" if parts.empty?
|
316
|
+
|
317
|
+
block_str = block_required ? "?{ (*untyped, **untyped) -> untyped }" : nil
|
318
|
+
|
319
|
+
[params_str, block_str]
|
320
|
+
end
|
186
321
|
end
|
187
322
|
end
|
188
323
|
end
|
data/lib/structure/types.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "set"
|
4
|
+
|
3
5
|
module Structure
|
4
6
|
# Type coercion methods for converting values to specific types
|
5
7
|
module Types
|
6
8
|
class << self
|
7
9
|
# Rails-style boolean truthy values
|
8
10
|
# Reference: https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
|
9
|
-
BOOLEAN_TRUTHY = [true, 1, "1", "t", "T", "true", "TRUE", "on", "ON"].freeze
|
11
|
+
BOOLEAN_TRUTHY = Set.new([true, 1, "1", "t", "T", "true", "TRUE", "on", "ON"]).freeze
|
10
12
|
private_constant :BOOLEAN_TRUTHY
|
11
13
|
|
12
14
|
# Main factory method for creating type coercers
|
@@ -58,7 +60,7 @@ module Structure
|
|
58
60
|
when String
|
59
61
|
lazy_class(type, context)
|
60
62
|
else
|
61
|
-
raise ArgumentError, "
|
63
|
+
raise ArgumentError, "cannot specify #{type.inspect} as type"
|
62
64
|
end
|
63
65
|
end
|
64
66
|
|
@@ -93,12 +95,15 @@ module Structure
|
|
93
95
|
value.map { |element| context.parse(element) }
|
94
96
|
end
|
95
97
|
when String
|
98
|
+
resolved_class = nil
|
99
|
+
|
96
100
|
lambda do |value|
|
97
101
|
unless value.respond_to?(:map)
|
98
102
|
raise TypeError, "can't convert #{value.class} into Array"
|
99
103
|
end
|
100
104
|
|
101
|
-
resolved_class
|
105
|
+
resolved_class ||= resolve_class(element_type, context)
|
106
|
+
# @type var resolved_class: untyped
|
102
107
|
value.map { |element| resolved_class.parse(element) }
|
103
108
|
end
|
104
109
|
else
|
data/lib/structure/version.rb
CHANGED
data/lib/structure.rb
CHANGED
@@ -4,6 +4,9 @@ require "structure/builder"
|
|
4
4
|
|
5
5
|
# A library for parsing data into immutable Ruby Data objects with type coercion
|
6
6
|
module Structure
|
7
|
+
TO_H_CHECKER = ->(x) { x.respond_to?(:to_h) && x }
|
8
|
+
private_constant :TO_H_CHECKER
|
9
|
+
|
7
10
|
class << self
|
8
11
|
# Creates a new Data class with attribute definitions and type coercion
|
9
12
|
#
|
@@ -21,13 +24,18 @@ module Structure
|
|
21
24
|
# person.age # => 30
|
22
25
|
def new(&block)
|
23
26
|
builder = Builder.new
|
24
|
-
builder.instance_eval(&block) if block
|
27
|
+
builder.instance_eval(&block) if block # steep:ignore
|
25
28
|
|
26
29
|
# @type var klass: untyped
|
27
30
|
klass = Data.define(*builder.attributes)
|
28
31
|
|
32
|
+
custom_methods_metadata = { instance: [], singleton: [] }.freeze # steep:ignore
|
33
|
+
|
29
34
|
# Enable custom method definitions by evaluating block on the class
|
30
35
|
if block
|
36
|
+
before_instance_methods = klass.instance_methods(false)
|
37
|
+
before_singleton_methods = klass.singleton_methods(false)
|
38
|
+
|
31
39
|
# Provide temporary dummy DSL methods to prevent NoMethodError during class_eval
|
32
40
|
klass.define_singleton_method(:attribute) { |*args, **kwargs, &blk| }
|
33
41
|
klass.define_singleton_method(:attribute?) { |*args, **kwargs, &blk| }
|
@@ -40,6 +48,36 @@ module Structure
|
|
40
48
|
klass.singleton_class.send(:remove_method, :attribute)
|
41
49
|
klass.singleton_class.send(:remove_method, :attribute?)
|
42
50
|
klass.singleton_class.send(:remove_method, :after_parse)
|
51
|
+
|
52
|
+
after_instance_methods = klass.instance_methods(false)
|
53
|
+
after_singleton_methods = klass.singleton_methods(false)
|
54
|
+
|
55
|
+
newly_defined_instance_methods = after_instance_methods - before_instance_methods
|
56
|
+
newly_defined_singleton_methods = after_singleton_methods - before_singleton_methods
|
57
|
+
|
58
|
+
instance_method_metadata = newly_defined_instance_methods.filter_map do |name|
|
59
|
+
next unless klass.public_method_defined?(name)
|
60
|
+
|
61
|
+
{
|
62
|
+
name: name,
|
63
|
+
parameters: klass.instance_method(name).parameters,
|
64
|
+
}.freeze
|
65
|
+
end
|
66
|
+
|
67
|
+
singleton_class = klass.singleton_class
|
68
|
+
singleton_method_metadata = newly_defined_singleton_methods.filter_map do |name|
|
69
|
+
next unless singleton_class.public_method_defined?(name)
|
70
|
+
|
71
|
+
{
|
72
|
+
name: name,
|
73
|
+
parameters: singleton_class.instance_method(name).parameters,
|
74
|
+
}.freeze
|
75
|
+
end
|
76
|
+
|
77
|
+
custom_methods_metadata = {
|
78
|
+
instance: instance_method_metadata.freeze,
|
79
|
+
singleton: singleton_method_metadata.freeze,
|
80
|
+
}.freeze
|
43
81
|
end
|
44
82
|
|
45
83
|
# Override initialize to make optional attributes truly optional
|
@@ -48,12 +86,14 @@ module Structure
|
|
48
86
|
klass.class_eval do
|
49
87
|
alias_method(:__data_initialize__, :initialize)
|
50
88
|
|
51
|
-
|
89
|
+
# steep:ignore:start
|
90
|
+
define_method(:initialize) do |**kwargs|
|
52
91
|
optional_attrs.each do |attr|
|
53
92
|
kwargs[attr] = nil unless kwargs.key?(attr)
|
54
93
|
end
|
55
|
-
__data_initialize__(**kwargs)
|
94
|
+
__data_initialize__(**kwargs)
|
56
95
|
end
|
96
|
+
# steep:ignore:end
|
57
97
|
end
|
58
98
|
end
|
59
99
|
|
@@ -69,6 +109,8 @@ module Structure
|
|
69
109
|
coercions: builder.coercions(klass),
|
70
110
|
after_parse: builder.after_parse_callback,
|
71
111
|
required: builder.required,
|
112
|
+
non_nullable: builder.non_nullable,
|
113
|
+
custom_methods: custom_methods_metadata,
|
72
114
|
}.freeze
|
73
115
|
klass.instance_variable_set(:@__structure_meta__, meta)
|
74
116
|
klass.singleton_class.attr_reader(:__structure_meta__)
|
@@ -79,7 +121,7 @@ module Structure
|
|
79
121
|
v = public_send(m)
|
80
122
|
value = case v
|
81
123
|
when Array then v.map { |x| x.respond_to?(:to_h) && x ? x.to_h : x }
|
82
|
-
when
|
124
|
+
when TO_H_CHECKER then v.to_h
|
83
125
|
else v
|
84
126
|
end
|
85
127
|
[m, value]
|
@@ -100,11 +142,13 @@ module Structure
|
|
100
142
|
|
101
143
|
overrides&.each { |k, v| data[k.to_s] = v }
|
102
144
|
|
103
|
-
final
|
104
|
-
mappings
|
105
|
-
defaults
|
106
|
-
|
107
|
-
|
145
|
+
final = {}
|
146
|
+
mappings = __structure_meta__[:mappings]
|
147
|
+
defaults = __structure_meta__[:defaults]
|
148
|
+
coercions = __structure_meta__[:coercions]
|
149
|
+
after_parse = __structure_meta__[:after_parse]
|
150
|
+
required = __structure_meta__[:required]
|
151
|
+
non_nullable = __structure_meta__[:non_nullable]
|
108
152
|
|
109
153
|
# Check for missing required attributes
|
110
154
|
required.each do |attr|
|
@@ -115,6 +159,8 @@ module Structure
|
|
115
159
|
end
|
116
160
|
|
117
161
|
mappings.each do |attr, from|
|
162
|
+
key_present = data.key?(from) || data.key?(from.to_sym)
|
163
|
+
|
118
164
|
value = data.fetch(from) do
|
119
165
|
data.fetch(from.to_sym) do
|
120
166
|
defaults[attr]
|
@@ -122,10 +168,16 @@ module Structure
|
|
122
168
|
end
|
123
169
|
|
124
170
|
if value
|
125
|
-
coercion =
|
171
|
+
coercion = coercions[attr]
|
126
172
|
value = coercion.call(value) if coercion
|
127
173
|
end
|
128
174
|
|
175
|
+
# Check non-null constraint after coercion
|
176
|
+
# Only check if key was present in data OR attribute has an explicit default
|
177
|
+
if value.nil? && non_nullable.include?(attr) && (key_present || defaults.key?(attr))
|
178
|
+
raise ArgumentError, "cannot be null: :#{attr}"
|
179
|
+
end
|
180
|
+
|
129
181
|
final[attr] = value
|
130
182
|
end
|
131
183
|
|
data/sig/structure/builder.rbs
CHANGED
@@ -5,12 +5,13 @@ module Structure
|
|
5
5
|
@defaults: Hash[Symbol, untyped]
|
6
6
|
@after_parse_callback: Proc?
|
7
7
|
@optional: Set[Symbol]
|
8
|
+
@non_nullable: Set[Symbol]
|
8
9
|
|
9
|
-
def attribute: (Symbol name, untyped type, ?from: String
|
10
|
-
| (Symbol name, ?from: String, ?default: untyped) ?{ (untyped) -> untyped } -> void
|
10
|
+
def attribute: (Symbol name, untyped type, ?from: String | Symbol | nil, ?default: untyped, ?null: bool) ?{ (untyped) -> untyped } -> void
|
11
|
+
| (Symbol name, ?from: String | Symbol | nil, ?default: untyped, ?null: bool) ?{ (untyped) -> untyped } -> void
|
11
12
|
|
12
|
-
def attribute?: (Symbol name, untyped type, ?from: String
|
13
|
-
| (Symbol name, ?from: String, ?default: untyped) ?{ (untyped) -> untyped } -> void
|
13
|
+
def attribute?: (Symbol name, untyped type, ?from: String | Symbol | nil, ?default: untyped, ?null: bool) ?{ (untyped) -> untyped } -> void
|
14
|
+
| (Symbol name, ?from: String | Symbol | nil, ?default: untyped, ?null: bool) ?{ (untyped) -> untyped } -> void
|
14
15
|
|
15
16
|
def after_parse: () { (Data) -> void } -> void
|
16
17
|
|
@@ -22,6 +23,10 @@ module Structure
|
|
22
23
|
def predicate_methods: () -> Hash[Symbol, Symbol]
|
23
24
|
def optional: () -> Array[Symbol]
|
24
25
|
def required: () -> Array[Symbol]
|
26
|
+
def non_nullable: () -> Array[Symbol]
|
25
27
|
def after_parse_callback: () -> (Proc | nil)
|
28
|
+
|
29
|
+
def method_missing: (Symbol, *untyped, **untyped) ?{ (*untyped, **untyped) -> untyped } -> untyped
|
30
|
+
def respond_to_missing?: (Symbol, bool) -> bool
|
26
31
|
end
|
27
32
|
end
|
data/sig/structure/rbs.rbs
CHANGED
@@ -1,10 +1,14 @@
|
|
1
1
|
module Structure
|
2
2
|
module RBS
|
3
|
+
EMPTY_CUSTOM_METHODS: { instance: Array[untyped], singleton: Array[untyped] }
|
4
|
+
|
3
5
|
def self.emit: (untyped klass) -> String?
|
4
6
|
def self.write: (untyped klass, ?dir: String) -> String?
|
5
7
|
|
6
|
-
private def self.emit_rbs_content: (class_name: String, attributes: Array[Symbol], types: Hash[Symbol, untyped], required: Array[Symbol], has_structure_modules: bool) -> String
|
8
|
+
private def self.emit_rbs_content: (class_name: String, attributes: Array[Symbol], types: Hash[Symbol, untyped], required: Array[Symbol], has_structure_modules: bool, custom_methods: untyped) -> String
|
7
9
|
private def self.parse_data_type: (untyped type, String class_name) -> String
|
8
10
|
private def self.map_type_to_rbs: (untyped type, String class_name) -> String
|
11
|
+
private def self.format_method_signature: (Hash[Symbol, untyped] method_meta, singleton: bool) -> String
|
12
|
+
private def self.build_parameters_fragment: (Array[[Symbol, untyped]] parameters) -> [String, String?]
|
9
13
|
end
|
10
14
|
end
|
data/sig/structure/types.rbs
CHANGED
data/sig/structure.rbs
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
module Structure
|
2
|
+
TO_H_CHECKER: Proc
|
3
|
+
|
2
4
|
interface _StructuredDataClass
|
3
5
|
def __structure_meta__: () -> {
|
4
6
|
types: Hash[Symbol, untyped],
|
@@ -10,5 +12,7 @@ module Structure
|
|
10
12
|
}
|
11
13
|
end
|
12
14
|
|
13
|
-
|
15
|
+
# Block evaluated twice: first for DSL methods (attribute calls), then for custom methods
|
16
|
+
# Context set to Builder so DSL methods are recognized
|
17
|
+
def self.new: () ?{ () [self: Structure::Builder] -> void } -> untyped
|
14
18
|
end
|