structure 4.0.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: 2306964e18c5a78a2241c8c42071fd0a1921fa8c7c54cf210b0825b20607cfca
4
- data.tar.gz: c43cf42996e715fe5c1ca1b57bf91ee411405f188382b2d10c0b593a66e90a6f
3
+ metadata.gz: 0d49fdb007e1c1690e686dc7c17498fd905b8e100c8dcf8fe3b374c99ec18c14
4
+ data.tar.gz: 3dbdc1a4e9ac4e6e70332084bda5612b4b17dc74a63a28e0eb084a2f49d440bb
5
5
  SHA512:
6
- metadata.gz: 314242bf300d7ec09e1755647d4a8fef224eb2c64fc0b6d945be386b7af008c22146e80993f1e753f1df9cc5b37841242b7ab4a14d71cde42732952e288fc196
7
- data.tar.gz: 72dcc21770b4f57b67fd64b8ad69695f02d0a55b2622717f63795b8da32aa8b45cd61db146b2e2c4dc02683588ca74dba831437652afaed9bb25fc159195d0b5
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
@@ -4,7 +4,14 @@ require "fileutils"
4
4
  require "pathname"
5
5
 
6
6
  module Structure
7
+ # Generates RBS type signatures for Structure classes
7
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
+
8
15
  class << self
9
16
  def emit(klass)
10
17
  return unless klass < Data
@@ -16,8 +23,12 @@ module Structure
16
23
  meta = klass.respond_to?(:__structure_meta__) ? klass.__structure_meta__ : {}
17
24
 
18
25
  attributes = meta[:mappings] ? meta[:mappings].keys : klass.members
19
- types = meta.fetch(:types, {}) # steep:ignore
20
- 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)
21
32
 
22
33
  emit_rbs_content(
23
34
  class_name:,
@@ -25,6 +36,7 @@ module Structure
25
36
  types:,
26
37
  required:,
27
38
  has_structure_modules: meta.any?,
39
+ custom_methods:,
28
40
  )
29
41
  end
30
42
 
@@ -49,7 +61,7 @@ module Structure
49
61
 
50
62
  private
51
63
 
52
- 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:)
53
65
  # @type var lines: Array[String]
54
66
  lines = []
55
67
  lines << "class #{class_name} < Data"
@@ -63,52 +75,147 @@ module Structure
63
75
  [attr, rbs_type != "untyped" ? "#{rbs_type}?" : rbs_type]
64
76
  end.to_h
65
77
 
66
- # Mark optional attributes with ? prefix in keyword params
67
- keyword_params = attributes.map do |attr|
68
- prefix = required.include?(attr) ? "" : "?"
69
- "#{prefix}#{attr}: #{rbs_types[attr]}"
70
- end.join(", ")
78
+ # Sort keyword params: required first, then optional (with ? prefix)
79
+ # Within each group, maintain declaration order
80
+ required_params = required.map { |attr| "#{attr}: #{rbs_types[attr]}" }
81
+ optional_params = (attributes - required).map { |attr| "?#{attr}: #{rbs_types[attr]}" }
82
+ keyword_params = (required_params + optional_params).join(", ")
71
83
  positional_params = attributes.map { |attr| rbs_types[attr] }.join(", ")
72
84
 
73
- lines << " def self.new: (#{keyword_params}) -> #{class_name}"
74
- lines << " | (#{positional_params}) -> #{class_name}"
75
- lines << ""
76
-
77
85
  needs_parse_data = types.any? do |_attr, type|
78
86
  type == :self || type == [:self]
79
87
  end
80
88
 
89
+ # Generate type alias first if needed (RBS::Sorter puts types at top)
81
90
  if needs_parse_data
82
- lines << " type parse_data = {"
83
- attributes.each do |attr|
91
+ lines << " type parse_data = { " + attributes.map { |attr|
84
92
  type = types.fetch(attr, nil)
85
93
  parse_type = parse_data_type(type, class_name)
86
- lines << " ?#{attr}: #{parse_type},"
87
- end
88
- lines[-1] = lines[-1].chomp(",")
89
- lines << " }"
90
- lines << ""
91
- lines << " def self.parse: (?parse_data data) -> #{class_name}"
92
- lines << " | (?Hash[String, untyped] data) -> #{class_name}"
94
+ "?#{attr}: #{parse_type}"
95
+ }.join(", ") + " }"
96
+ end
97
+
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
+ ]
116
+
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
133
+ members_tuple = attributes.map { |attr| ":#{attr}" }.join(", ")
134
+ singleton_methods_list << {
135
+ name: "members",
136
+ lines: [" def self.members: () -> [ #{members_tuple} ]"],
137
+ }
138
+
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
+ }
93
147
  else
94
- # For structures without special types, just use Hash
95
- 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
+ }
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 }
96
167
  end
168
+
97
169
  lines << ""
98
170
 
99
- attributes.each do |attr|
171
+ # Sort attr_reader lines alphabetically (RBS::Sorter does this)
172
+ attributes.sort.each do |attr|
100
173
  lines << " attr_reader #{attr}: #{rbs_types[attr]}"
101
174
  end
102
175
  lines << ""
103
176
 
104
- types.each do |attr, type|
105
- if type == :boolean && !attr.to_s.end_with?("?")
106
- lines << " def #{attr}?: () -> bool"
107
- end
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 = []
180
+
181
+ # Add boolean predicates
182
+ boolean_predicates = types.sort.select { |attr, type| type == :boolean && !attr.to_s.end_with?("?") }
183
+ boolean_predicates.each do |attr, _type|
184
+ instance_methods_list << {
185
+ name: "#{attr}?",
186
+ lines: [" def #{attr}?: () -> bool"],
187
+ }
188
+ end
189
+
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
+ }
108
199
  end
109
200
 
201
+ # Add standard instance methods
202
+ instance_methods_list << {
203
+ name: "members",
204
+ lines: [" def members: () -> [ #{members_tuple} ]"],
205
+ }
206
+
110
207
  hash_type = attributes.map { |attr| "#{attr}: #{rbs_types[attr]}" }.join(", ")
111
- 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
112
219
  end
113
220
 
114
221
  lines << "end"
@@ -163,6 +270,54 @@ module Structure
163
270
  "untyped"
164
271
  end
165
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
166
321
  end
167
322
  end
168
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.0.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,23 +24,76 @@ 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
+
34
+ # Enable custom method definitions by evaluating block on the class
35
+ if block
36
+ before_instance_methods = klass.instance_methods(false)
37
+ before_singleton_methods = klass.singleton_methods(false)
38
+
39
+ # Provide temporary dummy DSL methods to prevent NoMethodError during class_eval
40
+ klass.define_singleton_method(:attribute) { |*args, **kwargs, &blk| }
41
+ klass.define_singleton_method(:attribute?) { |*args, **kwargs, &blk| }
42
+ klass.define_singleton_method(:after_parse) { |&blk| }
43
+
44
+ # Evaluate block in class context for method definitions
45
+ klass.class_eval(&block)
46
+
47
+ # Remove temporary DSL methods
48
+ klass.singleton_class.send(:remove_method, :attribute)
49
+ klass.singleton_class.send(:remove_method, :attribute?)
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
81
+ end
82
+
29
83
  # Override initialize to make optional attributes truly optional
30
84
  optional_attrs = builder.optional
31
85
  unless optional_attrs.empty?
32
86
  klass.class_eval do
33
87
  alias_method(:__data_initialize__, :initialize)
34
88
 
35
- define_method(:initialize) do |**kwargs| # steep:ignore
89
+ # steep:ignore:start
90
+ define_method(:initialize) do |**kwargs|
36
91
  optional_attrs.each do |attr|
37
92
  kwargs[attr] = nil unless kwargs.key?(attr)
38
93
  end
39
- __data_initialize__(**kwargs) # steep:ignore
94
+ __data_initialize__(**kwargs)
40
95
  end
96
+ # steep:ignore:end
41
97
  end
42
98
  end
43
99
 
@@ -53,6 +109,8 @@ module Structure
53
109
  coercions: builder.coercions(klass),
54
110
  after_parse: builder.after_parse_callback,
55
111
  required: builder.required,
112
+ non_nullable: builder.non_nullable,
113
+ custom_methods: custom_methods_metadata,
56
114
  }.freeze
57
115
  klass.instance_variable_set(:@__structure_meta__, meta)
58
116
  klass.singleton_class.attr_reader(:__structure_meta__)
@@ -63,7 +121,7 @@ module Structure
63
121
  v = public_send(m)
64
122
  value = case v
65
123
  when Array then v.map { |x| x.respond_to?(:to_h) && x ? x.to_h : x }
66
- when ->(x) { x.respond_to?(:to_h) && x } then v.to_h
124
+ when TO_H_CHECKER then v.to_h
67
125
  else v
68
126
  end
69
127
  [m, value]
@@ -84,11 +142,13 @@ module Structure
84
142
 
85
143
  overrides&.each { |k, v| data[k.to_s] = v }
86
144
 
87
- final = {}
88
- mappings = __structure_meta__[:mappings]
89
- defaults = __structure_meta__[:defaults]
90
- after_parse = __structure_meta__[:after_parse]
91
- 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]
92
152
 
93
153
  # Check for missing required attributes
94
154
  required.each do |attr|
@@ -99,6 +159,8 @@ module Structure
99
159
  end
100
160
 
101
161
  mappings.each do |attr, from|
162
+ key_present = data.key?(from) || data.key?(from.to_sym)
163
+
102
164
  value = data.fetch(from) do
103
165
  data.fetch(from.to_sym) do
104
166
  defaults[attr]
@@ -106,10 +168,16 @@ module Structure
106
168
  end
107
169
 
108
170
  if value
109
- coercion = __structure_meta__[:coercions][attr]
171
+ coercion = coercions[attr]
110
172
  value = coercion.call(value) if coercion
111
173
  end
112
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
+
113
181
  final[attr] = value
114
182
  end
115
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.0.0
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hakan Ensari