dry-struct 0.4.0 → 0.6.0

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