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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74f52e94d4ffd40ad55e291a4ab72c108af572be936ee2d1571fef47699af2cb
4
- data.tar.gz: d2ef8dee35e2ea236237d1c34c68bc6877039145553f7e1b16729ea84e3778c4
3
+ metadata.gz: 0d49fdb007e1c1690e686dc7c17498fd905b8e100c8dcf8fe3b374c99ec18c14
4
+ data.tar.gz: 3dbdc1a4e9ac4e6e70332084bda5612b4b17dc74a63a28e0eb084a2f49d440bb
5
5
  SHA512:
6
- metadata.gz: f0c4192ddde552c7cd4a583aca9bdad31aeb64dc039680e965944696ee7648cc9a423017ab8cea5546c708ae72b7920c21fb2c96e74442932f24a3a85faa6b81
7
- data.tar.gz: 03d4ea5fda959cd3bcd60b79a323b8dbfaeb5792c22b809179b7f84f76aab36845e7dbcd2f30567cf7922eea9995997cfb730cd69e8dd726cebded2bd076f540
6
+ metadata.gz: 9d7c579f4e71000c4cabeaa4bd95deb3c01763b0ab5e426fb7544da65e7617a5e8ac7d32c7a3079f690a55c0021a57d97646732d516a25796cbc8999dbb6630a
7
+ data.tar.gz: '02791f8b7a9372c583839e35c7c20c6bb7ec03626256f92f9cda2ef11c5188d541f0f3b073a053d7ccf152da4a47c4af32eeb65b2e68a92ecc00e8494174ba7e'
@@ -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
- def attribute(name, type = nil, from: nil, default: nil, &block)
39
- mappings[name] = from || name.to_s
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, "Cannot specify both type and block for :#{name}"
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
- def attribute?(name, type = nil, from: nil, default: nil, &block)
64
- attribute(name, type, from: from, default: default, &block)
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.transform_values { |type| Types.coerce(type, context) }
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
- types = meta.fetch(:types, {}) # steep:ignore
24
- required = meta.fetch(:required, attributes) # steep:ignore
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
- 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 << ""
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
- # Generate members tuple type
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
- lines << " def self.members: () -> [ #{members_tuple} ]"
100
- lines << ""
134
+ singleton_methods_list << {
135
+ name: "members",
136
+ lines: [" def self.members: () -> [ #{members_tuple} ]"],
137
+ }
101
138
 
102
- # Generate parse method signatures
103
- if needs_parse_data
104
- lines << " def self.parse: (?parse_data data) -> #{class_name}"
105
- lines << " | (?Hash[String, untyped] data) -> #{class_name}"
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
- # Remove optional parentheses to match RBS::Sorter style
108
- lines << " def self.parse: (?Hash[String | Symbol, untyped], **untyped) -> #{class_name}"
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
- unless boolean_predicates.empty?
120
- lines << ""
121
- boolean_predicates.each do |attr, _type|
122
- lines << " def #{attr}?: () -> bool"
123
- end
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
- # Instance members method comes after attr_readers and predicates
127
- lines << " def members: () -> [ #{members_tuple} ]"
128
- lines << ""
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
- lines << " def to_h: () -> { #{hash_type} }"
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
@@ -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, "Cannot specify #{type.inspect} as type"
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 = resolve_class(element_type, context)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Structure
4
- VERSION = "4.1.0"
4
+ VERSION = "4.2.0"
5
5
  end
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
- define_method(:initialize) do |**kwargs| # steep:ignore
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) # steep:ignore
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 ->(x) { x.respond_to?(:to_h) && x } then v.to_h
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 = __structure_meta__[:mappings]
105
- defaults = __structure_meta__[:defaults]
106
- after_parse = __structure_meta__[:after_parse]
107
- required = __structure_meta__[:required]
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 = __structure_meta__[:coercions][attr]
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
 
@@ -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?, ?default: untyped) ?{ (untyped) -> untyped } -> void
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?, ?default: untyped) ?{ (untyped) -> untyped } -> void
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
@@ -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
@@ -4,7 +4,7 @@ module Structure
4
4
  def parse: (untyped) -> untyped
5
5
  end
6
6
 
7
- BOOLEAN_TRUTHY: Array[untyped]
7
+ BOOLEAN_TRUTHY: Set[untyped]
8
8
 
9
9
  self.@boolean: Proc
10
10
  self.@kernel_cache: Hash[untyped, Proc]
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
- def self.new: () ?{ (Structure::Builder) [self: Structure::Builder] -> void } -> untyped
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
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: 4.1.0
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hakan Ensari