dry-struct 0.4.0 → 0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 33a4358e2a11c38b8816778acc5722b6d08a4c4d
4
- data.tar.gz: 24bebcf82ec2e1dcb7536216acc397ae9aa6a3c7
2
+ SHA256:
3
+ metadata.gz: 33cd05dd51a3310d7ede685ae1ce8631a0955d7563afca7633a7625b9b34dd17
4
+ data.tar.gz: 182c58865ccb3e74595aa22d6c9730ce8ca0e078d1b6ecb5e8b69e026ce6409d
5
5
  SHA512:
6
- metadata.gz: 332a625be1cde86fefd74efe78e79b375c6c66a15f5ad062f26e736055b9a9e9aef9234299bd0a7ff0272ce2e80218060d0ddb52d0074a5bc7d1ed12a03e1971
7
- data.tar.gz: 757d33e5151cf3f30ffbe28798f1c86ab634f12612d4a7603431aae72513eedf088a3cf483603809b10ac6da870da2dc2f39ef1136e6fe99c42b25ee979ea681
6
+ metadata.gz: 3f5e4f6609b93e80ae775ab556812cc872883eaf423a74ae2e5191d2bef6907152653fa7fe441bb9aab945b7cf9f4ec62dd4d359ffcb88f4e976b8b53fb42ac8
7
+ data.tar.gz: 4b76e94533ff98f1c1d9d427967a5c19af3d58484a5e785c7eac15d50211d70e4718d35572bb6e89fbafeb9b9f092a73f68e517244676f28d4c7e4854f1a055a
data/.gitignore CHANGED
@@ -7,5 +7,6 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ *.log
10
11
 
11
12
  .DS_Store
data/.travis.yml CHANGED
@@ -4,13 +4,16 @@ sudo: required
4
4
  bundler_args: --without benchmarks tools
5
5
  script:
6
6
  - bundle exec rake spec
7
+ before_install:
8
+ - gem update --system
7
9
  after_success:
8
10
  - '[ -d coverage ] && bundle exec codeclimate-test-reporter'
9
11
  rvm:
10
- - 2.2.7
11
- - 2.3.3
12
- - 2.4.1
13
- - jruby-9.1.10.0
12
+ - 2.2.10
13
+ - 2.3.7
14
+ - 2.4.4
15
+ - 2.5.1
16
+ - jruby-9.2.0.0
14
17
  env:
15
18
  global:
16
19
  - COVERAGE=true
data/CHANGELOG.md CHANGED
@@ -1,3 +1,117 @@
1
+ # v0.6.0 2018-10-24
2
+
3
+ ## BREAKING CHANGES
4
+
5
+ * `Struct.attribute?` in the old sense is deprecated, use `has_attribute?` as a replacement
6
+
7
+ ## Added
8
+
9
+ * `Struct.attribute?` is an easy way to define omittable attributes (flash-gordon):
10
+
11
+ ```ruby
12
+ class User < Dry::Struct
13
+ attribute :name, Types::Strict::String
14
+ attribute? :email, Types::Strict::String
15
+ end
16
+ # User.new(name: 'John') # => #<User name="John">
17
+ ```
18
+
19
+ ## Fixed
20
+
21
+ * `Struct#to_h` recursively converts hash values to hashes, this was done to be consistent with current behavior for arrays (oeoeaio + ZimbiX)
22
+
23
+ [Compare v0.5.1...v0.6.0](https://github.com/dry-rb/dry-struct/compare/v0.5.1...v0.6.0)
24
+
25
+ # v0.5.1 2018-08-11
26
+
27
+ ## Fixed
28
+
29
+ * Constant resolution is now restricted to the current module when structs are automatically defined using the block syntax. This shouldn't break any existing code (piktur)
30
+
31
+ ## Added
32
+
33
+ * Pretty print extension (ojab)
34
+ ```ruby
35
+ Dry::Struct.load_extensions(:pretty_print)
36
+ PP.pp(user)
37
+ #<Test::User
38
+ name="Jane",
39
+ age=21,
40
+ address=#<Test::Address city="NYC", zipcode="123">>
41
+ ```
42
+
43
+ [Compare v0.5.0...v0.5.1](https://github.com/dry-rb/dry-struct/compare/v0.5.0...v0.5.1)
44
+
45
+ # v0.5.0 2018-05-03
46
+
47
+ ## BREAKING CHANGES
48
+
49
+ * `constructor_type` was removed, use `transform_types` and `transform_keys` as a replacement (see below)
50
+ * Default types are evaluated _only_ on missing values. Again, use `tranform_types` as a work around for `nil`s
51
+ * Values are now stored within a single instance variable names `@attributes`, this sped up struct creation and improved support for reserved attribute names such as `hash`, they don't get a getter but still can be read via `#[]`
52
+ * Ruby 2.3 is a minimal supported version
53
+
54
+ ## Added
55
+
56
+ * `Dry::Struct.transform_types` accepts a block which is yielded on every type to add. Since types are `dry-types`' objects that come with a robust DSL it's rather simple to restore the behavior of `constructor_type`. See https://github.com/dry-rb/dry-struct/pull/64 for details (flash-gordon)
57
+
58
+ Example: evaluate defaults on `nil` values
59
+
60
+ ```ruby
61
+ class User < Dry::Struct
62
+ transform_types do |type|
63
+ type.constructor { |value| value.nil? ? Undefined : value }
64
+ end
65
+ end
66
+ ```
67
+
68
+ * `Data::Struct.transform_keys` accepts a block/proc that transforms keys of input hashes. The most obvious usage is simbolization but arbitrary transformations are allowed (flash-gordon)
69
+
70
+ * `Dry.Struct` builds a struct by a hash of attribute names and types (citizen428)
71
+
72
+ ```ruby
73
+ User = Dry::Struct(name: 'strict.string') do
74
+ attribute :email, 'strict.string'
75
+ end
76
+ ```
77
+
78
+ * Support for `Struct.meta`, note that `.meta` returns a _new class_ (flash-gordon)
79
+
80
+ ```ruby
81
+ class User < Dry::Struct
82
+ attribute :name, Dry::Types['strict.string']
83
+ end
84
+
85
+ UserWithMeta = User.meta(foo: :bar)
86
+
87
+ User.new(name: 'Jade').class == UserWithMeta.new(name: 'Jade').class # => false
88
+ ```
89
+
90
+ * `Struct.attribute` yields a block with definition for nested structs. It defines a nested constant for the new struct and supports arrays (AMHOL + flash-gordon)
91
+
92
+ ```ruby
93
+ class User < Dry::Struct
94
+ attribute :name, Types::Strict::String
95
+ attribute :address do
96
+ attribute :country, Types::Strict::String
97
+ attribute :city, Types::Strict::String
98
+ end
99
+ attribute :accounts, Types::Strict::Array do
100
+ attribute :currency, Types::Strict::String
101
+ attribute :balance, Types::Strict::Decimal
102
+ end
103
+ end
104
+
105
+ # ^This automatically defines User::Address and User::Account
106
+ ```
107
+
108
+ ## Fixed
109
+
110
+ * Adding a new attribute invalidates `attribute_names` (flash-gordon)
111
+ * Struct classes track subclasses and define attributes in them, now it doesn't matter whether you define attributes first and _then_ subclass or vice versa. Note this can lead to memory leaks in Rails environment when struct classes are reloaded (flash-gordon)
112
+
113
+ [Compare v0.4.0...v0.5.0](https://github.com/dry-rb/dry-struct/compare/v0.4.0...v0.5.0)
114
+
1
115
  # v0.4.0 2017-11-04
2
116
 
3
117
  ## Changed
data/CONTRIBUTING.md CHANGED
@@ -6,11 +6,11 @@ If you found a bug, report an issue and describe what's the expected behavior ve
6
6
 
7
7
  ## Reporting feature requests
8
8
 
9
- Report a feature request **only after discussing it first on [discuss.dry-rb.org](https://discuss.dry-rb.org)** where it was accepted. Please provide a concise description of the feature, don't link to a discussion thread, and instead summarize what was discussed.
9
+ Report a feature request **only after discourseing it first on [discourse.dry-rb.org](https://discourse.dry-rb.org)** where it was accepted. Please provide a concise description of the feature, don't link to a discourseion thread, and instead summarize what was discourseed.
10
10
 
11
11
  ## Reporting questions, support requests, ideas, concerns etc.
12
12
 
13
- **PLEASE DON'T** - use [discuss.dry-rb.org](http://discuss.dry-rb.org) instead.
13
+ **PLEASE DON'T** - use [discourse.dry-rb.org](https://discourse.dry-rb.org) instead.
14
14
 
15
15
  # Pull Request Guidelines
16
16
 
@@ -26,4 +26,4 @@ Other requirements:
26
26
 
27
27
  # Asking for help
28
28
 
29
- If these guidelines aren't helpful, and you're stuck, please post a message on [discuss.dry-rb.org](https://discuss.dry-rb.org).
29
+ If these guidelines aren't helpful, and you're stuck, please post a message on [discourse.dry-rb.org](https://discourse.dry-rb.org).
data/Gemfile CHANGED
@@ -3,6 +3,7 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  gem 'dry-types', git: 'https://github.com/dry-rb/dry-types'
6
+ gem 'dry-inflector', git: 'https://github.com/dry-rb/dry-inflector'
6
7
 
7
8
  group :test do
8
9
  gem 'codeclimate-test-reporter', platform: :mri, require: false
@@ -11,7 +12,8 @@ group :test do
11
12
  end
12
13
 
13
14
  group :tools do
14
- gem 'byebug', platform: :mri
15
+ gem 'pry-byebug', platform: :mri
16
+ gem 'pry', platform: :jruby
15
17
  gem 'mutant'
16
18
  gem 'mutant-rspec'
17
19
  end
data/README.md CHANGED
@@ -1,6 +1,5 @@
1
1
  [gem]: https://rubygems.org/gems/dry-struct
2
2
  [travis]: https://travis-ci.org/dry-rb/dry-struct
3
- [gemnasium]: https://gemnasium.com/dry-rb/dry-struct
4
3
  [codeclimate]: https://codeclimate.com/github/dry-rb/dry-struct
5
4
  [coveralls]: https://coveralls.io/r/dry-rb/dry-struct
6
5
  [inchpages]: http://inch-ci.org/github/dry-rb/dry-struct
@@ -9,7 +8,6 @@
9
8
 
10
9
  [![Gem Version](https://badge.fury.io/rb/dry-struct.svg)][gem]
11
10
  [![Build Status](https://travis-ci.org/dry-rb/dry-struct.svg?branch=master)][travis]
12
- [![Dependency Status](https://gemnasium.com/dry-rb/dry-struct.svg)][gemnasium]
13
11
  [![Code Climate](https://codeclimate.com/github/dry-rb/dry-struct/badges/gpa.svg)][codeclimate]
14
12
  [![Test Coverage](https://codeclimate.com/github/dry-rb/dry-struct/badges/coverage.svg)][codeclimate]
15
13
  [![Inline docs](http://inch-ci.org/github/dry-rb/dry-struct.svg?branch=master)][inchpages]
data/bin/console CHANGED
@@ -3,12 +3,10 @@
3
3
  require 'bundler/setup'
4
4
  require 'dry/struct'
5
5
 
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
6
+ require 'irb'
8
7
 
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
8
+ module Types
9
+ include Dry::Types.module
10
+ end
12
11
 
13
- require 'irb'
14
- IRB.start
12
+ binding.irb
data/dry-struct.gemspec CHANGED
@@ -17,6 +17,8 @@ Gem::Specification.new do |spec|
17
17
  # delete this section to allow pushing this gem to any host.
18
18
  if spec.respond_to?(:metadata)
19
19
  spec.metadata['allowed_push_host'] = 'https://rubygems.org'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/dry-rb/dry-struct/blob/master/CHANGELOG.md'
21
+ spec.metadata['source_code_uri'] = 'https://github.com/dry-rb/dry-struct'
20
22
  else
21
23
  raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
22
24
  end
@@ -27,8 +29,8 @@ Gem::Specification.new do |spec|
27
29
  spec.require_paths = ['lib']
28
30
 
29
31
  spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
30
- spec.add_runtime_dependency 'dry-types', '~> 0.12', '>= 0.12.2'
31
- spec.add_runtime_dependency 'dry-core', '~> 0.4', '>= 0.4.1'
32
+ spec.add_runtime_dependency 'dry-types', '~> 0.13'
33
+ spec.add_runtime_dependency 'dry-core', '~> 0.4', '>= 0.4.3'
32
34
  spec.add_runtime_dependency 'ice_nine', '~> 0.11'
33
35
 
34
36
  spec.add_development_dependency 'bundler', '~> 1.6'
@@ -1,8 +1,10 @@
1
1
  require 'dry/core/class_attributes'
2
- require 'dry/equalizer'
2
+ require 'dry/core/inflector'
3
+ require 'dry/core/descendants_tracker'
3
4
 
4
5
  require 'dry/struct/errors'
5
6
  require 'dry/struct/constructor'
7
+ require 'dry/struct/sum'
6
8
 
7
9
  module Dry
8
10
  class Struct
@@ -17,31 +19,118 @@ module Dry
17
19
  def inherited(klass)
18
20
  super
19
21
 
20
- klass.equalizer Equalizer.new(*schema.keys)
21
- klass.send(:include, klass.equalizer)
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
30
+ end
22
31
  end
23
32
 
24
33
  # Adds an attribute for this {Struct} with given `name` and `type`
25
34
  # and modifies {.schema} accordingly.
26
35
  #
27
36
  # @param [Symbol] name name of the defined attribute
28
- # @param [Dry::Types::Definition] type
37
+ # @param [Dry::Types::Definition, nil] type or superclass of nested type
29
38
  # @return [Dry::Struct]
39
+ # @yield
40
+ # If a block is given, it will be evaluated in the context of
41
+ # a new struct class, and set as a nested type for the given
42
+ # attribute. A class with a matching name will also be defined for
43
+ # the nested type.
30
44
  # @raise [RepeatedAttributeError] when trying to define attribute with the
31
45
  # same name as previously defined one
32
46
  #
33
- # @example
47
+ # @example with nested structs
48
+ # class Language < Dry::Struct
49
+ # attribute :name, Types::String
50
+ # attribute :details, Dry::Struct do
51
+ # attribute :type, Types::String
52
+ # end
53
+ # end
54
+ #
55
+ # Language.schema
56
+ # #=> {
57
+ # :name=>#<Dry::Types::Definition primitive=String options={} meta={}>,
58
+ # :details=>Language::Details
59
+ # }
60
+ #
61
+ # ruby = Language.new(name: 'Ruby', details: { type: 'OO' })
62
+ # ruby.name #=> 'Ruby'
63
+ # ruby.details #=> #<Language::Details type="OO">
64
+ # ruby.details.type #=> 'OO'
65
+ #
66
+ # @example with a nested array of structs
34
67
  # class Language < Dry::Struct
35
68
  # attribute :name, Types::String
69
+ # array :versions, Types::String
70
+ # array :celebrities, Types::Array.of(Dry::Struct) do
71
+ # attribute :name, Types::String
72
+ # attribute :pseudonym, Types::String
73
+ # end
36
74
  # end
37
75
  #
38
76
  # Language.schema
39
- # #=> {name: #<Dry::Types::Definition primitive=String options={}>}
77
+ # #=> {
78
+ # :name=>#<Dry::Types::Definition primitive=String options={} meta={}>,
79
+ # :versions=>#<Dry::Types::Array::Member primitive=Array options={:member=>#<Dry::Types::Definition primitive=String options={} meta={}>} meta={}>,
80
+ # :celebrities=>#<Dry::Types::Array::Member primitive=Array options={:member=>Language::Celebrity} meta={}>
81
+ # }
40
82
  #
41
- # ruby = Language.new(name: 'Ruby')
83
+ # ruby = Language.new(
84
+ # name: 'Ruby',
85
+ # versions: %w(1.8.7 1.9.8 2.0.1),
86
+ # celebrities: [
87
+ # { name: 'Yukihiro Matsumoto', pseudonym: 'Matz' },
88
+ # { name: 'Aaron Patterson', pseudonym: 'tenderlove' }
89
+ # ]
90
+ # )
42
91
  # ruby.name #=> 'Ruby'
43
- def attribute(name, type)
44
- attributes(name => type)
92
+ # ruby.versions #=> ['1.8.7', '1.9.8', '2.0.1']
93
+ # ruby.celebrities
94
+ # #=> [
95
+ # #<Language::Celebrity name='Yukihiro Matsumoto' pseudonym='Matz'>,
96
+ # #<Language::Celebrity name='Aaron Patterson' pseudonym='tenderlove'>
97
+ # ]
98
+ # ruby.celebrities[0].name #=> 'Yukihiro Matsumoto'
99
+ # ruby.celebrities[0].pseudonym #=> 'Matz'
100
+ # ruby.celebrities[1].name #=> 'Aaron Patterson'
101
+ # ruby.celebrities[1].pseudonym #=> 'tenderlove'
102
+ def attribute(name, type = nil, &block)
103
+ attributes(name => build_type(name, type, &block))
104
+ end
105
+
106
+ # Adds an omittable (key is not required on initialization) attribute for this {Struct}
107
+ #
108
+ # @example
109
+ # class User < Dry::Struct
110
+ # attribute :name, Types::Strict::String
111
+ # attribute? :email, Types::Strict::String
112
+ # end
113
+ #
114
+ # User.new(name: 'John') # => #<User name="John">
115
+ #
116
+ # @param [Symbol] name name of the defined attribute
117
+ # @param [Dry::Types::Definition, nil] type or superclass of nested type
118
+ # @return [Dry::Struct]
119
+ #
120
+ def attribute?(*args, &block)
121
+ if args.size == 1 && block.nil?
122
+ Dry::Core::Deprecations.warn(
123
+ 'Dry::Struct.attribute? is deprecated for checking attribute presence, '\
124
+ 'use has_attribute? instead',
125
+ tag: :'dry-struct'
126
+ )
127
+
128
+ has_attribute?(args[0])
129
+ else
130
+ name, type = args
131
+
132
+ attribute(name, build_type(name, type, &block).meta(omittable: true))
133
+ end
45
134
  end
46
135
 
47
136
  # @param [Hash{Symbol => Dry::Types::Definition}] new_schema
@@ -50,7 +139,7 @@ module Dry
50
139
  # same name as previously defined one
51
140
  # @see #attribute
52
141
  # @example
53
- # class Book1 < Dry::Struct
142
+ # class Book < Dry::Struct
54
143
  # attributes(
55
144
  # title: Types::String,
56
145
  # author: Types::String
@@ -63,18 +152,60 @@ module Dry
63
152
  def attributes(new_schema)
64
153
  check_schema_duplication(new_schema)
65
154
 
66
- schema schema.merge(new_schema)
67
- input Types['coercible.hash'].public_send(constructor_type, schema)
155
+ input input.schema(new_schema)
68
156
 
69
157
  new_schema.each_key do |key|
70
- attr_reader(key) unless instance_methods.include?(key)
158
+ next if instance_methods.include?(key)
159
+ class_eval(<<-RUBY)
160
+ def #{ key }
161
+ @attributes[#{ key.inspect }]
162
+ end
163
+ RUBY
71
164
  end
72
165
 
73
- equalizer.instance_variable_get('@keys').concat(new_schema.keys)
166
+ @attribute_names = nil
167
+
168
+ descendants.
169
+ select { |d| d.superclass == self }.
170
+ each { |d| d.attributes(new_schema.reject { |k, _| d.schema.key?(k) }) }
74
171
 
75
172
  self
76
173
  end
77
174
 
175
+ # Add an arbitrary transformation for new attribute types.
176
+ #
177
+ # @param [#call,nil] proc
178
+ # @param [#call,nil] block
179
+ # @example
180
+ # class Book < Dry::Struct
181
+ # transform_types { |t| t.meta(struct: :Book) }
182
+ #
183
+ # attribute :title, Types::Strict::String
184
+ # end
185
+ #
186
+ # Book.schema[:title].meta # => { struct: :Book }
187
+ #
188
+ def transform_types(proc = nil, &block)
189
+ input input.with_type_transform(proc || block)
190
+ end
191
+
192
+ # Add an arbitrary transformation for input hash keys.
193
+ #
194
+ # @param [#call,nil] proc
195
+ # @param [#call,nil] block
196
+ # @example
197
+ # class Book < Dry::Struct
198
+ # transform_keys(&:to_sym)
199
+ #
200
+ # attribute :title, Types::Strict::String
201
+ # end
202
+ #
203
+ # Book.new('title' => "The Old Man and the Sea")
204
+ # # => #<Book title="The Old Man and the Sea">
205
+ def transform_keys(proc = nil, &block)
206
+ input input.with_key_transform(proc || block)
207
+ end
208
+
78
209
  # @param [Hash{Symbol => Dry::Types::Definition, Dry::Struct}] new_schema
79
210
  # @raise [RepeatedAttributeError] when trying to define attribute with the
80
211
  # same name as previously defined one
@@ -87,7 +218,6 @@ module Dry
87
218
 
88
219
  # @param [Hash{Symbol => Object},Dry::Struct] attributes
89
220
  # @raise [Struct::Error] if the given attributes don't conform {#schema}
90
- # with given {#constructor_type}
91
221
  def new(attributes = default_attributes)
92
222
  if attributes.instance_of?(self)
93
223
  attributes
@@ -110,34 +240,14 @@ module Dry
110
240
  alias_method :[], :call
111
241
 
112
242
  # @param [#call,nil] constructor
113
- # @param [Hash] options
243
+ # @param [Hash] _options
114
244
  # @param [#call,nil] block
115
245
  # @return [Dry::Struct::Constructor]
116
246
  def constructor(constructor = nil, **_options, &block)
117
247
  Struct::Constructor.new(self, fn: constructor || block)
118
248
  end
119
249
 
120
- # Retrieves default attributes from defined {.schema}.
121
- # Used in a {Struct} constructor if no attributes provided to {.new}
122
- #
123
- # @return [Hash{Symbol => Object}]
124
- def default_attributes
125
- check_invalid_schema_keys
126
- schema.each_with_object({}) { |(name, type), result|
127
- result[name] = type.evaluate if type.default?
128
- }
129
- end
130
-
131
- def check_invalid_schema_keys
132
- invalid_keys = schema.select { |name, type| type.instance_of?(String) }
133
- raise ArgumentError, argument_error_msg(invalid_keys.keys) if invalid_keys.any?
134
- end
135
-
136
- def argument_error_msg(keys)
137
- "Invaild argument for #{keys.join(', ')}"
138
- end
139
-
140
- # @param [Hash{Symbol => Object}] input
250
+ # @param [Hash{Symbol => Object},Dry::Struct] input
141
251
  # @yieldparam [Dry::Types::Result::Failure] failure
142
252
  # @yieldreturn [Dry::Types::ResultResult]
143
253
  # @return [Dry::Types::Result]
@@ -148,6 +258,17 @@ module Dry
148
258
  block_given? ? yield(failure) : failure
149
259
  end
150
260
 
261
+ # @param [Hash{Symbol => Object},Dry::Struct] input
262
+ # @return [Dry::Types::Result]
263
+ # @private
264
+ def try_struct(input)
265
+ if input.is_a?(self)
266
+ Types::Result::Success.new(input)
267
+ else
268
+ yield
269
+ end
270
+ end
271
+
151
272
  # @param [({Symbol => Object})] args
152
273
  # @return [Dry::Types::Result::Success]
153
274
  def success(*args)
@@ -157,7 +278,7 @@ module Dry
157
278
  # @param [({Symbol => Object})] args
158
279
  # @return [Dry::Types::Result::Failure]
159
280
  def failure(*args)
160
- result(Types::Result::Failure, *args)
281
+ result(::Dry::Types::Result::Failure, *args)
161
282
  end
162
283
 
163
284
  # @param [Class] klass
@@ -196,16 +317,94 @@ module Dry
196
317
  #
197
318
  # @param [Symbol] key Attribute name
198
319
  # @return [Boolean]
199
- def attribute?(key)
320
+ def has_attribute?(key)
200
321
  schema.key?(key)
201
322
  end
202
323
 
324
+ # @return [Hash{Symbol => Dry::Types::Definition, Dry::Struct}]
325
+ def schema
326
+ input.member_types
327
+ end
328
+
203
329
  # Gets the list of attribute names
204
330
  #
205
331
  # @return [Array<Symbol>]
206
332
  def attribute_names
207
333
  @attribute_names ||= schema.keys
208
334
  end
335
+
336
+ # @return [{Symbol => Object}]
337
+ def meta(meta = Undefined)
338
+ if meta.equal?(Undefined)
339
+ @meta
340
+ else
341
+ Class.new(self) do
342
+ @meta = @meta.merge(meta) unless meta.empty?
343
+ end
344
+ end
345
+ end
346
+
347
+ # Build a sum type
348
+ # @param [Dry::Types::Type] type
349
+ # @return [Dry::Types::Sum]
350
+ def |(type)
351
+ if type.is_a?(Class) && type <= Struct
352
+ Struct::Sum.new(self, type)
353
+ else
354
+ super
355
+ end
356
+ end
357
+
358
+ # Stores an object for building nested struct classes
359
+ # @return [StructBuilder]
360
+ def struct_builder
361
+ @struct_builder ||= StructBuilder.new(self).freeze
362
+ end
363
+ private :struct_builder
364
+
365
+ # Retrieves default attributes from defined {.schema}.
366
+ # Used in a {Struct} constructor if no attributes provided to {.new}
367
+ #
368
+ # @return [Hash{Symbol => Object}]
369
+ def default_attributes(default_schema = schema)
370
+ default_schema.each_with_object({}) do |(name, type), result|
371
+ result[name] = default_attributes(type.schema) if struct?(type)
372
+ end
373
+ end
374
+ private :default_attributes
375
+
376
+ # Checks if the given type is a Dry::Struct
377
+ #
378
+ # @param [Dry::Types::Definition, Dry::Struct] type
379
+ # @return [Boolean]
380
+ def struct?(type)
381
+ type.is_a?(Class) && type <= Struct
382
+ end
383
+ private :struct?
384
+
385
+ # Constructs a type
386
+ #
387
+ # @return [Dry::Types::Type, Dry::Struct]
388
+ def build_type(name, type, &block)
389
+ type_object =
390
+ if type.is_a?(String)
391
+ Dry::Types[type]
392
+ elsif block.nil? && type.nil?
393
+ raise(
394
+ ArgumentError,
395
+ 'you must supply a type or a block to `Dry::Struct.attribute`'
396
+ )
397
+ else
398
+ type
399
+ end
400
+
401
+ if block
402
+ struct_builder.(name, type_object, &block)
403
+ else
404
+ type_object
405
+ end
406
+ end
407
+ private :build_type
209
408
  end
210
409
  end
211
410
  end
@@ -11,5 +11,12 @@ module Dry
11
11
  super("Attribute :#{key} has already been defined")
12
12
  end
13
13
  end
14
+
15
+ # Raised when a struct doesn't have an attribute
16
+ class MissingAttributeError < KeyError
17
+ def initialize(key)
18
+ super("Missing attribute: #{ key.inspect }")
19
+ end
20
+ end
14
21
  end
15
22
  end
@@ -0,0 +1,20 @@
1
+ require 'pp'
2
+
3
+ module Dry
4
+ class Struct
5
+ def pretty_print(pp)
6
+ klass = self.class
7
+ pp.group(1, "#<#{ klass.name || klass.inspect }", '>') do
8
+ pp.seplist(@attributes.keys, proc { pp.text ',' }) do |column_name|
9
+ column_value = @attributes[column_name]
10
+ pp.breakable ' '
11
+ pp.group(1) do
12
+ pp.text column_name
13
+ pp.text '='
14
+ pp.pp column_value
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ Dry::Struct.register_extension(:pretty_print) do
2
+ require 'dry/struct/extensions/pretty_print'
3
+ end
@@ -7,9 +7,13 @@ module Dry
7
7
  # @return [Hash, Array]
8
8
  def self.[](value)
9
9
  if value.respond_to?(:to_hash)
10
- value.to_hash
11
- elsif value.respond_to?(:map)
12
- value.map { |item| self[item] }
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
15
+ elsif value.respond_to?(:to_ary)
16
+ value.to_ary.map { |item| self[item] }
13
17
  else
14
18
  value
15
19
  end
@@ -0,0 +1,86 @@
1
+ require 'dry/types/compiler'
2
+
3
+ module Dry
4
+ class Struct
5
+ # @private
6
+ class StructBuilder < Dry::Types::Compiler
7
+ attr_reader :struct
8
+
9
+ def initialize(struct)
10
+ super(Dry::Types)
11
+ @struct = struct
12
+ end
13
+
14
+ # @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
16
+ # @yield the body of the nested struct
17
+ def call(attr_name, type, &block)
18
+ const_name = const_name(type, attr_name)
19
+ check_name(const_name)
20
+
21
+ new_type = Class.new(parent(type), &block)
22
+ struct.const_set(const_name, new_type)
23
+
24
+ if array?(type)
25
+ type.of(new_type)
26
+ else
27
+ new_type
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def array?(type)
34
+ type.is_a?(Types::Type) && type.primitive.equal?(Array)
35
+ end
36
+
37
+ def parent(type)
38
+ if array?(type)
39
+ visit(type.to_ast)
40
+ else
41
+ type || default_superclass
42
+ end
43
+ end
44
+
45
+ def default_superclass
46
+ struct < Value ? Value : Struct
47
+ end
48
+
49
+ def const_name(type, attr_name)
50
+ snake_name = if array?(type)
51
+ Dry::Core::Inflector.singularize(attr_name)
52
+ else
53
+ attr_name
54
+ end
55
+
56
+ Dry::Core::Inflector.camelize(snake_name)
57
+ end
58
+
59
+ def check_name(name)
60
+ raise(
61
+ Struct::Error,
62
+ "Can't create nested attribute - `#{struct}::#{name}` already defined"
63
+ ) if struct.const_defined?(name, false)
64
+ end
65
+
66
+ def visit_constrained(node)
67
+ definition, * = node
68
+ visit(definition)
69
+ end
70
+
71
+ def visit_array(node)
72
+ member, * = node
73
+ member
74
+ end
75
+
76
+ def visit_definition(*)
77
+ default_superclass
78
+ end
79
+
80
+ def visit_constructor(node)
81
+ definition, * = node
82
+ visit(definition)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,42 @@
1
+ require 'dry/types/sum'
2
+
3
+ module Dry
4
+ class Struct
5
+ # A sum type of two or more structs
6
+ # As opposed to Dry::Types::Sum::Constrained
7
+ # this type tries no to coerce data first.
8
+ class Sum < Dry::Types::Sum::Constrained
9
+ # @param [Hash{Symbol => Object},Dry::Struct] input
10
+ # @yieldparam [Dry::Types::Result::Failure] failure
11
+ # @yieldreturn [Dry::Types::ResultResult]
12
+ # @return [Dry::Types::Result]
13
+ def try(input)
14
+ if input.is_a?(Struct)
15
+ try_struct(input) { super }
16
+ else
17
+ super
18
+ end
19
+ end
20
+
21
+ # Build a new sum type
22
+ # @param [Dry::Types::Type] type
23
+ # @return [Dry::Types::Sum]
24
+ def |(type)
25
+ if type.is_a?(Class) && type <= Struct || type.is_a?(Sum)
26
+ self.class.new(self, type)
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ protected
33
+
34
+ # @private
35
+ def try_struct(input)
36
+ left.try_struct(input) do
37
+ right.try_struct(input) { yield }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,5 +1,6 @@
1
1
  module Dry
2
2
  class Struct
3
- VERSION = '0.4.0'.freeze
3
+ # @private
4
+ VERSION = '0.6.0'.freeze
4
5
  end
5
6
  end
data/lib/dry/struct.rb CHANGED
@@ -1,12 +1,39 @@
1
- require 'dry/core/constants'
2
1
  require 'dry-types'
2
+ require 'dry-equalizer'
3
+ require 'dry/core/extensions'
4
+ require 'dry/core/constants'
3
5
 
4
6
  require 'dry/struct/version'
5
7
  require 'dry/struct/errors'
6
8
  require 'dry/struct/class_interface'
7
9
  require 'dry/struct/hashify'
10
+ require 'dry/struct/struct_builder'
8
11
 
9
12
  module Dry
13
+ # Constructor method for easily creating a {Dry::Struct}.
14
+ # @return [Dry::Struct]
15
+ # @example
16
+ # require 'dry-struct'
17
+ #
18
+ # module Types
19
+ # include Dry::Types.module
20
+ # end
21
+ #
22
+ # Person = Dry.Struct(name: Types::Strict::String, age: Types::Strict::Int)
23
+ # matz = Person.new(name: "Matz", age: 52)
24
+ # matz.name #=> "Matz"
25
+ # matz.age #=> 52
26
+ #
27
+ # Test = Dry.Struct(expected: Types::Strict::String) { input(input.strict) }
28
+ # Test[expected: "foo", unexpected: "bar"]
29
+ # #=> Dry::Struct::Error: [Test.new] unexpected keys [:unexpected] in Hash input
30
+ def self.Struct(attributes = Dry::Core::Constants::EMPTY_HASH, &block)
31
+ Class.new(Dry::Struct) do
32
+ attributes.each { |a, type| attribute a, type }
33
+ instance_eval(&block) if block
34
+ end
35
+ end
36
+
10
37
  # Typed {Struct} with virtus-like DSL for defining schema.
11
38
  #
12
39
  # ### Differences between dry-struct and virtus
@@ -18,14 +45,12 @@ module Dry
18
45
  # * Handling of attribute values is provided by standalone type objects from
19
46
  # [`dry-types`][].
20
47
  # * Handling of attribute hashes is provided by standalone hash schemas from
21
- # [`dry-types`][], which means there are different types of constructors in
22
- # {Struct} (see {Dry::Struct::ClassInterface#constructor_type})
48
+ # [`dry-types`][].
23
49
  # * Struct classes quack like [`dry-types`][], which means you can use them
24
50
  # in hash schemas, as array members or sum them
25
51
  #
26
52
  # {Struct} class can specify a constructor type, which uses [hash schemas][]
27
53
  # to handle attributes in `.new` method.
28
- # See {ClassInterface#new} for constructor types descriptions and examples.
29
54
  #
30
55
  # [`dry-types`]: https://github.com/dry-rb/dry-types
31
56
  # [Virtus]: https://github.com/solnic/virtus
@@ -57,130 +82,26 @@ module Dry
57
82
  # refactoring.title #=> 'Refactoring'
58
83
  # refactoring.subtitle #=> 'Improving the Design of Existing Code'
59
84
  class Struct
85
+ extend Dry::Core::Extensions
60
86
  include Dry::Core::Constants
61
87
  extend ClassInterface
62
88
 
63
- # {Dry::Types::Hash} subclass with specific behaviour defined for
64
- # @return [Dry::Types::Hash]
65
- # @see #constructor_type
66
- defines :input
67
- input Types['coercible.hash']
68
-
69
- # @return [Hash{Symbol => Dry::Types::Definition, Dry::Struct}]
70
- defines :schema
71
- schema EMPTY_HASH
89
+ include Dry::Equalizer(:__attributes__)
72
90
 
73
- CONSTRUCTOR_TYPE = Dry::Types['symbol'].enum(:permissive, :schema, :strict, :strict_with_defaults)
91
+ # {Dry::Types::Hash::Schema} subclass with specific behaviour defined for
92
+ # @return [Dry::Types::Hash::Schema]
93
+ defines :input
94
+ input Types['coercible.hash'].schema(EMPTY_HASH)
74
95
 
75
- # Sets or retrieves {#constructor} type as a symbol
76
- #
77
- # @note All examples below assume that you have defined {Struct} with
78
- # following attributes and explicitly call only {#constructor_type}:
79
- #
80
- # ```ruby
81
- # class User < Dry::Struct
82
- # attribute :name, Types::Strict::String.default('John Doe')
83
- # attribute :age, Types::Strict::Int
84
- # end
85
- # ```
86
- #
87
- # ### Common constructor types include:
88
- #
89
- # * `:permissive` - the default constructor type, useful for defining
90
- # {Struct}s that are instantiated using data from the database
91
- # (i.e. results of a database query), where you expect *all defined
92
- # attributes to be present* and it's OK to ignore other keys
93
- # (i.e. keys used for joining, that are not relevant from your domain
94
- # {Struct}s point of view). Default values **are not used** otherwise
95
- # you wouldn't notice missing data.
96
- # * `:schema` - missing keys will result in setting them using default
97
- # values, unexpected keys will be ignored.
98
- # * `:strict` - useful when you *do not expect keys other than the ones
99
- # you specified as attributes* in the input hash
100
- # * `:strict_with_defaults` - same as `:strict` but you are OK that some
101
- # values may be nil and you want defaults to be set
102
- #
103
- # To feel the difference between constructor types, look into examples.
104
- # Each of them provide the same attributes' definitions,
105
- # different constructor type, and 4 cases of given input:
106
- #
107
- # 1. Input omits a key for a value that does not have a default
108
- # 2. Input omits a key for a value that has a default
109
- # 3. Input contains nil for a value that specifies a default
110
- # 4. Input includes a key that was not specified in the schema
111
- #
112
- # @example `:permissive` constructor
113
- # class User < Dry::Struct
114
- # constructor_type :permissive
115
- # end
116
- #
117
- # User.new(name: "Jane")
118
- # #=> Dry::Struct::Error: [User.new] :age is missing in Hash input
119
- # User.new(age: 31)
120
- # #=> Dry::Struct::Error: [User.new] :name is missing in Hash input
121
- # User.new(name: nil, age: 31)
122
- # #=> #<User name="John Doe" age=31>
123
- # User.new(name: "Jane", age: 31, unexpected: "attribute")
124
- # #=> #<User name="Jane" age=31>
125
- #
126
- # @example `:schema` constructor
127
- # class User < Dry::Struct
128
- # constructor_type :schema
129
- # end
130
- #
131
- # User.new(name: "Jane") #=> #<User name="Jane" age=nil>
132
- # User.new(age: 31) #=> #<User name="John Doe" age=31>
133
- # User.new(name: nil, age: 31) #=> #<User name="John Doe" age=31>
134
- # User.new(name: "Jane", age: 31, unexpected: "attribute")
135
- # #=> #<User name="Jane" age=31>
136
- #
137
- # @example `:strict` constructor
138
- # class User < Dry::Struct
139
- # constructor_type :strict
140
- # end
141
- #
142
- # User.new(name: "Jane")
143
- # #=> Dry::Struct::Error: [User.new] :age is missing in Hash input
144
- # User.new(age: 31)
145
- # #=> Dry::Struct::Error: [User.new] :name is missing in Hash input
146
- # User.new(name: nil, age: 31)
147
- # #=> Dry::Struct::Error: [User.new] nil (NilClass) has invalid type for :name
148
- # User.new(name: "Jane", age: 31, unexpected: "attribute")
149
- # #=> Dry::Struct::Error: [User.new] unexpected keys [:unexpected] in Hash input
150
- #
151
- # @example `:strict_with_defaults` constructor
152
- # class User < Dry::Struct
153
- # constructor_type :strict_with_defaults
154
- # end
155
- #
156
- # User.new(name: "Jane")
157
- # #=> Dry::Struct::Error: [User.new] :age is missing in Hash input
158
- # User.new(age: 31)
159
- # #=> #<User name="John Doe" age=31>
160
- # User.new(name: nil, age: 31)
161
- # #=> Dry::Struct::Error: [User.new] nil (NilClass) has invalid type for :name
162
- # User.new(name: "Jane", age: 31, unexpected: "attribute")
163
- # #=> Dry::Struct::Error: [User.new] unexpected keys [:unexpected] in Hash input
164
- #
165
- # @see http://dry-rb.org/gems/dry-types/hash-schemas
166
- #
167
- # @overload constructor_type(type)
168
- # Sets the constructor type for {Struct}
169
- # @param [Symbol] type one of constructor types, see above
170
- # @return [Symbol]
171
- #
172
- # @overload constructor_type
173
- # Returns the constructor type for {Struct}
174
- # @return [Symbol] (:strict)
175
- defines :constructor_type, type: CONSTRUCTOR_TYPE
176
- constructor_type :permissive
96
+ @meta = EMPTY_HASH
177
97
 
178
- # @return [Dry::Equalizer]
179
- defines :equalizer
98
+ # @!attribute [Hash{Symbol => Object}] attributes
99
+ attr_reader :attributes
100
+ alias_method :__attributes__, :attributes
180
101
 
181
102
  # @param [Hash, #each] attributes
182
103
  def initialize(attributes)
183
- attributes.each { |key, value| instance_variable_set("@#{key}", value) }
104
+ @attributes = attributes
184
105
  end
185
106
 
186
107
  # Retrieves value of previously defined attribute by its' `name`
@@ -201,7 +122,7 @@ module Dry
201
122
  # rom_n_roda[:title] #=> 'Web Development with ROM and Roda'
202
123
  # rom_n_roda[:subtitle] #=> nil
203
124
  def [](name)
204
- public_send(name)
125
+ @attributes.fetch(name) { raise MissingAttributeError.new(name) }
205
126
  end
206
127
 
207
128
  # Converts the {Dry::Struct} to a hash with keys representing
@@ -223,7 +144,7 @@ module Dry
223
144
  # #=> {title: 'Web Development with ROM and Roda', subtitle: nil}
224
145
  def to_hash
225
146
  self.class.schema.keys.each_with_object({}) do |key, result|
226
- result[key] = Hashify[self[key]]
147
+ result[key] = Hashify[self[key]] if attributes.key?(key)
227
148
  end
228
149
  end
229
150
  alias_method :to_h, :to_hash
@@ -246,21 +167,21 @@ module Dry
246
167
  # )
247
168
  # #=> #<Book title="Web Development with ROM and Roda" subtitle="2nd edition">
248
169
  #
249
- # rom_n_roda.new(subtitle: '3nd edition')
250
- # #=> #<Book title="Web Development with ROM and Roda" subtitle="3nd edition">
170
+ # rom_n_roda.new(subtitle: '3rd edition')
171
+ # #=> #<Book title="Web Development with ROM and Roda" subtitle="3rd edition">
251
172
  def new(changeset)
252
173
  self.class[__attributes__.merge(changeset)]
253
174
  end
254
175
  alias_method :__new__, :new
255
176
 
256
- # @return[Hash{Symbol => Object}]
257
- # @api private
258
- def __attributes__
259
- self.class.attribute_names.each_with_object({}) do |key, h|
260
- h[key] = instance_variable_get(:"@#{ key }")
261
- end
177
+ # @return [String]
178
+ def inspect
179
+ klass = self.class
180
+ attrs = klass.attribute_names.map { |key| " #{key}=#{@attributes[key].inspect}" }.join
181
+ "#<#{ klass.name || klass.inspect }#{ attrs }>"
262
182
  end
263
183
  end
264
184
  end
265
185
 
266
186
  require 'dry/struct/value'
187
+ require 'dry/struct/extensions'
data/log/.gitkeep ADDED
File without changes
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-struct
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Solnica
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-11-04 00:00:00.000000000 Z
11
+ date: 2018-10-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-equalizer
@@ -30,20 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0.12'
34
- - - ">="
35
- - !ruby/object:Gem::Version
36
- version: 0.12.2
33
+ version: '0.13'
37
34
  type: :runtime
38
35
  prerelease: false
39
36
  version_requirements: !ruby/object:Gem::Requirement
40
37
  requirements:
41
38
  - - "~>"
42
39
  - !ruby/object:Gem::Version
43
- version: '0.12'
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: 0.12.2
40
+ version: '0.13'
47
41
  - !ruby/object:Gem::Dependency
48
42
  name: dry-core
49
43
  requirement: !ruby/object:Gem::Requirement
@@ -53,7 +47,7 @@ dependencies:
53
47
  version: '0.4'
54
48
  - - ">="
55
49
  - !ruby/object:Gem::Version
56
- version: 0.4.1
50
+ version: 0.4.3
57
51
  type: :runtime
58
52
  prerelease: false
59
53
  version_requirements: !ruby/object:Gem::Requirement
@@ -63,7 +57,7 @@ dependencies:
63
57
  version: '0.4'
64
58
  - - ">="
65
59
  - !ruby/object:Gem::Version
66
- version: 0.4.1
60
+ version: 0.4.3
67
61
  - !ruby/object:Gem::Dependency
68
62
  name: ice_nine
69
63
  requirement: !ruby/object:Gem::Requirement
@@ -161,14 +155,21 @@ files:
161
155
  - lib/dry/struct/class_interface.rb
162
156
  - lib/dry/struct/constructor.rb
163
157
  - lib/dry/struct/errors.rb
158
+ - lib/dry/struct/extensions.rb
159
+ - lib/dry/struct/extensions/pretty_print.rb
164
160
  - lib/dry/struct/hashify.rb
161
+ - lib/dry/struct/struct_builder.rb
162
+ - lib/dry/struct/sum.rb
165
163
  - lib/dry/struct/value.rb
166
164
  - lib/dry/struct/version.rb
165
+ - log/.gitkeep
167
166
  homepage: https://github.com/dry-rb/dry-struct
168
167
  licenses:
169
168
  - MIT
170
169
  metadata:
171
170
  allowed_push_host: https://rubygems.org
171
+ changelog_uri: https://github.com/dry-rb/dry-struct/blob/master/CHANGELOG.md
172
+ source_code_uri: https://github.com/dry-rb/dry-struct
172
173
  post_install_message:
173
174
  rdoc_options: []
174
175
  require_paths:
@@ -185,7 +186,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
185
186
  version: '0'
186
187
  requirements: []
187
188
  rubyforge_project:
188
- rubygems_version: 2.6.11
189
+ rubygems_version: 2.7.6
189
190
  signing_key:
190
191
  specification_version: 4
191
192
  summary: Typed structs and value objects.