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.
- 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)
|