structure 3.6.3 → 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 +6 -3
- data/lib/structure/types.rb +72 -75
- data/lib/structure/version.rb +1 -1
- data/lib/structure.rb +36 -60
- 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
|
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
|
37
|
-
when
|
38
|
-
|
47
|
+
self_referential(context)
|
48
|
+
when ->(t) { t.respond_to?(:name) && t.name && Kernel.respond_to?(t.name) }
|
49
|
+
kernel(type)
|
39
50
|
when ->(t) { t.is_a?(Array) && t.length == 1 }
|
40
|
-
array(type.first,
|
41
|
-
when Hash
|
42
|
-
raise ArgumentError, "Cannot specify #{type.inspect} as type"
|
51
|
+
array(type.first, context)
|
43
52
|
when ->(t) { t.respond_to?(:parse) }
|
44
53
|
parseable(type)
|
45
|
-
when ->(t) { t.respond_to?(:name) && t.name && Kernel.respond_to?(t.name) }
|
46
|
-
kernel(type)
|
47
|
-
when ->(t) { t.respond_to?(:call) }
|
48
|
-
type
|
49
54
|
when nil
|
50
55
|
type
|
56
|
+
when ->(t) { t.respond_to?(:call) }
|
57
|
+
type
|
58
|
+
when String
|
59
|
+
lazy_class(type, context)
|
51
60
|
else
|
52
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) {
|
71
|
+
def self_referential(context)
|
72
|
+
->(val) { context.parse(val) }
|
87
73
|
end
|
88
74
|
|
89
|
-
def
|
90
|
-
->(val) { type.parse(val) }
|
91
|
-
end
|
92
|
-
|
93
|
-
def string_class(class_name, context_class)
|
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,94 +26,70 @@ 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
|
-
|
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|
|
70
63
|
return data if data.is_a?(self)
|
71
64
|
|
72
65
|
unless data.respond_to?(:merge!)
|
73
66
|
raise TypeError, "can't convert #{data.class} into #{self}"
|
74
67
|
end
|
75
68
|
|
76
|
-
|
77
|
-
string_kwargs = kwargs.transform_keys(&:to_s)
|
78
|
-
data.merge!(string_kwargs)
|
79
|
-
# @type self: singleton(Data) & _StructuredDataClass
|
80
|
-
# @type var final: Hash[Symbol, untyped]
|
81
|
-
final = {}
|
69
|
+
overrides&.each { |k, v| data[k.to_s] = v }
|
82
70
|
|
83
|
-
|
84
|
-
|
71
|
+
final = {}
|
72
|
+
mappings = __structure_meta__[:mappings]
|
73
|
+
defaults = __structure_meta__[:defaults]
|
74
|
+
after_parse = __structure_meta__[:after_parse]
|
85
75
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
after = meta.fetch(:after)
|
91
|
-
|
92
|
-
attributes.each do |attr|
|
93
|
-
source = mappings[attr] || attr.to_s
|
94
|
-
value =
|
95
|
-
if data.key?(source) then data[source]
|
96
|
-
elsif data.key?(source.to_sym) then data[source.to_sym]
|
97
|
-
elsif defaults.key?(attr) then defaults[attr]
|
76
|
+
mappings.each do |attr, from|
|
77
|
+
value = data.fetch(from) do
|
78
|
+
data.fetch(from.to_sym) do
|
79
|
+
defaults[attr]
|
98
80
|
end
|
81
|
+
end
|
99
82
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
# Lambdas and other callables use direct invocation
|
104
|
-
value =
|
105
|
-
if coercion.is_a?(Proc) && !coercion.lambda?
|
106
|
-
instance_exec(value, &coercion) # steep:ignore
|
107
|
-
else
|
108
|
-
coercion.call(value)
|
109
|
-
end
|
83
|
+
if value
|
84
|
+
coercion = __structure_meta__[:coercions][attr]
|
85
|
+
value = coercion.call(value) if coercion
|
110
86
|
end
|
111
87
|
|
112
88
|
final[attr] = value
|
113
89
|
end
|
114
90
|
|
115
91
|
obj = new(**final)
|
116
|
-
|
92
|
+
after_parse&.call(obj)
|
117
93
|
obj
|
118
94
|
end
|
119
95
|
|
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
|
|