dry-struct 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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)