structure 3.6.2 → 3.7.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 +9 -4
- data/lib/structure/rbs.rb +17 -16
- data/lib/structure/types.rb +77 -80
- data/lib/structure/version.rb +1 -1
- data/lib/structure.rb +45 -71
- data/sig/structure/builder.rbs +1 -1
- data/sig/structure/types.rbs +12 -5
- data/sig/structure.rbs +1 -3
- 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: 272646d259a42b8a13d4c0fbc29072022da5260f5a7948d2f8dbaddb9f625adf
|
4
|
+
data.tar.gz: a3045255ecb35adc801f827c48a5c620dc90943aee453f474bae6ab85bc8b58d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4d5914cb2446f277c61369d156d8286cfc50b866423b7bc86523964216d76a677901848a3135a5f1061f0383569fb3ad03624c3806de9d7841281bd751e5aa16
|
7
|
+
data.tar.gz: 3684e0a22399da848c493eb6b837d0c8427f0dd6f24857a7f65812914366b448108da8af2b377fc937a08715ea13de633b3f538f6b843a288c65936123dd32b7
|
data/lib/structure/builder.rb
CHANGED
@@ -5,12 +5,14 @@ require "structure/types"
|
|
5
5
|
module Structure
|
6
6
|
# Builder class for accumulating attribute definitions
|
7
7
|
class Builder
|
8
|
-
|
8
|
+
# @api private
|
9
|
+
attr_reader :mappings, :defaults, :types, :after_parse_callback
|
9
10
|
|
11
|
+
# @api private
|
10
12
|
def initialize
|
11
13
|
@mappings = {}
|
12
|
-
@types = {}
|
13
14
|
@defaults = {}
|
15
|
+
@types = {}
|
14
16
|
end
|
15
17
|
|
16
18
|
# DSL method for defining attributes with optional type coercion
|
@@ -56,14 +58,17 @@ module Structure
|
|
56
58
|
@after_parse_callback = block
|
57
59
|
end
|
58
60
|
|
61
|
+
# @api private
|
59
62
|
def attributes
|
60
63
|
@mappings.keys
|
61
64
|
end
|
62
65
|
|
63
|
-
|
64
|
-
|
66
|
+
# @api private
|
67
|
+
def coercions(context = nil)
|
68
|
+
@types.transform_values { |type| Types.coerce(type, context) }
|
65
69
|
end
|
66
70
|
|
71
|
+
# @api private
|
67
72
|
def predicate_methods
|
68
73
|
@types.filter_map do |name, type|
|
69
74
|
if type == :boolean
|
data/lib/structure/rbs.rb
CHANGED
@@ -15,10 +15,13 @@ module Structure
|
|
15
15
|
# @type var meta: Hash[Symbol, untyped]
|
16
16
|
meta = klass.respond_to?(:__structure_meta__) ? klass.__structure_meta__ : {}
|
17
17
|
|
18
|
+
attributes = meta[:mappings] ? meta[:mappings].keys : klass.members
|
19
|
+
types = meta.fetch(:types, {}) # steep:ignore
|
20
|
+
|
18
21
|
emit_rbs_content(
|
19
|
-
class_name
|
20
|
-
attributes
|
21
|
-
types
|
22
|
+
class_name:,
|
23
|
+
attributes:,
|
24
|
+
types:,
|
22
25
|
has_structure_modules: meta.any?,
|
23
26
|
)
|
24
27
|
end
|
@@ -66,7 +69,7 @@ module Structure
|
|
66
69
|
lines << ""
|
67
70
|
|
68
71
|
needs_parse_data = types.any? do |_attr, type|
|
69
|
-
type == :self || type == [:self]
|
72
|
+
type == :self || type == [:self]
|
70
73
|
end
|
71
74
|
|
72
75
|
if needs_parse_data
|
@@ -111,13 +114,7 @@ module Structure
|
|
111
114
|
when [:self]
|
112
115
|
"Array[#{class_name} | parse_data]"
|
113
116
|
when Array
|
114
|
-
if type.
|
115
|
-
"Array[#{class_name} | parse_data]"
|
116
|
-
elsif type.first == :array
|
117
|
-
# For [:array, SomeType] format, use Array[untyped] since we coerce
|
118
|
-
"Array[untyped]"
|
119
|
-
elsif type.size == 1 && type.first == :self
|
120
|
-
# [:self] is handled above, this shouldn't happen
|
117
|
+
if type.size == 1 && type.first == :self
|
121
118
|
"Array[#{class_name} | parse_data]"
|
122
119
|
elsif type.size == 1
|
123
120
|
# Regular array type like [String], [Integer], etc.
|
@@ -136,16 +133,20 @@ module Structure
|
|
136
133
|
def map_type_to_rbs(type, class_name)
|
137
134
|
case type
|
138
135
|
when Class
|
139
|
-
type
|
136
|
+
if type == Array
|
137
|
+
"Array[untyped]"
|
138
|
+
elsif type == Hash
|
139
|
+
"Hash[untyped, untyped]"
|
140
|
+
else
|
141
|
+
type.name || "untyped"
|
142
|
+
end
|
143
|
+
|
140
144
|
when :boolean
|
141
145
|
"bool"
|
142
146
|
when :self
|
143
147
|
class_name || "untyped"
|
144
148
|
when Array
|
145
|
-
if type.size ==
|
146
|
-
element_type = map_type_to_rbs(type.last, class_name)
|
147
|
-
"Array[#{element_type}]"
|
148
|
-
elsif type.size == 1
|
149
|
+
if type.size == 1
|
149
150
|
# Single element array means array of that type
|
150
151
|
element_type = map_type_to_rbs(type.first, class_name)
|
151
152
|
"Array[#{element_type}]"
|
data/lib/structure/types.rb
CHANGED
@@ -11,130 +11,98 @@ module Structure
|
|
11
11
|
|
12
12
|
# Main factory method for creating type coercers
|
13
13
|
#
|
14
|
-
# @param type [Class, Symbol, Array, String] Type specification
|
15
|
-
# @
|
14
|
+
# @param type [Class, Symbol, Array, String, nil] Type specification
|
15
|
+
# @param context [Class, nil] Context class for lazy-loading and self-referential types
|
16
|
+
# @return [Proc, nil] Coercion proc or nil if no coercion needed
|
17
|
+
# @raise [ArgumentError] If type is a Hash instance (typed hashes not yet supported) or unsupported type
|
16
18
|
#
|
17
19
|
# @example Boolean type
|
18
20
|
# coerce(:boolean) # => boolean proc
|
19
21
|
#
|
22
|
+
# @example Self-referential types
|
23
|
+
# coerce(:self) # => proc that calls context.parse
|
24
|
+
#
|
20
25
|
# @example Kernel types
|
21
26
|
# coerce(Integer) # => proc that calls Kernel.Integer
|
22
27
|
#
|
28
|
+
# @example Array types
|
29
|
+
# coerce([String]) # => proc that coerces array elements to String
|
30
|
+
#
|
23
31
|
# @example Parseable types
|
24
32
|
# coerce(Date) # => proc that calls Date.parse
|
25
33
|
#
|
26
|
-
# @example
|
27
|
-
# coerce(
|
34
|
+
# @example No coercion
|
35
|
+
# coerce(nil) # => nil
|
36
|
+
#
|
37
|
+
# @example Custom lambdas
|
38
|
+
# coerce(->(val) { val.upcase }) # => returns the lambda itself
|
28
39
|
#
|
29
|
-
# @example
|
40
|
+
# @example Lazy-resolved classes
|
30
41
|
# coerce("MyClass") # => proc that resolves and coerces to MyClass
|
31
|
-
def coerce(type,
|
42
|
+
def coerce(type, context = nil)
|
32
43
|
case type
|
33
44
|
when :boolean
|
34
45
|
boolean
|
35
46
|
when :self
|
36
|
-
self_referential
|
47
|
+
self_referential(context)
|
48
|
+
when ->(t) { t.respond_to?(:name) && t.name && Kernel.respond_to?(t.name) }
|
49
|
+
kernel(type)
|
50
|
+
when ->(t) { t.is_a?(Array) && t.length == 1 }
|
51
|
+
array(type.first, context)
|
52
|
+
when ->(t) { t.respond_to?(:parse) }
|
53
|
+
parseable(type)
|
54
|
+
when nil
|
55
|
+
type
|
56
|
+
when ->(t) { t.respond_to?(:call) }
|
57
|
+
type
|
37
58
|
when String
|
38
|
-
|
39
|
-
when Array
|
40
|
-
if type.length == 1
|
41
|
-
array(type.first, context_class)
|
42
|
-
else
|
43
|
-
type
|
44
|
-
end
|
59
|
+
lazy_class(type, context)
|
45
60
|
else
|
46
|
-
|
47
|
-
parseable(type)
|
48
|
-
elsif type.respond_to?(:name) && type.name && Kernel.respond_to?(type.name)
|
49
|
-
kernel(type)
|
50
|
-
else
|
51
|
-
type
|
52
|
-
end
|
61
|
+
raise ArgumentError, "Cannot specify #{type.inspect} as type"
|
53
62
|
end
|
54
63
|
end
|
55
64
|
|
56
|
-
def resolve_class(class_name, context_class)
|
57
|
-
if context_class && defined?(context_class.name)
|
58
|
-
namespace = context_class.name.to_s.split("::")[0...-1]
|
59
|
-
if namespace.any?
|
60
|
-
begin
|
61
|
-
namespace.reduce(Object) { |mod, name| mod.const_get(name) }.const_get(class_name)
|
62
|
-
rescue NameError
|
63
|
-
Object.const_get(class_name)
|
64
|
-
end
|
65
|
-
else
|
66
|
-
Object.const_get(class_name)
|
67
|
-
end
|
68
|
-
else
|
69
|
-
Object.const_get(class_name)
|
70
|
-
end
|
71
|
-
rescue NameError => e
|
72
|
-
raise NameError, "Unable to resolve class '#{class_name}': #{e.message}"
|
73
|
-
end
|
74
|
-
|
75
65
|
private
|
76
66
|
|
77
67
|
def boolean
|
78
|
-
->(val) { BOOLEAN_TRUTHY.include?(val) }
|
79
|
-
end
|
80
|
-
|
81
|
-
def self_referential
|
82
|
-
proc { |val| parse(val) }
|
68
|
+
@boolean ||= ->(val) { BOOLEAN_TRUTHY.include?(val) }
|
83
69
|
end
|
84
70
|
|
85
|
-
def
|
86
|
-
->(val) {
|
87
|
-
end
|
88
|
-
|
89
|
-
def parseable(type)
|
90
|
-
->(val) { type.parse(val) }
|
71
|
+
def self_referential(context)
|
72
|
+
->(val) { context.parse(val) }
|
91
73
|
end
|
92
74
|
|
93
|
-
def
|
75
|
+
def lazy_class(class_name, context)
|
94
76
|
resolved_class = nil
|
95
|
-
mutex = Mutex.new
|
96
77
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
if resolved_class.respond_to?(:parse)
|
105
|
-
resolved_class.parse(value) # steep:ignore
|
106
|
-
else
|
107
|
-
value
|
108
|
-
end
|
78
|
+
->(value) do
|
79
|
+
resolved_class ||= resolve_class(class_name, context)
|
80
|
+
# @type var resolved_class: untyped
|
81
|
+
resolved_class.parse(value)
|
109
82
|
end
|
110
83
|
end
|
111
84
|
|
112
|
-
def array(element_type,
|
113
|
-
|
114
|
-
|
85
|
+
def array(element_type, context = nil)
|
86
|
+
case element_type
|
87
|
+
when :self
|
88
|
+
lambda do |value|
|
115
89
|
unless value.respond_to?(:map)
|
116
90
|
raise TypeError, "can't convert #{value.class} into Array"
|
117
91
|
end
|
118
92
|
|
119
|
-
value.map { |element| parse(element) }
|
93
|
+
value.map { |element| context.parse(element) }
|
120
94
|
end
|
121
|
-
|
122
|
-
|
95
|
+
when String
|
96
|
+
lambda do |value|
|
123
97
|
unless value.respond_to?(:map)
|
124
98
|
raise TypeError, "can't convert #{value.class} into Array"
|
125
99
|
end
|
126
100
|
|
127
|
-
resolved_class =
|
128
|
-
value.map
|
129
|
-
if resolved_class.respond_to?(:parse)
|
130
|
-
resolved_class.parse(element)
|
131
|
-
else
|
132
|
-
element
|
133
|
-
end
|
134
|
-
end
|
101
|
+
resolved_class = resolve_class(element_type, context)
|
102
|
+
value.map { |element| resolved_class.parse(element) }
|
135
103
|
end
|
136
104
|
else
|
137
|
-
element_coercer = coerce(element_type,
|
105
|
+
element_coercer = coerce(element_type, context)
|
138
106
|
lambda do |value|
|
139
107
|
unless value.respond_to?(:map)
|
140
108
|
raise TypeError, "can't convert #{value.class} into Array"
|
@@ -144,6 +112,35 @@ module Structure
|
|
144
112
|
end
|
145
113
|
end
|
146
114
|
end
|
115
|
+
|
116
|
+
def parseable(type)
|
117
|
+
@parseable_cache ||= {} # : Hash[untyped, Proc]
|
118
|
+
@parseable_cache[type] ||= ->(val) { type.parse(val) }
|
119
|
+
end
|
120
|
+
|
121
|
+
def kernel(type)
|
122
|
+
@kernel_cache ||= {} # : Hash[untyped, Proc]
|
123
|
+
@kernel_cache[type] ||= ->(val) { Kernel.send(type.name, val) }
|
124
|
+
end
|
125
|
+
|
126
|
+
def resolve_class(class_name, context)
|
127
|
+
if context && defined?(context.name)
|
128
|
+
namespace = context.name.to_s.split("::")[0...-1]
|
129
|
+
if namespace.any?
|
130
|
+
begin
|
131
|
+
namespace.reduce(Object) { |mod, name| mod.const_get(name) }.const_get(class_name)
|
132
|
+
rescue NameError
|
133
|
+
Object.const_get(class_name)
|
134
|
+
end
|
135
|
+
else
|
136
|
+
Object.const_get(class_name)
|
137
|
+
end
|
138
|
+
else
|
139
|
+
Object.const_get(class_name)
|
140
|
+
end
|
141
|
+
rescue NameError => e
|
142
|
+
raise NameError, "Unable to resolve class '#{class_name}': #{e.message}"
|
143
|
+
end
|
147
144
|
end
|
148
145
|
end
|
149
146
|
end
|
data/lib/structure/version.rb
CHANGED
data/lib/structure.rb
CHANGED
@@ -26,98 +26,72 @@ module Structure
|
|
26
26
|
# @type var klass: untyped
|
27
27
|
klass = Data.define(*builder.attributes)
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
predicates = builder.predicate_methods
|
33
|
-
after = builder.after_parse_callback
|
29
|
+
builder.predicate_methods.each do |pred, attr|
|
30
|
+
klass.define_method(pred) { !!public_send(attr) }
|
31
|
+
end
|
34
32
|
|
33
|
+
# Store metadata on class to avoid closure capture (memory optimization)
|
35
34
|
meta = {
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
predicates: predicates.freeze,
|
42
|
-
after: after,
|
35
|
+
types: builder.types,
|
36
|
+
defaults: builder.defaults,
|
37
|
+
mappings: builder.mappings,
|
38
|
+
coercions: builder.coercions(klass),
|
39
|
+
after_parse: builder.after_parse_callback,
|
43
40
|
}.freeze
|
44
41
|
klass.instance_variable_set(:@__structure_meta__, meta)
|
45
42
|
klass.singleton_class.attr_reader(:__structure_meta__)
|
46
43
|
|
47
|
-
# Define predicate methods
|
48
|
-
predicates.each do |pred, attr|
|
49
|
-
klass.define_method(pred) { !!public_send(attr) }
|
50
|
-
end
|
51
|
-
|
52
44
|
# recursive to_h
|
53
45
|
klass.define_method(:to_h) do
|
54
|
-
|
55
|
-
h = {}
|
56
|
-
klass.members.each do |m|
|
46
|
+
klass.members.to_h do |m|
|
57
47
|
v = public_send(m)
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
48
|
+
value = case v
|
49
|
+
when Array then v.map { |x| x.respond_to?(:to_h) && x ? x.to_h : x }
|
50
|
+
when ->(x) { x.respond_to?(:to_h) && x } then v.to_h
|
51
|
+
else v
|
52
|
+
end
|
53
|
+
[m, value]
|
64
54
|
end
|
65
|
-
h
|
66
55
|
end
|
67
56
|
|
68
|
-
# parse accepts JSON-ish hashes +
|
69
|
-
|
70
|
-
|
71
|
-
|
57
|
+
# parse accepts JSON-ish hashes + optional overrides hash
|
58
|
+
# overrides is a positional arg (not **kwargs) to avoid hash allocation when unused
|
59
|
+
#
|
60
|
+
# @type self: singleton(Data) & _StructuredDataClass
|
61
|
+
# @type var final: Hash[Symbol, untyped]
|
62
|
+
klass.singleton_class.define_method(:parse) do |data = {}, overrides = nil|
|
63
|
+
return data if data.is_a?(self)
|
72
64
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
# @type var kwargs: Hash[Symbol, untyped]
|
78
|
-
string_kwargs = kwargs.transform_keys(&:to_s)
|
79
|
-
data.merge!(string_kwargs)
|
80
|
-
# @type self: singleton(Data) & _StructuredDataClass
|
81
|
-
# @type var final: Hash[Symbol, untyped]
|
82
|
-
final = {}
|
83
|
-
|
84
|
-
# @type var meta: untyped
|
85
|
-
meta = __structure_meta__
|
65
|
+
unless data.respond_to?(:merge!)
|
66
|
+
raise TypeError, "can't convert #{data.class} into #{self}"
|
67
|
+
end
|
86
68
|
|
87
|
-
|
88
|
-
defaults = meta.fetch(:defaults)
|
89
|
-
mappings = meta.fetch(:mappings)
|
90
|
-
coercions = meta.fetch(:coercions)
|
91
|
-
after = meta.fetch(:after)
|
69
|
+
overrides&.each { |k, v| data[k.to_s] = v }
|
92
70
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
elsif data.key?(source.to_sym) then data[source.to_sym]
|
98
|
-
elsif defaults.key?(attr) then defaults[attr]
|
99
|
-
end
|
71
|
+
final = {}
|
72
|
+
mappings = __structure_meta__[:mappings]
|
73
|
+
defaults = __structure_meta__[:defaults]
|
74
|
+
after_parse = __structure_meta__[:after_parse]
|
100
75
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
value =
|
106
|
-
if coercion.is_a?(Proc) && !coercion.lambda?
|
107
|
-
instance_exec(value, &coercion) # steep:ignore
|
108
|
-
else
|
109
|
-
coercion.call(value)
|
110
|
-
end
|
76
|
+
mappings.each do |attr, from|
|
77
|
+
value = data.fetch(from) do
|
78
|
+
data.fetch(from.to_sym) do
|
79
|
+
defaults[attr]
|
111
80
|
end
|
81
|
+
end
|
112
82
|
|
113
|
-
|
83
|
+
if value
|
84
|
+
coercion = __structure_meta__[:coercions][attr]
|
85
|
+
value = coercion.call(value) if coercion
|
114
86
|
end
|
115
87
|
|
116
|
-
|
117
|
-
after&.call(obj) if after
|
118
|
-
obj
|
88
|
+
final[attr] = value
|
119
89
|
end
|
120
|
-
|
90
|
+
|
91
|
+
obj = new(**final)
|
92
|
+
after_parse&.call(obj)
|
93
|
+
obj
|
94
|
+
end
|
121
95
|
|
122
96
|
klass
|
123
97
|
end
|
data/sig/structure/builder.rbs
CHANGED
@@ -14,7 +14,7 @@ module Structure
|
|
14
14
|
def mappings: () -> Hash[Symbol, String]
|
15
15
|
def types: () -> Hash[Symbol, untyped]
|
16
16
|
def defaults: () -> Hash[Symbol, untyped]
|
17
|
-
def coercions: (?untyped?
|
17
|
+
def coercions: (?untyped? context) -> Hash[Symbol, Proc]
|
18
18
|
def predicate_methods: () -> Hash[Symbol, Symbol]
|
19
19
|
def after_parse_callback: () -> (Proc | nil)
|
20
20
|
end
|
data/sig/structure/types.rbs
CHANGED
@@ -1,18 +1,25 @@
|
|
1
1
|
module Structure
|
2
2
|
module Types
|
3
|
+
interface _ParseableClass
|
4
|
+
def parse: (untyped) -> untyped
|
5
|
+
end
|
6
|
+
|
3
7
|
BOOLEAN_TRUTHY: Array[untyped]
|
4
8
|
|
5
9
|
self.@boolean: Proc
|
10
|
+
self.@kernel_cache: Hash[untyped, Proc]
|
11
|
+
self.@parseable_cache: Hash[untyped, Proc]
|
12
|
+
self.@string_class_cache: Hash[Array[untyped], Proc]
|
6
13
|
|
7
|
-
def self.coerce: (untyped type, ?untyped?
|
8
|
-
def self.resolve_class: (String class_name, untyped? context_class) -> untyped
|
14
|
+
def self.coerce: (untyped type, ?untyped? context) -> untyped
|
9
15
|
|
10
16
|
private def self.boolean: () -> Proc
|
11
|
-
private def self.self_referential: () -> Proc
|
12
|
-
private def self.
|
13
|
-
private def self.array: (untyped element_type, ?untyped?
|
17
|
+
private def self.self_referential: (_ParseableClass context) -> Proc
|
18
|
+
private def self.lazy_class: (String class_name, untyped? context) -> Proc
|
19
|
+
private def self.array: (untyped element_type, ?untyped? context) -> Proc
|
14
20
|
private def self.parseable: (untyped type) -> Proc
|
15
21
|
private def self.kernel: (Class type) -> Proc
|
16
22
|
private def self.parse: (untyped val) -> untyped
|
23
|
+
private def self.resolve_class: (String class_name, untyped? context) -> _ParseableClass
|
17
24
|
end
|
18
25
|
end
|
data/sig/structure.rbs
CHANGED
@@ -1,13 +1,11 @@
|
|
1
1
|
module Structure
|
2
2
|
interface _StructuredDataClass
|
3
3
|
def __structure_meta__: () -> {
|
4
|
-
attributes: Array[Symbol],
|
5
4
|
types: Hash[Symbol, untyped],
|
6
5
|
defaults: Hash[Symbol, untyped],
|
7
6
|
mappings: Hash[Symbol, String],
|
8
7
|
coercions: Hash[Symbol, untyped],
|
9
|
-
|
10
|
-
after: untyped
|
8
|
+
after_parse: untyped
|
11
9
|
}
|
12
10
|
end
|
13
11
|
|