dry-struct 1.2.0 → 1.3.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +105 -61
  3. data/LICENSE +1 -1
  4. data/README.md +16 -12
  5. data/dry-struct.gemspec +26 -27
  6. data/lib/dry-struct.rb +2 -0
  7. data/lib/dry/struct.rb +14 -3
  8. data/lib/dry/struct/class_interface.rb +91 -34
  9. data/lib/dry/struct/compiler.rb +22 -0
  10. data/lib/dry/struct/constructor.rb +4 -24
  11. data/lib/dry/struct/errors.rb +13 -3
  12. data/lib/dry/struct/extensions.rb +2 -0
  13. data/lib/dry/struct/extensions/pretty_print.rb +3 -1
  14. data/lib/dry/struct/hashify.rb +5 -1
  15. data/lib/dry/struct/printer.rb +5 -0
  16. data/lib/dry/struct/struct_builder.rb +18 -11
  17. data/lib/dry/struct/sum.rb +3 -0
  18. data/lib/dry/struct/value.rb +4 -6
  19. data/lib/dry/struct/version.rb +3 -1
  20. metadata +36 -59
  21. data/.codeclimate.yml +0 -12
  22. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +0 -10
  23. data/.github/ISSUE_TEMPLATE/---bug-report.md +0 -30
  24. data/.github/ISSUE_TEMPLATE/---feature-request.md +0 -18
  25. data/.github/workflows/ci.yml +0 -74
  26. data/.github/workflows/docsite.yml +0 -34
  27. data/.github/workflows/sync_configs.yml +0 -34
  28. data/.gitignore +0 -12
  29. data/.rspec +0 -4
  30. data/.rubocop.yml +0 -95
  31. data/.yardopts +0 -4
  32. data/CODE_OF_CONDUCT.md +0 -13
  33. data/CONTRIBUTING.md +0 -29
  34. data/Gemfile +0 -28
  35. data/Rakefile +0 -10
  36. data/benchmarks/basic.rb +0 -57
  37. data/benchmarks/constrained.rb +0 -37
  38. data/benchmarks/profile_instantiation.rb +0 -19
  39. data/benchmarks/setup.rb +0 -11
  40. data/bin/console +0 -12
  41. data/bin/setup +0 -7
  42. data/docsite/source/index.html.md +0 -103
  43. data/docsite/source/nested-structs.html.md +0 -49
  44. data/docsite/source/recipes.html.md +0 -143
  45. data/log/.gitkeep +0 -0
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/struct'
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry-types'
2
4
  require 'dry-equalizer'
3
5
  require 'dry/core/extensions'
4
6
  require 'dry/core/constants'
7
+ require 'dry/core/deprecations'
5
8
 
6
9
  require 'dry/struct/version'
7
10
  require 'dry/struct/errors'
@@ -85,6 +88,12 @@ module Dry
85
88
  extend Core::Extensions
86
89
  include Core::Constants
87
90
  extend ClassInterface
91
+ extend Core::Deprecations[:'dry-struct']
92
+
93
+ class << self
94
+ # override `Dry::Types::Builder#prepend`
95
+ define_method(:prepend, ::Module.method(:prepend))
96
+ end
88
97
 
89
98
  autoload :Value, 'dry/struct/value'
90
99
 
@@ -95,7 +104,8 @@ module Dry
95
104
  defines :schema
96
105
  schema Types['coercible.hash'].schema(EMPTY_HASH)
97
106
 
98
- @meta = EMPTY_HASH
107
+ defines :abstract_class
108
+ abstract
99
109
 
100
110
  # @!attribute [Hash{Symbol => Object}] attributes
101
111
  attr_reader :attributes
@@ -144,12 +154,13 @@ module Dry
144
154
  # )
145
155
  # rom_n_roda.to_hash
146
156
  # #=> {title: 'Web Development with ROM and Roda', subtitle: nil}
147
- def to_hash
157
+ def to_h
148
158
  self.class.schema.each_with_object({}) do |key, result|
149
159
  result[key.name] = Hashify[self[key.name]] if attributes.key?(key.name)
150
160
  end
151
161
  end
152
- alias_method :to_h, :to_hash
162
+ alias_method :to_hash, :to_h
163
+ # deprecate :to_hash, :to_h, message: "Implicit convertion structs to hashes is deprecated. Use .to_h"
153
164
 
154
165
  # Create a copy of {Dry::Struct} with overriden attributes
155
166
  #
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'weakref'
1
4
  require 'dry/core/class_attributes'
2
5
  require 'dry/core/inflector'
3
6
  require 'dry/core/descendants_tracker'
@@ -19,14 +22,8 @@ module Dry
19
22
  def inherited(klass)
20
23
  super
21
24
 
22
- base = self
23
-
24
- klass.class_eval do
25
- @meta = base.meta
26
-
27
- unless name.eql?('Dry::Struct::Value')
28
- extend Core::DescendantsTracker
29
- end
25
+ unless klass.name.eql?('Dry::Struct::Value')
26
+ klass.extend(Core::DescendantsTracker)
30
27
  end
31
28
  end
32
29
 
@@ -96,10 +93,45 @@ module Dry
96
93
  # ruby.celebrities[0].pseudonym #=> 'Matz'
97
94
  # ruby.celebrities[1].name #=> 'Aaron Patterson'
98
95
  # ruby.celebrities[1].pseudonym #=> 'tenderlove'
99
- def attribute(name, type = nil, &block)
96
+ def attribute(name, type = Undefined, &block)
100
97
  attributes(name => build_type(name, type, &block))
101
98
  end
102
99
 
100
+ # Add atributes from another struct
101
+ #
102
+ # @example
103
+ # class Address < Dry::Struct
104
+ # attribute :city, Types::String
105
+ # attribute :country, Types::String
106
+ # end
107
+ #
108
+ # class User < Dry::Struct
109
+ # attribute :name, Types::String
110
+ # attributes_from Address
111
+ # end
112
+ #
113
+ # User.new(name: 'Quispe', city: 'La Paz', country: 'Bolivia')
114
+ #
115
+ # @example with nested structs
116
+ # class User < Dry::Struct
117
+ # attribute :name, Types::String
118
+ # attribute :address do
119
+ # attributes_from Address
120
+ # end
121
+ # end
122
+ #
123
+ # @param struct [Dry::Struct]
124
+ def attributes_from(struct)
125
+ extracted_schema = struct.schema.keys.map { |key|
126
+ if key.required?
127
+ [key.name, key.type]
128
+ else
129
+ [:"#{key.name}?", key.type]
130
+ end
131
+ }.to_h
132
+ attributes(extracted_schema)
133
+ end
134
+
103
135
  # Adds an omittable (key is not required on initialization) attribute for this {Struct}
104
136
  #
105
137
  # @example
@@ -124,9 +156,9 @@ module Dry
124
156
 
125
157
  has_attribute?(args[0])
126
158
  else
127
- name, type = args
159
+ name, * = args
128
160
 
129
- attribute(:"#{ name }?", build_type(name, type, &block))
161
+ attribute(:"#{name}?", build_type(*args, &block))
130
162
  end
131
163
  end
132
164
 
@@ -154,14 +186,7 @@ module Dry
154
186
 
155
187
  schema schema.schema(new_schema)
156
188
 
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
189
+ define_accessors(keys)
165
190
 
166
191
  @attribute_names = nil
167
192
 
@@ -222,16 +247,25 @@ module Dry
222
247
 
223
248
  # @param [Hash{Symbol => Object},Dry::Struct] attributes
224
249
  # @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
250
+ def new(attributes = default_attributes, safe = false, &block)
251
+ if attributes.is_a?(Struct)
252
+ if equal?(attributes.class)
253
+ attributes
254
+ else
255
+ # This implicit coercion is arguable but makes sense overall
256
+ # in cases there you pass child struct to the base struct constructor
257
+ # User.new(super_user)
258
+ #
259
+ # We may deprecate this behavior in future forcing people to be explicit
260
+ new(attributes.to_h, safe, &block)
261
+ end
228
262
  elsif safe
229
263
  load(schema.call_safe(attributes) { |output = attributes| return yield output })
230
264
  else
231
265
  load(schema.call_unsafe(attributes))
232
266
  end
233
267
  rescue Types::CoercionError => error
234
- raise Error, "[#{self}.new] #{error}"
268
+ raise Error, "[#{self}.new] #{error}", error.backtrace
235
269
  end
236
270
 
237
271
  # @api private
@@ -260,10 +294,9 @@ module Dry
260
294
  end
261
295
 
262
296
  # @param [#call,nil] constructor
263
- # @param [Hash] _options
264
297
  # @param [#call,nil] block
265
298
  # @return [Dry::Struct::Constructor]
266
- def constructor(constructor = nil, **_options, &block)
299
+ def constructor(constructor = nil, **, &block)
267
300
  Constructor.new(self, fn: constructor || block)
268
301
  end
269
302
 
@@ -336,7 +369,7 @@ module Dry
336
369
 
337
370
  # @return [Proc]
338
371
  def to_proc
339
- proc { |input| call(input) }
372
+ @to_proc ||= proc { |input| call(input) }
340
373
  end
341
374
 
342
375
  # Checks if this {Struct} has the given attribute
@@ -357,10 +390,12 @@ module Dry
357
390
  # @return [{Symbol => Object}]
358
391
  def meta(meta = Undefined)
359
392
  if meta.equal?(Undefined)
360
- @meta
393
+ schema.meta
394
+ elsif meta.empty?
395
+ self
361
396
  else
362
397
  ::Class.new(self) do
363
- @meta = @meta.merge(meta) unless meta.empty?
398
+ schema schema.meta(meta) unless meta.empty?
364
399
  end
365
400
  end
366
401
  end
@@ -376,6 +411,21 @@ module Dry
376
411
  end
377
412
  end
378
413
 
414
+ # Make the struct abstract. This class will be used as a default
415
+ # parent class for nested structs
416
+ def abstract
417
+ abstract_class self
418
+ end
419
+
420
+ # Dump to the AST
421
+ #
422
+ # @return [Array]
423
+ #
424
+ # @api public
425
+ def to_ast(meta: true)
426
+ [:struct, [::WeakRef.new(self), schema.to_ast(meta: meta)]]
427
+ end
428
+
379
429
  # Stores an object for building nested struct classes
380
430
  # @return [StructBuilder]
381
431
  def struct_builder
@@ -406,11 +456,11 @@ module Dry
406
456
  # Constructs a type
407
457
  #
408
458
  # @return [Dry::Types::Type, Dry::Struct]
409
- def build_type(name, type, &block)
459
+ def build_type(name, type = Undefined, &block)
410
460
  type_object =
411
461
  if type.is_a?(::String)
412
462
  Types[type]
413
- elsif block.nil? && type.nil?
463
+ elsif block.nil? && Undefined.equal?(type)
414
464
  raise(
415
465
  ::ArgumentError,
416
466
  'you must supply a type or a block to `Dry::Struct.attribute`'
@@ -427,11 +477,18 @@ module Dry
427
477
  end
428
478
  private :build_type
429
479
 
430
- # @api private
431
- # @return [Boolean]
432
- def value?
433
- false
480
+ def define_accessors(keys)
481
+ keys.each do |key|
482
+ next if instance_methods.include?(key)
483
+
484
+ class_eval(<<-RUBY)
485
+ def #{key}
486
+ @attributes[#{key.inspect}]
487
+ end
488
+ RUBY
489
+ end
434
490
  end
491
+ private :define_accessors
435
492
  end
436
493
  end
437
494
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'weakref'
4
+ require 'dry/types/compiler'
5
+
6
+ module Dry
7
+ class Struct
8
+ class Compiler < Types::Compiler
9
+ def visit_struct(node)
10
+ struct, _ = node
11
+
12
+ struct.__getobj__
13
+ rescue ::WeakRef::RefError
14
+ if struct.weakref_alive?
15
+ raise
16
+ else
17
+ raise RecycledStructError
18
+ end
19
+ end
20
+ end
21
+ end
22
+ 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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Dry::Struct.register_extension(:pretty_print) do
2
4
  require 'dry/struct/extensions/pretty_print'
3
5
  end
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
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
9
+ pp.group(1, "#<#{klass.name || klass.inspect}", '>') do
8
10
  pp.seplist(@attributes.keys, proc { pp.text ',' }) do |column_name|
9
11
  column_value = @attributes[column_name]
10
12
  pp.breakable ' '
@@ -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,7 +8,9 @@ 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)
11
+ if value.is_a?(Struct)
12
+ value.to_h.transform_values { |current| self[current] }
13
+ elsif value.respond_to?(:to_hash)
10
14
  value.to_hash.transform_values { |current| self[current] }
11
15
  elsif value.respond_to?(:to_ary)
12
16
  value.to_ary.map { |item| self[item] }
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/types/printer'
2
4
 
3
5
  module Dry
@@ -5,6 +7,7 @@ module Dry
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
@@ -1,9 +1,11 @@
1
- require 'dry/types/compiler'
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/struct/compiler'
2
4
 
3
5
  module Dry
4
6
  class Struct
5
7
  # @private
6
- class StructBuilder < Types::Compiler
8
+ class StructBuilder < Compiler
7
9
  attr_reader :struct
8
10
 
9
11
  def initialize(struct)
@@ -12,13 +14,22 @@ module Dry
12
14
  end
13
15
 
14
16
  # @param [Symbol|String] attr_name the name of the nested type
15
- # @param [Dry::Struct,Dry::Types::Type::Array] type the superclass of the nested struct
17
+ # @param [Dry::Struct,Dry::Types::Type::Array,Undefined] type the superclass of the nested struct
16
18
  # @yield the body of the nested struct
17
19
  def call(attr_name, type, &block)
18
20
  const_name = const_name(type, attr_name)
19
21
  check_name(const_name)
20
22
 
21
- new_type = ::Class.new(parent(type), &block)
23
+ builder = self
24
+ parent = parent(type)
25
+
26
+ new_type = ::Class.new(Undefined.default(parent, struct.abstract_class)) do
27
+ if Undefined.equal?(parent)
28
+ schema builder.struct.schema.clear
29
+ end
30
+
31
+ class_exec(&block)
32
+ end
22
33
  struct.const_set(const_name, new_type)
23
34
 
24
35
  if array?(type)
@@ -38,14 +49,10 @@ module Dry
38
49
  if array?(type)
39
50
  visit(type.to_ast)
40
51
  else
41
- type || default_superclass
52
+ type
42
53
  end
43
54
  end
44
55
 
45
- def default_superclass
46
- struct.value? ? Value : Struct
47
- end
48
-
49
56
  def const_name(type, attr_name)
50
57
  snake_name =
51
58
  if array?(type)
@@ -71,11 +78,11 @@ module Dry
71
78
 
72
79
  def visit_array(node)
73
80
  member, * = node
74
- member
81
+ visit(member)
75
82
  end
76
83
 
77
84
  def visit_nominal(*)
78
- default_superclass
85
+ Undefined
79
86
  end
80
87
 
81
88
  def visit_constructor(node)