dry-struct 1.0.0 → 1.6.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.
@@ -1,32 +1,22 @@
1
- require 'dry/core/class_attributes'
2
- require 'dry/core/inflector'
3
- require 'dry/core/descendants_tracker'
1
+ # frozen_string_literal: true
4
2
 
5
- require 'dry/struct/errors'
6
- require 'dry/struct/constructor'
7
- require 'dry/struct/sum'
3
+ require "weakref"
8
4
 
9
5
  module Dry
10
6
  class Struct
11
7
  # Class-level interface of {Struct} and {Value}
12
- module ClassInterface
8
+ module ClassInterface # rubocop:disable Metrics/ModuleLength
13
9
  include Core::ClassAttributes
14
10
 
15
- include Dry::Types::Type
16
- include Dry::Types::Builder
11
+ include Types::Type
12
+ include Types::Builder
17
13
 
18
14
  # @param [Class] klass
19
15
  def inherited(klass)
20
16
  super
21
17
 
22
- base = self
23
-
24
- klass.class_eval do
25
- @meta = base.meta
26
-
27
- unless equal?(Value)
28
- extend Dry::Core::DescendantsTracker
29
- end
18
+ unless klass.name.eql?("Dry::Struct::Value")
19
+ klass.extend(Core::DescendantsTracker)
30
20
  end
31
21
  end
32
22
 
@@ -52,8 +42,12 @@ module Dry
52
42
  # end
53
43
  # end
54
44
  #
55
- # Language.schema
56
- # # => #<Dry::Types[Constructor<Schema<keys={name: Nominal<String> details: Language::Details}> fn=Kernel.Hash>]>
45
+ # Language.schema # new lines for readability
46
+ # # => #<Dry::Types[
47
+ # Constructor<Schema<keys={
48
+ # name: Constrained<Nominal<String> rule=[type?(String)]>
49
+ # details: Language::Details
50
+ # }> fn=Kernel.Hash>]>
57
51
  #
58
52
  # ruby = Language.new(name: 'Ruby', details: { type: 'OO' })
59
53
  # ruby.name #=> 'Ruby'
@@ -70,11 +64,13 @@ module Dry
70
64
  # end
71
65
  # end
72
66
  #
73
- # Language.schema
67
+ # Language.schema # new lines for readability
74
68
  # => #<Dry::Types[Constructor<Schema<keys={
75
- # name: Nominal<String>
76
- # versions: Array<Nominal<String>>
77
- # celebrities: Array<Language::Celebrity>
69
+ # name: Constrained<Nominal<String> rule=[type?(String)]>
70
+ # versions: Constrained<
71
+ # Array<Constrained<Nominal<String> rule=[type?(String)]>
72
+ # > rule=[type?(Array)]>
73
+ # celebrities: Constrained<Array<Language::Celebrity> rule=[type?(Array)]>
78
74
  # }> fn=Kernel.Hash>]>
79
75
  #
80
76
  # ruby = Language.new(
@@ -96,19 +92,54 @@ module Dry
96
92
  # ruby.celebrities[0].pseudonym #=> 'Matz'
97
93
  # ruby.celebrities[1].name #=> 'Aaron Patterson'
98
94
  # ruby.celebrities[1].pseudonym #=> 'tenderlove'
99
- def attribute(name, type = nil, &block)
95
+ def attribute(name, type = Undefined, &block)
100
96
  attributes(name => build_type(name, type, &block))
101
97
  end
102
98
 
99
+ # Add atributes from another struct
100
+ #
101
+ # @example
102
+ # class Address < Dry::Struct
103
+ # attribute :city, Types::String
104
+ # attribute :country, Types::String
105
+ # end
106
+ #
107
+ # class User < Dry::Struct
108
+ # attribute :name, Types::String
109
+ # attributes_from Address
110
+ # end
111
+ #
112
+ # User.new(name: 'Quispe', city: 'La Paz', country: 'Bolivia')
113
+ #
114
+ # @example with nested structs
115
+ # class User < Dry::Struct
116
+ # attribute :name, Types::String
117
+ # attribute :address do
118
+ # attributes_from Address
119
+ # end
120
+ # end
121
+ #
122
+ # @param struct [Dry::Struct]
123
+ def attributes_from(struct)
124
+ extracted_schema = struct.schema.keys.map { |key|
125
+ if key.required?
126
+ [key.name, key.type]
127
+ else
128
+ [:"#{key.name}?", key.type]
129
+ end
130
+ }.to_h
131
+ attributes(extracted_schema)
132
+ end
133
+
103
134
  # Adds an omittable (key is not required on initialization) attribute for this {Struct}
104
135
  #
105
136
  # @example
106
137
  # class User < Dry::Struct
107
- # attribute :name, Types::Strict::String
108
- # attribute? :email, Types::Strict::String
138
+ # attribute :name, Types::String
139
+ # attribute? :email, Types::String
109
140
  # end
110
141
  #
111
- # User.new(name: 'John') # => #<User name="John">
142
+ # User.new(name: 'John') # => #<User name="John" email=nil>
112
143
  #
113
144
  # @param [Symbol] name name of the defined attribute
114
145
  # @param [Dry::Types::Type, nil] type or superclass of nested type
@@ -116,17 +147,17 @@ module Dry
116
147
  #
117
148
  def attribute?(*args, &block)
118
149
  if args.size == 1 && block.nil?
119
- Dry::Core::Deprecations.warn(
120
- 'Dry::Struct.attribute? is deprecated for checking attribute presence, '\
121
- 'use has_attribute? instead',
122
- tag: :'dry-struct'
150
+ Core::Deprecations.warn(
151
+ "Dry::Struct.attribute? is deprecated for checking attribute presence, "\
152
+ "use has_attribute? instead",
153
+ tag: :"dry-struct"
123
154
  )
124
155
 
125
156
  has_attribute?(args[0])
126
157
  else
127
- name, type = args
158
+ name, * = args
128
159
 
129
- attribute(:"#{ name }?", build_type(name, type, &block))
160
+ attribute(:"#{name}?", build_type(*args, &block))
130
161
  end
131
162
  end
132
163
 
@@ -145,29 +176,22 @@ module Dry
145
176
  #
146
177
  # Book.schema
147
178
  # # => #<Dry::Types[Constructor<Schema<keys={
148
- # # title: Nominal<String>
149
- # # author: Nominal<String>
179
+ # # title: Constrained<Nominal<String> rule=[type?(String)]>
180
+ # # author: Constrained<Nominal<String> rule=[type?(String)]>
150
181
  # # }> fn=Kernel.Hash>]>
151
182
  def attributes(new_schema)
152
- keys = new_schema.keys.map { |k| k.to_s.chomp('?').to_sym }
183
+ keys = new_schema.keys.map { |k| k.to_s.chomp("?").to_sym }
153
184
  check_schema_duplication(keys)
154
185
 
155
186
  schema schema.schema(new_schema)
156
187
 
157
- keys.each do |key|
158
- next if instance_methods.include?(key)
159
- class_eval(<<-RUBY)
160
- def #{key}
161
- @attributes[#{key.inspect}]
162
- end
163
- RUBY
164
- end
188
+ define_accessors(keys)
165
189
 
166
190
  @attribute_names = nil
167
191
 
168
192
  direct_descendants = descendants.select { |d| d.superclass == self }
169
193
  direct_descendants.each do |d|
170
- inherited_attrs = new_schema.reject { |k, _| d.has_attribute?(k.to_s.chomp('?').to_sym) }
194
+ inherited_attrs = new_schema.reject { |k, _| d.has_attribute?(k.to_s.chomp("?").to_sym) }
171
195
  d.attributes(inherited_attrs)
172
196
  end
173
197
 
@@ -182,7 +206,7 @@ module Dry
182
206
  # class Book < Dry::Struct
183
207
  # transform_types { |t| t.meta(struct: :Book) }
184
208
  #
185
- # attribute :title, Types::Strict::String
209
+ # attribute :title, Types::String
186
210
  # end
187
211
  #
188
212
  # Book.schema.key(:title).meta # => { struct: :Book }
@@ -199,7 +223,7 @@ module Dry
199
223
  # class Book < Dry::Struct
200
224
  # transform_keys(&:to_sym)
201
225
  #
202
- # attribute :title, Types::Strict::String
226
+ # attribute :title, Types::String
203
227
  # end
204
228
  #
205
229
  # Book.new('title' => "The Old Man and the Sea")
@@ -208,7 +232,7 @@ module Dry
208
232
  schema schema.with_key_transform(proc || block)
209
233
  end
210
234
 
211
- # @param [Hash{Symbol => Dry::Types::Type, Dry::Struct}] new_schema
235
+ # @param [Hash{Symbol => Dry::Types::Type, Dry::Struct}] new_keys
212
236
  # @raise [RepeatedAttributeError] when trying to define attribute with the
213
237
  # same name as previously defined one
214
238
  def check_schema_duplication(new_keys)
@@ -222,16 +246,25 @@ module Dry
222
246
 
223
247
  # @param [Hash{Symbol => Object},Dry::Struct] attributes
224
248
  # @raise [Struct::Error] if the given attributes don't conform {#schema}
225
- def new(attributes = default_attributes, safe = false)
226
- if equal?(attributes.class)
227
- attributes
249
+ def new(attributes = default_attributes, safe = false, &block) # rubocop:disable Style/OptionalBooleanParameter
250
+ if attributes.is_a?(Struct)
251
+ if equal?(attributes.class)
252
+ attributes
253
+ else
254
+ # This implicit coercion is arguable but makes sense overall
255
+ # in cases there you pass child struct to the base struct constructor
256
+ # User.new(super_user)
257
+ #
258
+ # We may deprecate this behavior in future forcing people to be explicit
259
+ new(attributes.to_h, safe, &block)
260
+ end
228
261
  elsif safe
229
262
  load(schema.call_safe(attributes) { |output = attributes| return yield output })
230
263
  else
231
264
  load(schema.call_unsafe(attributes))
232
265
  end
233
- rescue Types::CoercionError => error
234
- raise Struct::Error, "[#{self}.new] #{error}"
266
+ rescue Types::CoercionError => e
267
+ raise Error, "[#{self}.new] #{e}", e.backtrace
235
268
  end
236
269
 
237
270
  # @api private
@@ -255,27 +288,26 @@ module Dry
255
288
  # @api private
256
289
  def load(attributes)
257
290
  struct = allocate
258
- struct.send(:initialize, attributes)
291
+ struct.__send__(:initialize, attributes)
259
292
  struct
260
293
  end
261
294
 
262
295
  # @param [#call,nil] constructor
263
- # @param [Hash] _options
264
296
  # @param [#call,nil] block
265
297
  # @return [Dry::Struct::Constructor]
266
- def constructor(constructor = nil, **_options, &block)
267
- Struct::Constructor.new(self, fn: constructor || block)
298
+ def constructor(constructor = nil, **, &block)
299
+ Constructor[self, fn: constructor || block]
268
300
  end
269
301
 
270
302
  # @param [Hash{Symbol => Object},Dry::Struct] input
271
303
  # @yieldparam [Dry::Types::Result::Failure] failure
272
- # @yieldreturn [Dry::Types::ResultResult]
304
+ # @yieldreturn [Dry::Types::Result]
273
305
  # @return [Dry::Types::Result]
274
306
  def try(input)
275
- Types::Result::Success.new(self[input])
276
- rescue Struct::Error => e
277
- failure = Types::Result::Failure.new(input, e.message)
278
- block_given? ? yield(failure) : failure
307
+ success(self[input])
308
+ rescue Error => e
309
+ failure_result = failure(input, e)
310
+ block_given? ? yield(failure_result) : failure_result
279
311
  end
280
312
 
281
313
  # @param [Hash{Symbol => Object},Dry::Struct] input
@@ -298,7 +330,7 @@ module Dry
298
330
  # @param [({Symbol => Object})] args
299
331
  # @return [Dry::Types::Result::Failure]
300
332
  def failure(*args)
301
- result(::Dry::Types::Result::Failure, *args)
333
+ result(Types::Result::Failure, *args)
302
334
  end
303
335
 
304
336
  # @param [Class] klass
@@ -312,7 +344,7 @@ module Dry
312
344
  false
313
345
  end
314
346
 
315
- # @param [Object, Dry::Struct] value
347
+ # @param [Object, Dry::Struct] other
316
348
  # @return [Boolean]
317
349
  def ===(other)
318
350
  other.is_a?(self)
@@ -336,7 +368,7 @@ module Dry
336
368
 
337
369
  # @return [Proc]
338
370
  def to_proc
339
- proc { |input| call(input) }
371
+ @to_proc ||= proc { |input| call(input) }
340
372
  end
341
373
 
342
374
  # Checks if this {Struct} has the given attribute
@@ -357,10 +389,12 @@ module Dry
357
389
  # @return [{Symbol => Object}]
358
390
  def meta(meta = Undefined)
359
391
  if meta.equal?(Undefined)
360
- @meta
392
+ schema.meta
393
+ elsif meta.empty?
394
+ self
361
395
  else
362
- Class.new(self) do
363
- @meta = @meta.merge(meta) unless meta.empty?
396
+ ::Class.new(self) do
397
+ schema schema.meta(meta) unless meta.empty?
364
398
  end
365
399
  end
366
400
  end
@@ -369,13 +403,28 @@ module Dry
369
403
  # @param [Dry::Types::Type] type
370
404
  # @return [Dry::Types::Sum]
371
405
  def |(type)
372
- if type.is_a?(Class) && type <= Struct
373
- Struct::Sum.new(self, type)
406
+ if type.is_a?(::Class) && type <= Struct
407
+ Sum.new(self, type)
374
408
  else
375
409
  super
376
410
  end
377
411
  end
378
412
 
413
+ # Make the struct abstract. This class will be used as a default
414
+ # parent class for nested structs
415
+ def abstract
416
+ abstract_class self
417
+ end
418
+
419
+ # Dump to the AST
420
+ #
421
+ # @return [Array]
422
+ #
423
+ # @api public
424
+ def to_ast(meta: true)
425
+ [:struct, [::WeakRef.new(self), schema.to_ast(meta: meta)]]
426
+ end
427
+
379
428
  # Stores an object for building nested struct classes
380
429
  # @return [StructBuilder]
381
430
  def struct_builder
@@ -399,21 +448,21 @@ module Dry
399
448
  # @param [Dry::Types::Type] type
400
449
  # @return [Boolean]
401
450
  def struct?(type)
402
- type.is_a?(Class) && type <= Struct
451
+ type.is_a?(::Class) && type <= Struct
403
452
  end
404
453
  private :struct?
405
454
 
406
455
  # Constructs a type
407
456
  #
408
457
  # @return [Dry::Types::Type, Dry::Struct]
409
- def build_type(name, type, &block)
458
+ def build_type(name, type = Undefined, &block)
410
459
  type_object =
411
- if type.is_a?(String)
412
- Dry::Types[type]
413
- elsif block.nil? && type.nil?
460
+ if type.is_a?(::String)
461
+ Types[type]
462
+ elsif block.nil? && Undefined.equal?(type)
414
463
  raise(
415
- ArgumentError,
416
- 'you must supply a type or a block to `Dry::Struct.attribute`'
464
+ ::ArgumentError,
465
+ "you must supply a type or a block to `Dry::Struct.attribute`"
417
466
  )
418
467
  else
419
468
  type
@@ -426,6 +475,19 @@ module Dry
426
475
  end
427
476
  end
428
477
  private :build_type
478
+
479
+ def define_accessors(keys)
480
+ keys.each do |key|
481
+ next if instance_methods.include?(key)
482
+
483
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
484
+ def #{key} # def email
485
+ @attributes[#{key.inspect}] # @attributes[:email]
486
+ end # end
487
+ RUBY
488
+ end
489
+ end
490
+ private :define_accessors
429
491
  end
430
492
  end
431
493
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ class Struct
5
+ class Compiler < Types::Compiler
6
+ def visit_struct(node)
7
+ struct, _ = node
8
+
9
+ struct.__getobj__
10
+ rescue ::WeakRef::RefError
11
+ if struct.weakref_alive?
12
+ raise
13
+ else
14
+ raise RecycledStructError
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,29 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  class Struct
3
- class Constructor
4
- include Dry::Equalizer(:type)
5
- include Dry::Types::Type
6
-
7
- # @return [#call]
8
- attr_reader :fn
9
-
10
- # @return [#call]
11
- attr_reader :type
12
-
13
- # @param [Struct] type
14
- # @param [Hash] options
15
- # @param [#call, nil] block
16
- def initialize(type, options = {}, &block)
17
- @type = type
18
- @fn = options.fetch(:fn, block)
19
- end
20
-
21
- # @param [Object] input
22
- # @return [Object]
23
- def call(input)
24
- type[fn[input]]
25
- end
26
- alias_method :[], :call
5
+ class Constructor < Types::Constructor
6
+ alias_method :primitive, :type
27
7
  end
28
8
  end
29
9
  end
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  class Struct
3
5
  # Raised when given input doesn't conform schema and constructor type
4
6
  Error = Class.new(TypeError)
5
7
 
6
8
  # Raised when defining duplicate attributes
7
- class RepeatedAttributeError < ArgumentError
9
+ class RepeatedAttributeError < ::ArgumentError
8
10
  # @param [Symbol] key
9
11
  # attribute name that is the same as previously defined one
10
12
  def initialize(key)
@@ -13,9 +15,17 @@ module Dry
13
15
  end
14
16
 
15
17
  # Raised when a struct doesn't have an attribute
16
- class MissingAttributeError < KeyError
18
+ class MissingAttributeError < ::KeyError
17
19
  def initialize(key)
18
- super("Missing attribute: #{ key.inspect }")
20
+ super("Missing attribute: #{key.inspect}")
21
+ end
22
+ end
23
+
24
+ # When struct class stored in ast was garbage collected because no alive objects exists
25
+ # This shouldn't happen in a working application
26
+ class RecycledStructError < ::RuntimeError
27
+ def initialize
28
+ super("Reference to struct class was garbage collected")
19
29
  end
20
30
  end
21
31
  end
@@ -1,16 +1,18 @@
1
- require 'pp'
1
+ # frozen_string_literal: true
2
+
3
+ require "pp"
2
4
 
3
5
  module Dry
4
6
  class Struct
5
7
  def pretty_print(pp)
6
8
  klass = self.class
7
- pp.group(1, "#<#{ klass.name || klass.inspect }", '>') do
8
- pp.seplist(@attributes.keys, proc { pp.text ',' }) do |column_name|
9
+ pp.group(1, "#<#{klass.name || klass.inspect}", ">") do
10
+ pp.seplist(@attributes.keys, proc { pp.text "," }) do |column_name|
9
11
  column_value = @attributes[column_name]
10
- pp.breakable ' '
12
+ pp.breakable " "
11
13
  pp.group(1) do
12
- pp.text column_name
13
- pp.text '='
14
+ pp.text column_name.to_s
15
+ pp.text "="
14
16
  pp.pp column_value
15
17
  end
16
18
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Dry::Struct.register_extension(:pretty_print) do
2
- require 'dry/struct/extensions/pretty_print'
4
+ require "dry/struct/extensions/pretty_print"
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  class Struct
3
5
  # Helper for {Struct#to_hash} implementation
@@ -6,12 +8,10 @@ module Dry
6
8
  # @param [#to_hash, #map, Object] value
7
9
  # @return [Hash, Array]
8
10
  def self.[](value)
9
- if value.respond_to?(:to_hash)
10
- if RUBY_VERSION >= '2.4'
11
- value.to_hash.transform_values { |v| self[v] }
12
- else
13
- value.to_hash.each_with_object({}) { |(k, v), h| h[k] = self[v] }
14
- end
11
+ if value.is_a?(Struct)
12
+ value.to_h.transform_values { |current| self[current] }
13
+ elsif value.respond_to?(:to_hash)
14
+ value.to_hash.transform_values { |current| self[current] }
15
15
  elsif value.respond_to?(:to_ary)
16
16
  value.to_ary.map { |item| self[item] }
17
17
  else
@@ -1,10 +1,13 @@
1
- require 'dry/types/printer'
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/types/printer"
2
4
 
3
5
  module Dry
4
6
  module Types
5
7
  # @api private
6
8
  class Printer
7
9
  MAPPING[Struct::Sum] = :visit_struct_sum
10
+ MAPPING[Struct::Constructor] = :visit_struct_constructor
8
11
 
9
12
  def visit_struct_sum(sum)
10
13
  visit_sum_constructors(sum) do |constructors|
@@ -13,6 +16,8 @@ module Dry
13
16
  end
14
17
  end
15
18
  end
19
+
20
+ alias_method :visit_struct_constructor, :visit_constructor
16
21
  end
17
22
  end
18
23
  end