structure 3.7.0 → 4.1.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 +27 -3
- data/lib/structure/rbs.rb +45 -19
- data/lib/structure/version.rb +1 -1
- data/lib/structure.rb +41 -0
- data/sig/structure/builder.rbs +6 -0
- data/sig/structure/rbs.rbs +1 -1
- data/sig/structure.rbs +2 -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: 74f52e94d4ffd40ad55e291a4ab72c108af572be936ee2d1571fef47699af2cb
|
|
4
|
+
data.tar.gz: d2ef8dee35e2ea236237d1c34c68bc6877039145553f7e1b16729ea84e3778c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f0c4192ddde552c7cd4a583aca9bdad31aeb64dc039680e965944696ee7648cc9a423017ab8cea5546c708ae72b7920c21fb2c96e74442932f24a3a85faa6b81
|
|
7
|
+
data.tar.gz: 03d4ea5fda959cd3bcd60b79a323b8dbfaeb5792c22b809179b7f84f76aab36845e7dbcd2f30567cf7922eea9995997cfb730cd69e8dd726cebded2bd076f540
|
data/lib/structure/builder.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Structure
|
|
|
13
13
|
@mappings = {}
|
|
14
14
|
@defaults = {}
|
|
15
15
|
@types = {}
|
|
16
|
+
@optional = Set.new
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
# DSL method for defining attributes with optional type coercion
|
|
@@ -45,6 +46,25 @@ module Structure
|
|
|
45
46
|
end
|
|
46
47
|
end
|
|
47
48
|
|
|
49
|
+
# DSL method for defining optional attributes (key can be missing from input hash)
|
|
50
|
+
#
|
|
51
|
+
# @param name [Symbol] The attribute name
|
|
52
|
+
# @param type [Class, Symbol, Array, nil] Type for coercion (e.g., String, :boolean, [String])
|
|
53
|
+
# @param from [String, nil] Source key in the data hash (defaults to name.to_s)
|
|
54
|
+
# @param default [Object, nil] Default value if attribute is missing
|
|
55
|
+
# @yield [value] Block for custom transformation
|
|
56
|
+
# @raise [ArgumentError] If both type and block are provided
|
|
57
|
+
#
|
|
58
|
+
# @example Optional attribute
|
|
59
|
+
# attribute? :age, Integer
|
|
60
|
+
#
|
|
61
|
+
# @example Optional with default
|
|
62
|
+
# attribute? :status, String, default: "pending"
|
|
63
|
+
def attribute?(name, type = nil, from: nil, default: nil, &block)
|
|
64
|
+
attribute(name, type, from: from, default: default, &block)
|
|
65
|
+
@optional.add(name)
|
|
66
|
+
end
|
|
67
|
+
|
|
48
68
|
# Defines a callback to run after parsing
|
|
49
69
|
#
|
|
50
70
|
# @yield [instance] Block that receives the parsed instance
|
|
@@ -59,9 +79,13 @@ module Structure
|
|
|
59
79
|
end
|
|
60
80
|
|
|
61
81
|
# @api private
|
|
62
|
-
def attributes
|
|
63
|
-
|
|
64
|
-
|
|
82
|
+
def attributes = @mappings.keys
|
|
83
|
+
|
|
84
|
+
# @api private
|
|
85
|
+
def optional = @optional.to_a
|
|
86
|
+
|
|
87
|
+
# @api private
|
|
88
|
+
def required = attributes - optional
|
|
65
89
|
|
|
66
90
|
# @api private
|
|
67
91
|
def coercions(context = nil)
|
data/lib/structure/rbs.rb
CHANGED
|
@@ -4,6 +4,10 @@ require "fileutils"
|
|
|
4
4
|
require "pathname"
|
|
5
5
|
|
|
6
6
|
module Structure
|
|
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.
|
|
7
11
|
module RBS
|
|
8
12
|
class << self
|
|
9
13
|
def emit(klass)
|
|
@@ -17,11 +21,13 @@ module Structure
|
|
|
17
21
|
|
|
18
22
|
attributes = meta[:mappings] ? meta[:mappings].keys : klass.members
|
|
19
23
|
types = meta.fetch(:types, {}) # steep:ignore
|
|
24
|
+
required = meta.fetch(:required, attributes) # steep:ignore
|
|
20
25
|
|
|
21
26
|
emit_rbs_content(
|
|
22
27
|
class_name:,
|
|
23
28
|
attributes:,
|
|
24
29
|
types:,
|
|
30
|
+
required:,
|
|
25
31
|
has_structure_modules: meta.any?,
|
|
26
32
|
)
|
|
27
33
|
end
|
|
@@ -47,7 +53,7 @@ module Structure
|
|
|
47
53
|
|
|
48
54
|
private
|
|
49
55
|
|
|
50
|
-
def emit_rbs_content(class_name:, attributes:, types:, has_structure_modules:)
|
|
56
|
+
def emit_rbs_content(class_name:, attributes:, types:, required:, has_structure_modules:)
|
|
51
57
|
# @type var lines: Array[String]
|
|
52
58
|
lines = []
|
|
53
59
|
lines << "class #{class_name} < Data"
|
|
@@ -61,46 +67,66 @@ module Structure
|
|
|
61
67
|
[attr, rbs_type != "untyped" ? "#{rbs_type}?" : rbs_type]
|
|
62
68
|
end.to_h
|
|
63
69
|
|
|
64
|
-
|
|
70
|
+
# Sort keyword params: required first, then optional (with ? prefix)
|
|
71
|
+
# Within each group, maintain declaration order
|
|
72
|
+
required_params = required.map { |attr| "#{attr}: #{rbs_types[attr]}" }
|
|
73
|
+
optional_params = (attributes - required).map { |attr| "?#{attr}: #{rbs_types[attr]}" }
|
|
74
|
+
keyword_params = (required_params + optional_params).join(", ")
|
|
65
75
|
positional_params = attributes.map { |attr| rbs_types[attr] }.join(", ")
|
|
66
76
|
|
|
67
|
-
lines << " def self.new: (#{keyword_params}) -> #{class_name}"
|
|
68
|
-
lines << " | (#{positional_params}) -> #{class_name}"
|
|
69
|
-
lines << ""
|
|
70
|
-
|
|
71
77
|
needs_parse_data = types.any? do |_attr, type|
|
|
72
78
|
type == :self || type == [:self]
|
|
73
79
|
end
|
|
74
80
|
|
|
81
|
+
# Generate type alias first if needed (RBS::Sorter puts types at top)
|
|
75
82
|
if needs_parse_data
|
|
76
|
-
lines << " type parse_data = {"
|
|
77
|
-
attributes.each do |attr|
|
|
83
|
+
lines << " type parse_data = { " + attributes.map { |attr|
|
|
78
84
|
type = types.fetch(attr, nil)
|
|
79
85
|
parse_type = parse_data_type(type, class_name)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
"?#{attr}: #{parse_type}"
|
|
87
|
+
}.join(", ") + " }"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
lines << " def self.new: (#{keyword_params}) -> #{class_name}"
|
|
91
|
+
lines << " | (#{positional_params}) -> #{class_name}"
|
|
92
|
+
lines << ""
|
|
93
|
+
lines << " def self.[]: (#{keyword_params}) -> #{class_name}"
|
|
94
|
+
lines << " | (#{positional_params}) -> #{class_name}"
|
|
95
|
+
lines << ""
|
|
96
|
+
|
|
97
|
+
# Generate members tuple type
|
|
98
|
+
members_tuple = attributes.map { |attr| ":#{attr}" }.join(", ")
|
|
99
|
+
lines << " def self.members: () -> [ #{members_tuple} ]"
|
|
100
|
+
lines << ""
|
|
101
|
+
|
|
102
|
+
# Generate parse method signatures
|
|
103
|
+
if needs_parse_data
|
|
85
104
|
lines << " def self.parse: (?parse_data data) -> #{class_name}"
|
|
86
105
|
lines << " | (?Hash[String, untyped] data) -> #{class_name}"
|
|
87
106
|
else
|
|
88
|
-
#
|
|
89
|
-
lines << " def self.parse: (?
|
|
107
|
+
# Remove optional parentheses to match RBS::Sorter style
|
|
108
|
+
lines << " def self.parse: (?Hash[String | Symbol, untyped], **untyped) -> #{class_name}"
|
|
90
109
|
end
|
|
91
110
|
lines << ""
|
|
92
111
|
|
|
93
|
-
|
|
112
|
+
# Sort attr_reader lines alphabetically (RBS::Sorter does this)
|
|
113
|
+
attributes.sort.each do |attr|
|
|
94
114
|
lines << " attr_reader #{attr}: #{rbs_types[attr]}"
|
|
95
115
|
end
|
|
96
|
-
lines << ""
|
|
97
116
|
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
# Add boolean predicates
|
|
118
|
+
boolean_predicates = types.sort.select { |attr, type| type == :boolean && !attr.to_s.end_with?("?") }
|
|
119
|
+
unless boolean_predicates.empty?
|
|
120
|
+
lines << ""
|
|
121
|
+
boolean_predicates.each do |attr, _type|
|
|
100
122
|
lines << " def #{attr}?: () -> bool"
|
|
101
123
|
end
|
|
102
124
|
end
|
|
103
125
|
|
|
126
|
+
# Instance members method comes after attr_readers and predicates
|
|
127
|
+
lines << " def members: () -> [ #{members_tuple} ]"
|
|
128
|
+
lines << ""
|
|
129
|
+
|
|
104
130
|
hash_type = attributes.map { |attr| "#{attr}: #{rbs_types[attr]}" }.join(", ")
|
|
105
131
|
lines << " def to_h: () -> { #{hash_type} }"
|
|
106
132
|
end
|
data/lib/structure/version.rb
CHANGED
data/lib/structure.rb
CHANGED
|
@@ -26,6 +26,37 @@ module Structure
|
|
|
26
26
|
# @type var klass: untyped
|
|
27
27
|
klass = Data.define(*builder.attributes)
|
|
28
28
|
|
|
29
|
+
# Enable custom method definitions by evaluating block on the class
|
|
30
|
+
if block
|
|
31
|
+
# Provide temporary dummy DSL methods to prevent NoMethodError during class_eval
|
|
32
|
+
klass.define_singleton_method(:attribute) { |*args, **kwargs, &blk| }
|
|
33
|
+
klass.define_singleton_method(:attribute?) { |*args, **kwargs, &blk| }
|
|
34
|
+
klass.define_singleton_method(:after_parse) { |&blk| }
|
|
35
|
+
|
|
36
|
+
# Evaluate block in class context for method definitions
|
|
37
|
+
klass.class_eval(&block)
|
|
38
|
+
|
|
39
|
+
# Remove temporary DSL methods
|
|
40
|
+
klass.singleton_class.send(:remove_method, :attribute)
|
|
41
|
+
klass.singleton_class.send(:remove_method, :attribute?)
|
|
42
|
+
klass.singleton_class.send(:remove_method, :after_parse)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Override initialize to make optional attributes truly optional
|
|
46
|
+
optional_attrs = builder.optional
|
|
47
|
+
unless optional_attrs.empty?
|
|
48
|
+
klass.class_eval do
|
|
49
|
+
alias_method(:__data_initialize__, :initialize)
|
|
50
|
+
|
|
51
|
+
define_method(:initialize) do |**kwargs| # steep:ignore
|
|
52
|
+
optional_attrs.each do |attr|
|
|
53
|
+
kwargs[attr] = nil unless kwargs.key?(attr)
|
|
54
|
+
end
|
|
55
|
+
__data_initialize__(**kwargs) # steep:ignore
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
29
60
|
builder.predicate_methods.each do |pred, attr|
|
|
30
61
|
klass.define_method(pred) { !!public_send(attr) }
|
|
31
62
|
end
|
|
@@ -37,6 +68,7 @@ module Structure
|
|
|
37
68
|
mappings: builder.mappings,
|
|
38
69
|
coercions: builder.coercions(klass),
|
|
39
70
|
after_parse: builder.after_parse_callback,
|
|
71
|
+
required: builder.required,
|
|
40
72
|
}.freeze
|
|
41
73
|
klass.instance_variable_set(:@__structure_meta__, meta)
|
|
42
74
|
klass.singleton_class.attr_reader(:__structure_meta__)
|
|
@@ -72,6 +104,15 @@ module Structure
|
|
|
72
104
|
mappings = __structure_meta__[:mappings]
|
|
73
105
|
defaults = __structure_meta__[:defaults]
|
|
74
106
|
after_parse = __structure_meta__[:after_parse]
|
|
107
|
+
required = __structure_meta__[:required]
|
|
108
|
+
|
|
109
|
+
# Check for missing required attributes
|
|
110
|
+
required.each do |attr|
|
|
111
|
+
from = mappings[attr]
|
|
112
|
+
next if data.key?(from) || data.key?(from.to_sym) || defaults.key?(attr)
|
|
113
|
+
|
|
114
|
+
raise ArgumentError, "missing keyword: :#{attr}"
|
|
115
|
+
end
|
|
75
116
|
|
|
76
117
|
mappings.each do |attr, from|
|
|
77
118
|
value = data.fetch(from) do
|
data/sig/structure/builder.rbs
CHANGED
|
@@ -4,10 +4,14 @@ module Structure
|
|
|
4
4
|
@types: Hash[Symbol, untyped]
|
|
5
5
|
@defaults: Hash[Symbol, untyped]
|
|
6
6
|
@after_parse_callback: Proc?
|
|
7
|
+
@optional: Set[Symbol]
|
|
7
8
|
|
|
8
9
|
def attribute: (Symbol name, untyped type, ?from: String?, ?default: untyped) ?{ (untyped) -> untyped } -> void
|
|
9
10
|
| (Symbol name, ?from: String, ?default: untyped) ?{ (untyped) -> untyped } -> void
|
|
10
11
|
|
|
12
|
+
def attribute?: (Symbol name, untyped type, ?from: String?, ?default: untyped) ?{ (untyped) -> untyped } -> void
|
|
13
|
+
| (Symbol name, ?from: String, ?default: untyped) ?{ (untyped) -> untyped } -> void
|
|
14
|
+
|
|
11
15
|
def after_parse: () { (Data) -> void } -> void
|
|
12
16
|
|
|
13
17
|
def attributes: () -> Array[Symbol]
|
|
@@ -16,6 +20,8 @@ module Structure
|
|
|
16
20
|
def defaults: () -> Hash[Symbol, untyped]
|
|
17
21
|
def coercions: (?untyped? context) -> Hash[Symbol, Proc]
|
|
18
22
|
def predicate_methods: () -> Hash[Symbol, Symbol]
|
|
23
|
+
def optional: () -> Array[Symbol]
|
|
24
|
+
def required: () -> Array[Symbol]
|
|
19
25
|
def after_parse_callback: () -> (Proc | nil)
|
|
20
26
|
end
|
|
21
27
|
end
|
data/sig/structure/rbs.rbs
CHANGED
|
@@ -3,7 +3,7 @@ module Structure
|
|
|
3
3
|
def self.emit: (untyped klass) -> String?
|
|
4
4
|
def self.write: (untyped klass, ?dir: String) -> String?
|
|
5
5
|
|
|
6
|
-
private def self.emit_rbs_content: (class_name: String, attributes: Array[Symbol], types: Hash[Symbol, untyped], has_structure_modules: bool) -> String
|
|
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
|
|
7
7
|
private def self.parse_data_type: (untyped type, String class_name) -> String
|
|
8
8
|
private def self.map_type_to_rbs: (untyped type, String class_name) -> String
|
|
9
9
|
end
|
data/sig/structure.rbs
CHANGED