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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +105 -61
- data/LICENSE +1 -1
- data/README.md +16 -12
- data/dry-struct.gemspec +26 -27
- data/lib/dry-struct.rb +2 -0
- data/lib/dry/struct.rb +14 -3
- data/lib/dry/struct/class_interface.rb +91 -34
- data/lib/dry/struct/compiler.rb +22 -0
- data/lib/dry/struct/constructor.rb +4 -24
- data/lib/dry/struct/errors.rb +13 -3
- data/lib/dry/struct/extensions.rb +2 -0
- data/lib/dry/struct/extensions/pretty_print.rb +3 -1
- data/lib/dry/struct/hashify.rb +5 -1
- data/lib/dry/struct/printer.rb +5 -0
- data/lib/dry/struct/struct_builder.rb +18 -11
- data/lib/dry/struct/sum.rb +3 -0
- data/lib/dry/struct/value.rb +4 -6
- data/lib/dry/struct/version.rb +3 -1
- metadata +36 -59
- data/.codeclimate.yml +0 -12
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +0 -10
- data/.github/ISSUE_TEMPLATE/---bug-report.md +0 -30
- data/.github/ISSUE_TEMPLATE/---feature-request.md +0 -18
- data/.github/workflows/ci.yml +0 -74
- data/.github/workflows/docsite.yml +0 -34
- data/.github/workflows/sync_configs.yml +0 -34
- data/.gitignore +0 -12
- data/.rspec +0 -4
- data/.rubocop.yml +0 -95
- data/.yardopts +0 -4
- data/CODE_OF_CONDUCT.md +0 -13
- data/CONTRIBUTING.md +0 -29
- data/Gemfile +0 -28
- data/Rakefile +0 -10
- data/benchmarks/basic.rb +0 -57
- data/benchmarks/constrained.rb +0 -37
- data/benchmarks/profile_instantiation.rb +0 -19
- data/benchmarks/setup.rb +0 -11
- data/bin/console +0 -12
- data/bin/setup +0 -7
- data/docsite/source/index.html.md +0 -103
- data/docsite/source/nested-structs.html.md +0 -49
- data/docsite/source/recipes.html.md +0 -143
- data/log/.gitkeep +0 -0
data/lib/dry-struct.rb
CHANGED
data/lib/dry/struct.rb
CHANGED
@@ -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
|
-
|
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
|
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 :
|
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
|
-
|
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 =
|
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,
|
159
|
+
name, * = args
|
128
160
|
|
129
|
-
attribute(:"#{
|
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
|
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
|
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,
|
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
|
-
|
393
|
+
schema.meta
|
394
|
+
elsif meta.empty?
|
395
|
+
self
|
361
396
|
else
|
362
397
|
::Class.new(self) do
|
363
|
-
|
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? &&
|
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
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
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
|
-
|
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
|
data/lib/dry/struct/errors.rb
CHANGED
@@ -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: #{
|
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,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, "#<#{
|
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 ' '
|
data/lib/dry/struct/hashify.rb
CHANGED
@@ -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.
|
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] }
|
data/lib/dry/struct/printer.rb
CHANGED
@@ -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
|
-
|
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 <
|
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
|
-
|
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
|
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
|
-
|
85
|
+
Undefined
|
79
86
|
end
|
80
87
|
|
81
88
|
def visit_constructor(node)
|