structure 3.4.0 → 3.5.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 +4 -7
- data/lib/structure/rbs.rb +58 -8
- data/lib/structure/types.rb +72 -61
- data/lib/structure/version.rb +1 -1
- data/lib/structure.rb +25 -9
- data/sig/structure/builder.rbs +6 -2
- data/sig/structure/rbs.rbs +5 -6
- data/sig/structure/types.rbs +8 -8
- data/sig/structure/version.rbs +3 -0
- data/sig/structure.rbs +4 -13
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 69015a521f4a1a35438cd69d0d195e8f0fa276156d64371c8a6437030b14caf0
|
4
|
+
data.tar.gz: d4e5c263a7c92ae2bfbdbe5213b933c55ea1a8f0c9a8d6583302f8c032bb69a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1e8ace0197fb9b9cd4811cc1eb7774fe98eff67df140400b567a0a636b9bdabcf2e7858586a7b13f96fbc95e101e2eef74518d9940b1f7c4dab168fec119fb3e
|
7
|
+
data.tar.gz: 9d7fa653f6b4aeb60b10bfca4a940677bee89c6803a8e50d893e9dc0ce687306691bbb6f92f4af0a59c89245c3583e8b6d64f477d27b44b71f0f1b7fc359bb26
|
data/lib/structure/builder.rb
CHANGED
@@ -33,16 +33,13 @@ module Structure
|
|
33
33
|
# Money.new(value["amount"], value["currency"])
|
34
34
|
# end
|
35
35
|
def attribute(name, type = nil, from: nil, default: nil, &block)
|
36
|
-
|
37
|
-
|
38
|
-
@defaults[name] = default unless default.nil?
|
36
|
+
mappings[name] = from || name.to_s
|
37
|
+
defaults[name] = default unless default.nil?
|
39
38
|
|
40
39
|
if type && block
|
41
40
|
raise ArgumentError, "Cannot specify both type and block for :#{name}"
|
42
|
-
|
43
|
-
|
44
|
-
elsif type
|
45
|
-
@types[name] = type
|
41
|
+
else
|
42
|
+
types[name] = type || block
|
46
43
|
end
|
47
44
|
end
|
48
45
|
|
data/lib/structure/rbs.rb
CHANGED
@@ -12,12 +12,13 @@ module Structure
|
|
12
12
|
class_name = klass.name
|
13
13
|
return unless class_name
|
14
14
|
|
15
|
+
# @type var meta: Hash[Symbol, untyped]
|
15
16
|
meta = klass.respond_to?(:__structure_meta__) ? klass.__structure_meta__ : {}
|
16
17
|
|
17
18
|
emit_rbs_content(
|
18
19
|
class_name: class_name,
|
19
20
|
attributes: meta.fetch(:attributes, klass.members),
|
20
|
-
types: meta.fetch(:types, {}),
|
21
|
+
types: meta.fetch(:types, {}), # steep:ignore
|
21
22
|
has_structure_modules: meta.any?,
|
22
23
|
)
|
23
24
|
end
|
@@ -35,20 +36,18 @@ module Structure
|
|
35
36
|
dir_path = dir_path.join(*path_segments) unless path_segments.empty?
|
36
37
|
FileUtils.mkdir_p(dir_path)
|
37
38
|
|
38
|
-
file_path = dir_path.join(filename)
|
39
|
+
file_path = dir_path.join(filename).to_s
|
39
40
|
File.write(file_path, rbs_content)
|
40
41
|
|
41
|
-
file_path
|
42
|
+
file_path
|
42
43
|
end
|
43
44
|
|
44
45
|
private
|
45
46
|
|
46
47
|
def emit_rbs_content(class_name:, attributes:, types:, has_structure_modules:)
|
48
|
+
# @type var lines: Array[String]
|
47
49
|
lines = []
|
48
50
|
lines << "class #{class_name} < Data"
|
49
|
-
lines << " extend Structure::ClassMethods" if has_structure_modules
|
50
|
-
lines << " include Structure::InstanceMethods"
|
51
|
-
lines << ""
|
52
51
|
|
53
52
|
unless attributes.empty?
|
54
53
|
# map types to rbs
|
@@ -66,6 +65,28 @@ module Structure
|
|
66
65
|
lines << " | (#{positional_params}) -> instance"
|
67
66
|
lines << ""
|
68
67
|
|
68
|
+
needs_parse_data = types.any? do |_attr, type|
|
69
|
+
type == :self || type == [:self] || (type.is_a?(Array) && type.first == :array)
|
70
|
+
end
|
71
|
+
|
72
|
+
if needs_parse_data
|
73
|
+
lines << " type parse_data = {"
|
74
|
+
attributes.each do |attr|
|
75
|
+
type = types.fetch(attr, nil)
|
76
|
+
parse_type = parse_data_type(type, class_name)
|
77
|
+
lines << " ?#{attr}: #{parse_type},"
|
78
|
+
end
|
79
|
+
lines[-1] = lines[-1].chomp(",")
|
80
|
+
lines << " }"
|
81
|
+
lines << ""
|
82
|
+
lines << " def self.parse: (?parse_data data) -> instance"
|
83
|
+
lines << " | (?Hash[String, untyped] data) -> instance"
|
84
|
+
else
|
85
|
+
# For structures without special types, just use Hash
|
86
|
+
lines << " def self.parse: (?(Hash[String | Symbol, untyped]), **untyped) -> instance"
|
87
|
+
end
|
88
|
+
lines << ""
|
89
|
+
|
69
90
|
attributes.each do |attr|
|
70
91
|
lines << " attr_reader #{attr}: #{rbs_types[attr]}"
|
71
92
|
end
|
@@ -85,6 +106,33 @@ module Structure
|
|
85
106
|
lines.join("\n")
|
86
107
|
end
|
87
108
|
|
109
|
+
def parse_data_type(type, class_name)
|
110
|
+
case type
|
111
|
+
when [:self]
|
112
|
+
"Array[#{class_name} | parse_data]"
|
113
|
+
when Array
|
114
|
+
if type.first == :array && type.last == :self
|
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
|
121
|
+
"Array[#{class_name} | parse_data]"
|
122
|
+
elsif type.size == 1
|
123
|
+
# Regular array type like [String], [Integer], etc.
|
124
|
+
# Use Array[untyped] since we coerce values
|
125
|
+
"Array[untyped]"
|
126
|
+
else
|
127
|
+
"untyped"
|
128
|
+
end
|
129
|
+
when :self
|
130
|
+
"#{class_name} | parse_data"
|
131
|
+
else
|
132
|
+
"untyped"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
88
136
|
def map_type_to_rbs(type, class_name)
|
89
137
|
case type
|
90
138
|
when Class
|
@@ -97,8 +145,10 @@ module Structure
|
|
97
145
|
if type.size == 2 && type.first == :array
|
98
146
|
element_type = map_type_to_rbs(type.last, class_name)
|
99
147
|
"Array[#{element_type}]"
|
100
|
-
elsif type ==
|
101
|
-
|
148
|
+
elsif type.size == 1
|
149
|
+
# Single element array means array of that type
|
150
|
+
element_type = map_type_to_rbs(type.first, class_name)
|
151
|
+
"Array[#{element_type}]"
|
102
152
|
else
|
103
153
|
"untyped"
|
104
154
|
end
|
data/lib/structure/types.rb
CHANGED
@@ -3,77 +3,88 @@
|
|
3
3
|
module Structure
|
4
4
|
# Type coercion methods for converting values to specific types
|
5
5
|
module Types
|
6
|
-
|
6
|
+
class << self
|
7
|
+
# Rails-style boolean truthy values
|
8
|
+
# Reference: https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
|
9
|
+
BOOLEAN_TRUTHY = [true, 1, "1", "t", "T", "true", "TRUE", "on", "ON"].freeze
|
10
|
+
private_constant :BOOLEAN_TRUTHY
|
7
11
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
if type.length == 1
|
37
|
-
array(type.first)
|
38
|
-
else
|
39
|
-
type
|
40
|
-
end
|
41
|
-
else
|
42
|
-
# Handle Class, Module, and any other types
|
43
|
-
if type.respond_to?(:parse)
|
44
|
-
parseable(type)
|
45
|
-
elsif type.respond_to?(:name) && type.name && Kernel.respond_to?(type.name)
|
46
|
-
kernel(type)
|
12
|
+
# Main factory method for creating type coercers
|
13
|
+
#
|
14
|
+
# @param type [Class, Symbol, Array] Type specification
|
15
|
+
# @return [Proc, Object] Coercion proc or the type itself if no coercion available
|
16
|
+
#
|
17
|
+
# @example Boolean type
|
18
|
+
# coerce(:boolean) # => boolean proc
|
19
|
+
#
|
20
|
+
# @example Kernel types
|
21
|
+
# coerce(Integer) # => proc that calls Kernel.Integer
|
22
|
+
#
|
23
|
+
# @example Parseable types
|
24
|
+
# coerce(Date) # => proc that calls Date.parse
|
25
|
+
#
|
26
|
+
# @example Array types
|
27
|
+
# coerce([String]) # => proc that coerces array elements to String
|
28
|
+
def coerce(type)
|
29
|
+
case type
|
30
|
+
when :boolean
|
31
|
+
boolean
|
32
|
+
when :self
|
33
|
+
self_referential
|
34
|
+
when Array
|
35
|
+
if type.length == 1
|
36
|
+
array(type.first)
|
37
|
+
else
|
38
|
+
type
|
39
|
+
end
|
47
40
|
else
|
48
|
-
type
|
41
|
+
if type.respond_to?(:parse)
|
42
|
+
parseable(type)
|
43
|
+
elsif type.respond_to?(:name) && type.name && Kernel.respond_to?(type.name)
|
44
|
+
kernel(type)
|
45
|
+
else
|
46
|
+
type
|
47
|
+
end
|
49
48
|
end
|
50
49
|
end
|
51
|
-
end
|
52
50
|
|
53
|
-
|
51
|
+
private
|
54
52
|
|
55
|
-
|
56
|
-
|
57
|
-
|
53
|
+
def boolean
|
54
|
+
@boolean ||= ->(val) { BOOLEAN_TRUTHY.include?(val) }
|
55
|
+
end
|
58
56
|
|
59
|
-
|
60
|
-
|
61
|
-
|
57
|
+
def self_referential
|
58
|
+
proc { |val| parse(val) }
|
59
|
+
end
|
62
60
|
|
63
|
-
|
64
|
-
|
65
|
-
|
61
|
+
def kernel(type)
|
62
|
+
->(val) { Kernel.send(type.name, val) }
|
63
|
+
end
|
66
64
|
|
67
|
-
|
68
|
-
|
69
|
-
|
65
|
+
def parseable(type)
|
66
|
+
->(val) { type.parse(val) }
|
67
|
+
end
|
70
68
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
69
|
+
def array(element_type)
|
70
|
+
if element_type == :self
|
71
|
+
proc do |value|
|
72
|
+
unless value.respond_to?(:map)
|
73
|
+
raise TypeError, "can't convert #{value.class} into Array"
|
74
|
+
end
|
75
|
+
|
76
|
+
value.map { |element| parse(element) }
|
77
|
+
end
|
78
|
+
else
|
79
|
+
element_coercer = coerce(element_type)
|
80
|
+
lambda do |value|
|
81
|
+
unless value.respond_to?(:map)
|
82
|
+
raise TypeError, "can't convert #{value.class} into Array"
|
83
|
+
end
|
84
|
+
|
85
|
+
value.map { |element| element_coercer.call(element) }
|
86
|
+
end
|
87
|
+
end
|
77
88
|
end
|
78
89
|
end
|
79
90
|
end
|
data/lib/structure/version.rb
CHANGED
data/lib/structure.rb
CHANGED
@@ -23,6 +23,7 @@ module Structure
|
|
23
23
|
builder = Builder.new
|
24
24
|
builder.instance_eval(&block) if block
|
25
25
|
|
26
|
+
# @type var klass: untyped
|
26
27
|
klass = Data.define(*builder.attributes)
|
27
28
|
|
28
29
|
# capture metadata and attach to class
|
@@ -47,8 +48,9 @@ module Structure
|
|
47
48
|
|
48
49
|
# recursive to_h
|
49
50
|
klass.define_method(:to_h) do
|
50
|
-
|
51
|
-
|
51
|
+
# @type var h: Hash[Symbol, untyped]
|
52
|
+
h = {}
|
53
|
+
klass.members.each do |m|
|
52
54
|
v = public_send(m)
|
53
55
|
h[m] =
|
54
56
|
case v
|
@@ -57,23 +59,38 @@ module Structure
|
|
57
59
|
else v
|
58
60
|
end
|
59
61
|
end
|
62
|
+
h
|
60
63
|
end
|
61
64
|
|
62
65
|
# parse accepts JSON-ish hashes + kwargs override
|
63
66
|
klass.define_singleton_method(:parse) do |data = {}, **kwargs|
|
64
|
-
|
65
|
-
|
67
|
+
return data if data.is_a?(self)
|
68
|
+
|
69
|
+
unless data.respond_to?(:merge!)
|
70
|
+
raise TypeError, "can't convert #{data.class} into #{self}"
|
71
|
+
end
|
66
72
|
|
67
|
-
|
73
|
+
# @type var kwargs: Hash[Symbol, untyped]
|
74
|
+
string_kwargs = kwargs.transform_keys(&:to_s)
|
75
|
+
data.merge!(string_kwargs)
|
76
|
+
# @type self: singleton(Data) & _StructuredDataClass
|
77
|
+
# @type var final: Hash[Symbol, untyped]
|
68
78
|
final = {}
|
79
|
+
|
80
|
+
# TODO: `__structure_meta__` exists but seems not to return the types it defines, so going untyped for now
|
81
|
+
#
|
82
|
+
# @type var meta: untyped
|
69
83
|
meta = __structure_meta__
|
70
84
|
|
71
|
-
meta
|
85
|
+
attributes = meta.fetch(:attributes)
|
86
|
+
defaults = meta.fetch(:defaults)
|
87
|
+
|
88
|
+
attributes.each do |attr|
|
72
89
|
source = mappings[attr] || attr.to_s
|
73
90
|
value =
|
74
91
|
if data.key?(source) then data[source]
|
75
92
|
elsif data.key?(source.to_sym) then data[source.to_sym]
|
76
|
-
elsif
|
93
|
+
elsif defaults.key?(attr) then defaults[attr]
|
77
94
|
end
|
78
95
|
|
79
96
|
coercion = coercions[attr]
|
@@ -81,7 +98,7 @@ module Structure
|
|
81
98
|
# self-referential types need class context to call parse
|
82
99
|
value =
|
83
100
|
if coercion.is_a?(Proc) && !coercion.lambda?
|
84
|
-
instance_exec(value, &coercion)
|
101
|
+
instance_exec(value, &coercion) # steep:ignore
|
85
102
|
else
|
86
103
|
coercion.call(value)
|
87
104
|
end
|
@@ -90,7 +107,6 @@ module Structure
|
|
90
107
|
final[attr] = value
|
91
108
|
end
|
92
109
|
|
93
|
-
#: untyped
|
94
110
|
obj = new(**final)
|
95
111
|
after&.call(obj)
|
96
112
|
obj
|
data/sig/structure/builder.rbs
CHANGED
@@ -1,11 +1,15 @@
|
|
1
1
|
module Structure
|
2
2
|
class Builder
|
3
|
-
|
3
|
+
@mappings: Hash[Symbol, String]
|
4
|
+
@types: Hash[Symbol, untyped]
|
5
|
+
@defaults: Hash[Symbol, untyped]
|
6
|
+
@after_parse_callback: Proc?
|
7
|
+
|
8
|
+
def attribute: (Symbol name, untyped type, ?from: String?, ?default: untyped) ?{ (untyped) -> untyped } -> void
|
4
9
|
| (Symbol name, ?from: String, ?default: untyped) ?{ (untyped) -> untyped } -> void
|
5
10
|
|
6
11
|
def after_parse: () { (Data) -> void } -> void
|
7
12
|
|
8
|
-
# Methods used by the factory after the block
|
9
13
|
def attributes: () -> Array[Symbol]
|
10
14
|
def mappings: () -> Hash[Symbol, String]
|
11
15
|
def types: () -> Hash[Symbol, untyped]
|
data/sig/structure/rbs.rbs
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
module Structure
|
2
2
|
module RBS
|
3
|
-
def self.emit: (
|
4
|
-
def self.write: (
|
3
|
+
def self.emit: (untyped klass) -> String?
|
4
|
+
def self.write: (untyped klass, ?dir: String) -> String?
|
5
5
|
|
6
|
-
private
|
7
|
-
|
8
|
-
def self.
|
9
|
-
def self.map_type_to_rbs: (untyped type, String class_name) -> String
|
6
|
+
private def self.emit_rbs_content: (class_name: String, attributes: Array[Symbol], types: Hash[Symbol, untyped], has_structure_modules: bool) -> String
|
7
|
+
private def self.parse_data_type: (untyped type, String class_name) -> String
|
8
|
+
private def self.map_type_to_rbs: (untyped type, String class_name) -> String
|
10
9
|
end
|
11
10
|
end
|
data/sig/structure/types.rbs
CHANGED
@@ -2,15 +2,15 @@ module Structure
|
|
2
2
|
module Types
|
3
3
|
BOOLEAN_TRUTHY: Array[untyped]
|
4
4
|
|
5
|
-
|
5
|
+
self.@boolean: Proc
|
6
6
|
|
7
|
-
|
7
|
+
def self.coerce: (untyped type) -> untyped
|
8
8
|
|
9
|
-
def self.boolean: () -> Proc
|
10
|
-
def self.self_referential: () -> Proc
|
11
|
-
def self.array: (untyped
|
12
|
-
def self.parseable: (
|
13
|
-
def self.kernel: (Class type) -> Proc
|
14
|
-
def self.parse: (untyped val) -> untyped
|
9
|
+
private def self.boolean: () -> Proc
|
10
|
+
private def self.self_referential: () -> Proc
|
11
|
+
private def self.array: (untyped type) -> Proc
|
12
|
+
private def self.parseable: (untyped type) -> Proc
|
13
|
+
private def self.kernel: (Class type) -> Proc
|
14
|
+
private def self.parse: (untyped val) -> untyped
|
15
15
|
end
|
16
16
|
end
|
data/sig/structure.rbs
CHANGED
@@ -1,21 +1,12 @@
|
|
1
1
|
module Structure
|
2
|
-
|
3
|
-
|
4
|
-
module ClassMethods
|
5
|
-
def parse: (?(Hash[String | Symbol, untyped] | nil) data, **untyped kwargs) -> instance
|
6
|
-
def members: () -> Array[Symbol]
|
2
|
+
interface _StructuredDataClass
|
7
3
|
def __structure_meta__: () -> {
|
8
4
|
attributes: Array[Symbol],
|
9
5
|
types: Hash[Symbol, untyped],
|
10
|
-
defaults: Hash[Symbol, untyped]
|
11
|
-
from: Hash[Symbol, String]
|
6
|
+
defaults: Hash[Symbol, untyped]
|
12
7
|
}
|
8
|
+
def parse: (?Hash[String | Symbol, untyped] data, **untyped kwargs) -> instance
|
13
9
|
end
|
14
10
|
|
15
|
-
|
16
|
-
def deconstruct: () -> Array[untyped]
|
17
|
-
def deconstruct_keys: (Array[Symbol]?) -> Hash[Symbol, untyped]
|
18
|
-
def with: (**untyped) -> instance
|
19
|
-
def to_h: () -> Hash[Symbol, untyped]
|
20
|
-
end
|
11
|
+
def self.new: () ?{ (Structure::Builder) [self: Structure::Builder] -> void } -> untyped
|
21
12
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: structure
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Hakan Ensari
|
@@ -24,6 +24,7 @@ files:
|
|
24
24
|
- sig/structure/builder.rbs
|
25
25
|
- sig/structure/rbs.rbs
|
26
26
|
- sig/structure/types.rbs
|
27
|
+
- sig/structure/version.rbs
|
27
28
|
licenses:
|
28
29
|
- MIT
|
29
30
|
metadata:
|