structure 4.3.0 → 4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f9c2544c619e8f11dcafb8765dd9ebfee9c931927bf71d44aed73c7640d98915
4
- data.tar.gz: 6156fbc60895981a25c1a5816fcdb6c367855552ac6a1397f19a7c22b602761c
3
+ metadata.gz: 548fe77f526e4c9f238da59611f13c3f13614f3111f3cf466054bacb84883b18
4
+ data.tar.gz: f6bda78f70879a1d373c8b5aa1aeb0ef3475004b0475ac5100adf9b6bfa5e657
5
5
  SHA512:
6
- metadata.gz: '09728c8023e6d0102df31ab627d76e98aa6212a14dc7d98151f24dadeb7a46ff12c796a2990bb27a69fa78828dfe1b451669d3547041c3da2175d9dbb6bfebb4'
7
- data.tar.gz: ee06e66c737f47ecc9c18a17470b995c6b5abe0c7708e875fbcb11e9b718371eea991d76aa6b31d2695048fc2203fb06b7d91258c8031b747e111b1fed907a9a
6
+ metadata.gz: 84f01c87a3e85b73d364d3994b1c0af646aedd50db182e9966b48bb21cc77432448342986931600f843b62c58677202247888791f58c901e809d1830e944915e
7
+ data.tar.gz: 8d65b5847a2c6839f49cd66ad9a0d08afa7eda6adeba4930dcb79fcff97b8f580e639b0b9a1c13c1a64c4f31c16a72a9517201998793d2c16143e44af69a7d95
@@ -41,14 +41,15 @@ module Structure
41
41
  # @example Non-nullable attribute
42
42
  # attribute :id, String, null: false
43
43
  def attribute(name, type = nil, from: nil, default: nil, null: true, &block)
44
- mappings[name] = (from || name).to_s
44
+ key = from || name
45
+ mappings[name] = key.is_a?(Symbol) ? key.name : -key.to_s
45
46
  defaults[name] = default unless default.nil?
46
47
  @non_nullable.add(name) unless null
47
48
 
48
49
  if type && block
49
50
  raise ArgumentError, "cannot specify both type and block for :#{name}"
50
51
  else
51
- types[name] = type || block
52
+ types[name] = shareable(type) || shareable_block(block)
52
53
  end
53
54
  end
54
55
 
@@ -68,7 +69,7 @@ module Structure
68
69
  # @example Optional with default
69
70
  # attribute? :status, String, default: "pending"
70
71
  #
71
- # @example Optional but non-nullable when present
72
+ # @example Optional but non-nullable when present (e.g. GraphQL String!)
72
73
  # attribute? :name, String, null: false
73
74
  def attribute?(name, type = nil, from: nil, default: nil, null: true, &block)
74
75
  attribute(name, type, from: from, default: default, null: null, &block)
@@ -77,15 +78,22 @@ module Structure
77
78
 
78
79
  # Defines a callback to run after parsing
79
80
  #
81
+ # @param callable [#call, nil] Object that receives the parsed instance, such as a module with a call method
80
82
  # @yield [instance] Block that receives the parsed instance
81
83
  # @return [void]
84
+ # @raise [ArgumentError] If both a callable and a block are provided
82
85
  #
83
86
  # @example Validation
84
87
  # after_parse do |order|
85
88
  # raise "Invalid order" if order.total < 0
86
89
  # end
87
- def after_parse(&block)
88
- @after_parse_callback = block
90
+ #
91
+ # @example With a module, which any Ractor can call
92
+ # after_parse(OrderValidator)
93
+ def after_parse(callable = nil, &block)
94
+ raise ArgumentError, "cannot specify both a callable and a block for after_parse" if callable && block
95
+
96
+ @after_parse_callback = callable || shareable_block(block)
89
97
  end
90
98
 
91
99
  # @api private
@@ -104,7 +112,7 @@ module Structure
104
112
  def coercions(context = nil)
105
113
  @types.to_h do |attr, type|
106
114
  coercion = Types.coerce(type, context)
107
- [attr, coercion]
115
+ [attr, coercion] #: [Symbol, Proc]
108
116
  rescue ArgumentError => e
109
117
  raise ArgumentError, "#{e.message} for :#{attr}"
110
118
  end
@@ -118,5 +126,32 @@ module Structure
118
126
  end
119
127
  end.to_h
120
128
  end
129
+
130
+ private
131
+
132
+ # Before Ruby 4.1, Ractors that call the same shareable block share its $~
133
+ # (https://bugs.ruby-lang.org/issues/22216), so one that matches a Regexp would read another Ractor's match. A block
134
+ # is shared from 4.1 only, and only when Ruby can: one that uses a local variable from around it stays as it is.
135
+ if defined?(Ractor) && Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("4.1")
136
+ def shareable_block(block)
137
+ block && Ractor.shareable_proc(&block) # steep:ignore
138
+ rescue Ractor::IsolationError, ArgumentError
139
+ block
140
+ end
141
+ else
142
+ def shareable_block(block)
143
+ block
144
+ end
145
+ end
146
+
147
+ # Returns the type as another Ractor can read it: a frozen copy of an Array or of a class name. The caller's own
148
+ # object is left as it is.
149
+ def shareable(type)
150
+ case type
151
+ when Array then type.map { |element| shareable(element) }.freeze
152
+ when String then -type
153
+ else type
154
+ end
155
+ end
121
156
  end
122
157
  end
data/lib/structure/rbs.rb CHANGED
@@ -28,6 +28,7 @@ module Structure
28
28
  types = meta.fetch(:types, default_types)
29
29
  # @type var required: Array[Symbol]
30
30
  required = meta.fetch(:required, attributes)
31
+ non_nullable = meta.fetch(:non_nullable, [])
31
32
  custom_methods = meta.fetch(:custom_methods, EMPTY_CUSTOM_METHODS)
32
33
 
33
34
  emit_rbs_content(
@@ -35,6 +36,7 @@ module Structure
35
36
  attributes:,
36
37
  types:,
37
38
  required:,
39
+ non_nullable:,
38
40
  has_structure_modules: meta.any?,
39
41
  custom_methods:,
40
42
  )
@@ -59,9 +61,33 @@ module Structure
59
61
  file_path
60
62
  end
61
63
 
64
+ # Write RBS files for multiple classes or all Structure classes in a module
65
+ #
66
+ # @param classes [Array<Class>, Module] Classes to generate RBS for, or a module to scan
67
+ # @param dir [String] Output directory (default: "sig")
68
+ # @return [Array<String>] Paths of written RBS files
69
+ #
70
+ # @example With array of classes
71
+ # Structure::RBS.write_all([Person, Address, Order])
72
+ #
73
+ # @example With module namespace
74
+ # Structure::RBS.write_all(Peddler::Models)
75
+ def write_all(classes, dir: "sig")
76
+ classes = structure_classes_in(classes) if classes.is_a?(Module)
77
+
78
+ classes.filter_map { |klass| write(klass, dir: dir) }
79
+ end
80
+
62
81
  private
63
82
 
64
- def emit_rbs_content(class_name:, attributes:, types:, required:, has_structure_modules:, custom_methods:)
83
+ def structure_classes_in(mod)
84
+ mod.constants.filter_map do |const_name|
85
+ const = mod.const_get(const_name)
86
+ const if const.is_a?(Class) && const < Data && const.respond_to?(:__structure_meta__)
87
+ end
88
+ end
89
+
90
+ def emit_rbs_content(class_name:, attributes:, types:, required:, non_nullable:, has_structure_modules:, custom_methods:)
65
91
  # @type var lines: Array[String]
66
92
  lines = []
67
93
  lines << "class #{class_name} < Data"
@@ -71,8 +97,9 @@ module Structure
71
97
  rbs_types = attributes.map do |attr|
72
98
  type = types.fetch(attr, nil)
73
99
  rbs_type = map_type_to_rbs(type, class_name)
100
+ nullable = !(required.include?(attr) && non_nullable.include?(attr))
74
101
 
75
- [attr, rbs_type != "untyped" ? "#{rbs_type}?" : rbs_type]
102
+ [attr, rbs_type != "untyped" && nullable ? "#{rbs_type}?" : rbs_type]
76
103
  end.to_h
77
104
 
78
105
  # Sort keyword params: required first, then optional (with ? prefix)
@@ -64,28 +64,39 @@ module Structure
64
64
  end
65
65
  end
66
66
 
67
+ # Makes an object shareable when Ruby can. A lambda that closes over a caller's unshareable object can't be,
68
+ # and comes back as it is. Where Ruby has no Ractor, nothing is shared.
69
+ if defined?(Ractor)
70
+ # @api private
71
+ def shareable(object)
72
+ Ractor.make_shareable(object)
73
+ rescue Ractor::IsolationError
74
+ object
75
+ end
76
+ else
77
+ # @api private
78
+ def shareable(object)
79
+ object
80
+ end
81
+ end
82
+
67
83
  private
68
84
 
69
85
  def boolean
70
- @boolean ||= ->(val) { BOOLEAN_TRUTHY.include?(val) }
86
+ @boolean ||= shareable(->(val) { BOOLEAN_TRUTHY.include?(val) })
71
87
  end
72
88
 
73
89
  def self_referential(context)
74
- ->(val) { context.parse(val) }
90
+ shareable(->(val) { context.parse(val) })
75
91
  end
76
92
 
77
93
  def lazy_class(class_name, context)
78
- resolved_class = nil
79
-
80
- ->(value) do
81
- resolved_class ||= resolve_class(class_name, context)
82
- # @type var resolved_class: untyped
83
- resolved_class.parse(value)
84
- end
94
+ key = shareable(Object.new)
95
+ shareable(->(value) { parse_as(resolved_class(key, class_name, context), value) })
85
96
  end
86
97
 
87
98
  def array(element_type, context = nil)
88
- case element_type
99
+ coercion = case element_type
89
100
  when :self
90
101
  lambda do |value|
91
102
  unless value.respond_to?(:map)
@@ -95,16 +106,14 @@ module Structure
95
106
  value.map { |element| context.parse(element) }
96
107
  end
97
108
  when String
98
- resolved_class = nil
99
-
109
+ key = shareable(Object.new)
100
110
  lambda do |value|
101
111
  unless value.respond_to?(:map)
102
112
  raise TypeError, "can't convert #{value.class} into Array"
103
113
  end
104
114
 
105
- resolved_class ||= resolve_class(element_type, context)
106
- # @type var resolved_class: untyped
107
- value.map { |element| resolved_class.parse(element) }
115
+ element_class = resolved_class(key, element_type, context)
116
+ value.map { |element| parse_as(element_class, element) }
108
117
  end
109
118
  else
110
119
  element_coercer = coerce(element_type, context)
@@ -116,16 +125,38 @@ module Structure
116
125
  value.map { |element| element_coercer.call(element) }
117
126
  end
118
127
  end
128
+
129
+ shareable(coercion)
119
130
  end
120
131
 
121
132
  def parseable(type)
122
133
  @parseable_cache ||= {} # : Hash[untyped, Proc]
123
- @parseable_cache[type] ||= ->(val) { type.parse(val) }
134
+ @parseable_cache[type] ||= shareable(->(val) { parse_as(type, val) })
135
+ end
136
+
137
+ # A method must make this call, not a lambda. A parser written in C, such as Date.parse, keeps its match in
138
+ # the caller's $~, and a lambda shared between Ractors shares its $~ too.
139
+ def parse_as(type, val)
140
+ type.parse(val)
124
141
  end
125
142
 
126
143
  def kernel(type)
127
144
  @kernel_cache ||= {} # : Hash[untyped, Proc]
128
- @kernel_cache[type] ||= ->(val) { Kernel.send(type.name, val) }
145
+ @kernel_cache[type] ||= shareable(->(val) { Kernel.send(type.name, val) })
146
+ end
147
+
148
+ # Resolves a class given by name once in each Ractor. A shareable lambda can't keep the class itself, so it
149
+ # keeps a key into a weak map in the Ractor's local storage. Where Ruby has no Ractor, the key keeps the class.
150
+ if defined?(Ractor)
151
+ def resolved_class(key, class_name, context)
152
+ classes = Ractor.current[:__structure_classes__] ||= ObjectSpace::WeakMap.new # steep:ignore
153
+ classes[key] ||= resolve_class(class_name, context)
154
+ end
155
+ else
156
+ def resolved_class(key, class_name, context)
157
+ key.instance_variable_get(:@resolved) ||
158
+ key.instance_variable_set(:@resolved, resolve_class(class_name, context))
159
+ end
129
160
  end
130
161
 
131
162
  def resolve_class(class_name, context)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Structure
4
- VERSION = "4.3.0"
4
+ VERSION = "4.5.0"
5
5
  end
data/lib/structure.rb CHANGED
@@ -4,7 +4,7 @@ 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 }
7
+ TO_H_CHECKER = Types.shareable(->(x) { x.respond_to?(:to_h) && x })
8
8
  private_constant :TO_H_CHECKER
9
9
 
10
10
  class << self
@@ -29,7 +29,7 @@ module Structure
29
29
  # @type var klass: untyped
30
30
  klass = Data.define(*builder.attributes)
31
31
 
32
- custom_methods_metadata = { instance: [], singleton: [] }.freeze # steep:ignore
32
+ custom_methods_metadata = { instance: [].freeze, singleton: [].freeze }.freeze # steep:ignore
33
33
 
34
34
  # Enable custom method definitions by evaluating block on the class
35
35
  if block
@@ -39,7 +39,7 @@ module Structure
39
39
  # Provide temporary dummy DSL methods to prevent NoMethodError during class_eval
40
40
  klass.define_singleton_method(:attribute) { |*args, **kwargs, &blk| }
41
41
  klass.define_singleton_method(:attribute?) { |*args, **kwargs, &blk| }
42
- klass.define_singleton_method(:after_parse) { |&blk| }
42
+ klass.define_singleton_method(:after_parse) { |*args, &blk| }
43
43
 
44
44
  # Evaluate block in class context for method definitions
45
45
  klass.class_eval(&block)
@@ -60,7 +60,7 @@ module Structure
60
60
 
61
61
  {
62
62
  name: name,
63
- parameters: klass.instance_method(name).parameters,
63
+ parameters: Types.shareable(klass.instance_method(name).parameters),
64
64
  }.freeze
65
65
  end
66
66
 
@@ -70,7 +70,7 @@ module Structure
70
70
 
71
71
  {
72
72
  name: name,
73
- parameters: singleton_class.instance_method(name).parameters,
73
+ parameters: Types.shareable(singleton_class.instance_method(name).parameters),
74
74
  }.freeze
75
75
  end
76
76
 
@@ -81,42 +81,46 @@ module Structure
81
81
  end
82
82
 
83
83
  # Override initialize to make optional attributes truly optional
84
- optional_attrs = builder.optional
84
+ optional_attrs = builder.optional.freeze
85
85
  unless optional_attrs.empty?
86
- klass.class_eval do
87
- alias_method(:__data_initialize__, :initialize)
86
+ klass.alias_method(:__data_initialize__, :initialize)
88
87
 
89
- # steep:ignore:start
90
- define_method(:initialize) do |**kwargs|
91
- optional_attrs.each do |attr|
92
- kwargs[attr] = nil unless kwargs.key?(attr)
93
- end
94
- __data_initialize__(**kwargs)
88
+ # steep:ignore:start
89
+ define_shareable_method(klass, :initialize) do |**kwargs|
90
+ optional_attrs.each do |attr|
91
+ kwargs[attr] = nil unless kwargs.key?(attr)
95
92
  end
96
- # steep:ignore:end
93
+ __data_initialize__(**kwargs)
97
94
  end
95
+ # steep:ignore:end
98
96
  end
99
97
 
100
98
  builder.predicate_methods.each do |pred, attr|
101
- klass.define_method(pred) { !!public_send(attr) }
99
+ define_common_method(klass, pred) { !!public_send(attr) }
102
100
  end
103
101
 
104
- # Store metadata on class to avoid closure capture (memory optimization)
105
- meta = {
106
- types: builder.types,
107
- defaults: builder.defaults,
108
- mappings: builder.mappings,
109
- coercions: builder.coercions(klass),
102
+ # Store metadata on class to avoid closure capture (memory optimization). What Structure builds of it is
103
+ # frozen, so the whole is shareable when the caller's own types, defaults and blocks are, and then any
104
+ # Ractor can parse.
105
+ klass.instance_variable_set(:@__structure_meta__, {
106
+ types: builder.types.freeze,
107
+ defaults: builder.defaults.freeze,
108
+ mappings: builder.mappings.freeze,
109
+ coercions: builder.coercions(klass).freeze,
110
110
  after_parse: builder.after_parse_callback,
111
- required: builder.required,
112
- non_nullable: builder.non_nullable,
111
+ required: builder.required.freeze,
112
+ non_nullable: builder.non_nullable.freeze,
113
113
  custom_methods: custom_methods_metadata,
114
- }.freeze
115
- klass.instance_variable_set(:@__structure_meta__, meta)
116
- klass.singleton_class.attr_reader(:__structure_meta__)
114
+ }.freeze)
115
+ define_common_method(klass.singleton_class, :__structure_meta__) do
116
+ @__structure_meta__
117
+ rescue Ractor::IsolationError
118
+ raise Ractor::IsolationError,
119
+ "#{self} parses only in the main Ractor: a type, block, default or after_parse callback is not shareable"
120
+ end
117
121
 
118
122
  # recursive to_h
119
- klass.define_method(:to_h) do
123
+ define_shareable_method(klass, :to_h) do
120
124
  klass.members.to_h do |m|
121
125
  v = public_send(m)
122
126
  value = case v
@@ -130,10 +134,9 @@ module Structure
130
134
 
131
135
  # parse accepts JSON-ish hashes + optional overrides hash
132
136
  # overrides is a positional arg (not **kwargs) to avoid hash allocation when unused
133
- #
134
- # @type self: singleton(Data) & _StructuredDataClass
135
- # @type var final: Hash[Symbol, untyped]
136
- klass.singleton_class.define_method(:parse) do |data = {}, overrides = nil|
137
+ define_common_method(klass.singleton_class, :parse) do |data = {}, overrides = nil|
138
+ # @type self: singleton(Data) & _StructuredDataClass
139
+ # @type var final: Hash[Symbol, untyped]
137
140
  return data if data.is_a?(self)
138
141
 
139
142
  unless data.respond_to?(:merge!)
@@ -143,12 +146,13 @@ module Structure
143
146
  overrides&.each { |k, v| data[k.to_s] = v }
144
147
 
145
148
  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]
149
+ meta = __structure_meta__
150
+ mappings = meta[:mappings]
151
+ defaults = meta[:defaults]
152
+ coercions = meta[:coercions]
153
+ after_parse = meta[:after_parse]
154
+ required = meta[:required]
155
+ non_nullable = meta[:non_nullable]
152
156
 
153
157
  # Check for missing required attributes
154
158
  required.each do |attr|
@@ -186,10 +190,25 @@ module Structure
186
190
  obj
187
191
  end
188
192
 
189
- klass.define_singleton_method(:load) { |data| parse(data) if data } # steep:ignore
190
- klass.define_singleton_method(:dump) { |value| value&.to_h }
193
+ define_common_method(klass.singleton_class, :load) { |data| parse(data) if data } # steep:ignore
194
+ define_common_method(klass.singleton_class, :dump) { |value| value&.to_h }
191
195
 
192
196
  klass
193
197
  end
198
+
199
+ private
200
+
201
+ # Defines a method that any Ractor can call, not only the one that defined it. The block may read only locals
202
+ # that hold shareable values and are assigned once.
203
+ def define_shareable_method(target, name, &block)
204
+ target.define_method(name, &Types.shareable(block))
205
+ end
206
+
207
+ # The same, for a method whose block is identical for every structure. The first block given for a name is
208
+ # made shareable once and reused; later ones are ignored.
209
+ def define_common_method(target, name, &block)
210
+ @common_methods ||= {} # : Hash[Symbol, Proc]
211
+ target.define_method(name, &(@common_methods[name] ||= Types.shareable(block)))
212
+ end
194
213
  end
195
214
  end
@@ -0,0 +1,200 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ return unless defined?(Tapioca::Dsl::Compiler)
5
+
6
+ module Tapioca
7
+ module Dsl
8
+ module Compilers
9
+ # Generates RBI files for Structure-based Data classes.
10
+ #
11
+ # For example, given:
12
+ #
13
+ # Person = Structure.new do
14
+ # attribute :name, String
15
+ # attribute? :age, Integer
16
+ # end
17
+ #
18
+ # This compiler will generate:
19
+ #
20
+ # class Person < Data
21
+ # sig { params(name: String, age: T.nilable(Integer)).returns(Person) }
22
+ # sig { params(name: String, age: T.nilable(Integer)).void }
23
+ # def initialize(name:, age: nil); end
24
+ #
25
+ # sig { returns(String) }
26
+ # def name; end
27
+ #
28
+ # sig { returns(T.nilable(Integer)) }
29
+ # def age; end
30
+ #
31
+ # sig { params(data: T::Hash[T.any(String, Symbol), T.untyped]).returns(Person) }
32
+ # def self.parse(data = {}); end
33
+ # end
34
+ #: [ConstantType = singleton(::Data)]
35
+ class Structure < Compiler
36
+ extend T::Sig
37
+
38
+ class << self
39
+ extend T::Sig
40
+
41
+ sig { override.returns(T::Enumerable[Module]) }
42
+ def gather_constants
43
+ all_classes.select do |klass|
44
+ klass < ::Data && klass.respond_to?(:__structure_meta__)
45
+ end
46
+ end
47
+ end
48
+
49
+ sig { override.void }
50
+ def decorate
51
+ meta = constant.__structure_meta__
52
+ return unless meta
53
+
54
+ attributes = meta[:mappings]&.keys || constant.members
55
+ types = meta.fetch(:types, {})
56
+ required = meta.fetch(:required, attributes)
57
+ non_nullable = meta.fetch(:non_nullable, [])
58
+
59
+ root.create_path(constant) do |klass|
60
+ generate_new_and_brackets(klass, attributes, types, required, non_nullable)
61
+ generate_parse(klass)
62
+ generate_load_dump(klass)
63
+ generate_members(klass, attributes)
64
+ generate_attr_readers(klass, attributes, types, non_nullable, required)
65
+ generate_to_h(klass, attributes, types, non_nullable, required)
66
+ generate_boolean_predicates(klass, types)
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ sig { params(klass: RBI::Scope, attributes: T::Array[Symbol], types: T::Hash[Symbol, T.untyped], required: T::Array[Symbol], non_nullable: T::Array[Symbol]).void }
73
+ def generate_new_and_brackets(klass, attributes, types, required, non_nullable)
74
+ params = attributes.map do |attr|
75
+ type = map_type_to_sorbet(types[attr], nullable: !(required.include?(attr) && non_nullable.include?(attr)))
76
+ is_required = required.include?(attr)
77
+ if is_required
78
+ create_kw_param(attr.to_s, type: type)
79
+ else
80
+ create_kw_opt_param(attr.to_s, type: type, default: "nil")
81
+ end
82
+ end
83
+
84
+ klass.create_method("initialize", parameters: params, return_type: "void")
85
+ klass.create_method("new", parameters: params, return_type: constant.name.to_s, class_method: true)
86
+ klass.create_method("[]", parameters: params, return_type: constant.name.to_s, class_method: true)
87
+ end
88
+
89
+ sig { params(klass: RBI::Scope).void }
90
+ def generate_parse(klass)
91
+ klass.create_method(
92
+ "parse",
93
+ parameters: [
94
+ create_opt_param("data", type: "T::Hash[T.any(String, Symbol), T.untyped]", default: "{}"),
95
+ create_opt_param("overrides", type: "T.nilable(T::Hash[Symbol, T.untyped])", default: "nil"),
96
+ ],
97
+ return_type: constant.name.to_s,
98
+ class_method: true,
99
+ )
100
+ end
101
+
102
+ sig { params(klass: RBI::Scope).void }
103
+ def generate_load_dump(klass)
104
+ klass.create_method(
105
+ "load",
106
+ parameters: [create_param("data", type: "T.nilable(T::Hash[T.any(String, Symbol), T.untyped])")],
107
+ return_type: "T.nilable(#{constant.name})",
108
+ class_method: true,
109
+ )
110
+ klass.create_method(
111
+ "dump",
112
+ parameters: [create_param("value", type: "T.nilable(#{constant.name})")],
113
+ return_type: "T.nilable(T::Hash[Symbol, T.untyped])",
114
+ class_method: true,
115
+ )
116
+ end
117
+
118
+ sig { params(klass: RBI::Scope, attributes: T::Array[Symbol]).void }
119
+ def generate_members(klass, attributes)
120
+ members_type = "[#{attributes.map { |a| ":#{a}" }.join(", ")}]"
121
+ klass.create_method("members", return_type: members_type, class_method: true)
122
+ klass.create_method("members", return_type: members_type)
123
+ end
124
+
125
+ sig { params(klass: RBI::Scope, attributes: T::Array[Symbol], types: T::Hash[Symbol, T.untyped], non_nullable: T::Array[Symbol], required: T::Array[Symbol]).void }
126
+ def generate_attr_readers(klass, attributes, types, non_nullable, required)
127
+ attributes.each do |attr|
128
+ type = map_type_to_sorbet(types[attr], nullable: !(required.include?(attr) && non_nullable.include?(attr)))
129
+ klass.create_method(attr.to_s, return_type: type)
130
+ end
131
+ end
132
+
133
+ sig { params(klass: RBI::Scope, attributes: T::Array[Symbol], types: T::Hash[Symbol, T.untyped], non_nullable: T::Array[Symbol], required: T::Array[Symbol]).void }
134
+ def generate_to_h(klass, attributes, types, non_nullable, required)
135
+ hash_pairs = attributes.map do |attr|
136
+ type = map_type_to_sorbet(types[attr], nullable: !(required.include?(attr) && non_nullable.include?(attr)))
137
+ "#{attr}: #{type}"
138
+ end.join(", ")
139
+
140
+ klass.create_method("to_h", return_type: "{ #{hash_pairs} }")
141
+ end
142
+
143
+ sig { params(klass: RBI::Scope, types: T::Hash[Symbol, T.untyped]).void }
144
+ def generate_boolean_predicates(klass, types)
145
+ types.each do |attr, type|
146
+ next unless type == :boolean
147
+ next if attr.to_s.end_with?("?")
148
+
149
+ klass.create_method("#{attr}?", return_type: "T::Boolean")
150
+ end
151
+ end
152
+
153
+ sig { params(type: T.untyped, nullable: T::Boolean).returns(String) }
154
+ def map_type_to_sorbet(type, nullable: true)
155
+ raw = case type
156
+ when Class
157
+ if type == Array
158
+ "T::Array[T.untyped]"
159
+ elsif type == Hash
160
+ "T::Hash[T.untyped, T.untyped]"
161
+ else
162
+ type.name || "T.untyped"
163
+ end
164
+ when :boolean
165
+ "T::Boolean"
166
+ when :self
167
+ constant.name.to_s
168
+ when Array
169
+ if type.size == 1
170
+ element_type = map_type_to_sorbet_element(type.first)
171
+ "T::Array[#{element_type}]"
172
+ else
173
+ "T.untyped"
174
+ end
175
+ when Proc
176
+ "T.untyped"
177
+ else
178
+ "T.untyped"
179
+ end
180
+
181
+ nullable ? "T.nilable(#{raw})" : raw
182
+ end
183
+
184
+ sig { params(type: T.untyped).returns(String) }
185
+ def map_type_to_sorbet_element(type)
186
+ case type
187
+ when Class
188
+ type.name || "T.untyped"
189
+ when :boolean
190
+ "T::Boolean"
191
+ when :self
192
+ constant.name.to_s
193
+ else
194
+ "T.untyped"
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -3,7 +3,7 @@ module Structure
3
3
  @mappings: Hash[Symbol, String]
4
4
  @types: Hash[Symbol, untyped]
5
5
  @defaults: Hash[Symbol, untyped]
6
- @after_parse_callback: Proc?
6
+ @after_parse_callback: untyped
7
7
  @optional: Set[Symbol]
8
8
  @non_nullable: Set[Symbol]
9
9
 
@@ -13,7 +13,7 @@ module Structure
13
13
  def attribute?: (Symbol name, untyped type, ?from: String | Symbol | nil, ?default: untyped, ?null: bool) ?{ (untyped) -> untyped } -> void
14
14
  | (Symbol name, ?from: String | Symbol | nil, ?default: untyped, ?null: bool) ?{ (untyped) -> untyped } -> void
15
15
 
16
- def after_parse: () { (Data) -> void } -> void
16
+ def after_parse: (?untyped callable) ?{ (Data) -> void } -> void
17
17
 
18
18
  def attributes: () -> Array[Symbol]
19
19
  def mappings: () -> Hash[Symbol, String]
@@ -24,7 +24,10 @@ module Structure
24
24
  def optional: () -> Array[Symbol]
25
25
  def required: () -> Array[Symbol]
26
26
  def non_nullable: () -> Array[Symbol]
27
- def after_parse_callback: () -> (Proc | nil)
27
+ def after_parse_callback: () -> untyped
28
+
29
+ private def shareable: (untyped type) -> untyped
30
+ private def shareable_block: (Proc? block) -> Proc?
28
31
 
29
32
  def method_missing: (Symbol, *untyped, **untyped) ?{ (*untyped, **untyped) -> untyped } -> untyped
30
33
  def respond_to_missing?: (Symbol, bool) -> bool
@@ -4,8 +4,10 @@ module Structure
4
4
 
5
5
  def self.emit: (untyped klass) -> String?
6
6
  def self.write: (untyped klass, ?dir: String) -> String?
7
+ def self.write_all: (Array[untyped] | Module classes, ?dir: String) -> Array[String]
7
8
 
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
9
+ private def self.structure_classes_in: (Module mod) -> Array[untyped]
10
+ private def self.emit_rbs_content: (class_name: String, attributes: Array[Symbol], types: Hash[Symbol, untyped], required: Array[Symbol], non_nullable: Array[Symbol], has_structure_modules: bool, custom_methods: untyped) -> String
9
11
  private def self.parse_data_type: (untyped type, String class_name) -> String
10
12
  private def self.map_type_to_rbs: (untyped type, String class_name) -> String
11
13
  private def self.format_method_signature: (Hash[Symbol, untyped] method_meta, singleton: bool) -> String
@@ -12,14 +12,17 @@ module Structure
12
12
  self.@string_class_cache: Hash[Array[untyped], Proc]
13
13
 
14
14
  def self.coerce: (untyped type, ?untyped? context) -> untyped
15
+ def self.shareable: [T] (T object) -> T
15
16
 
16
17
  private def self.boolean: () -> Proc
17
18
  private def self.self_referential: (_ParseableClass context) -> Proc
18
19
  private def self.lazy_class: (String class_name, untyped? context) -> Proc
19
20
  private def self.array: (untyped element_type, ?untyped? context) -> Proc
20
21
  private def self.parseable: (untyped type) -> Proc
22
+ private def self.parse_as: (untyped type, untyped val) -> untyped
21
23
  private def self.kernel: (Class type) -> Proc
22
24
  private def self.parse: (untyped val) -> untyped
25
+ private def self.resolved_class: (Object key, String class_name, untyped? context) -> _ParseableClass
23
26
  private def self.resolve_class: (String class_name, untyped? context) -> _ParseableClass
24
27
  end
25
28
  end
data/sig/structure.rbs CHANGED
@@ -1,6 +1,8 @@
1
1
  module Structure
2
2
  TO_H_CHECKER: Proc
3
3
 
4
+ self.@common_methods: Hash[Symbol, Proc]
5
+
4
6
  interface _StructuredDataClass
5
7
  def __structure_meta__: () -> {
6
8
  types: Hash[Symbol, untyped],
@@ -15,4 +17,7 @@ module Structure
15
17
  # Block evaluated twice: first for DSL methods (attribute calls), then for custom methods
16
18
  # Context set to Builder so DSL methods are recognized
17
19
  def self.new: () ?{ () [self: Structure::Builder] -> void } -> untyped
20
+
21
+ private def self.define_shareable_method: (untyped target, Symbol name) { (?) -> untyped } -> void
22
+ private def self.define_common_method: (untyped target, Symbol name) { (?) -> untyped } -> void
18
23
  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.3.0
4
+ version: 4.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hakan Ensari
@@ -20,6 +20,7 @@ files:
20
20
  - lib/structure/rbs.rb
21
21
  - lib/structure/types.rb
22
22
  - lib/structure/version.rb
23
+ - lib/tapioca/dsl/compilers/structure.rb
23
24
  - sig/structure.rbs
24
25
  - sig/structure/builder.rbs
25
26
  - sig/structure/rbs.rbs
@@ -36,14 +37,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
36
37
  requirements:
37
38
  - - ">="
38
39
  - !ruby/object:Gem::Version
39
- version: '3.2'
40
+ version: '3.3'
40
41
  required_rubygems_version: !ruby/object:Gem::Requirement
41
42
  requirements:
42
43
  - - ">="
43
44
  - !ruby/object:Gem::Version
44
45
  version: '0'
45
46
  requirements: []
46
- rubygems_version: 3.7.2
47
+ rubygems_version: 4.0.20
47
48
  specification_version: 4
48
49
  summary: Structure your data
49
50
  test_files: []