rom-repository 1.3.2 → 1.3.3

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
2
  SHA1:
3
- metadata.gz: 3d274cac190d09e34254d67581d2cd8647e30301
4
- data.tar.gz: 39db9816c2762e1c47422df900b85d4d82b91556
3
+ metadata.gz: 2939fe9e61cbec70da58fe863b4c49384feabf57
4
+ data.tar.gz: 7cf8ca8cfc9acb006d2065bcdfcd6fcac6c4531e
5
5
  SHA512:
6
- metadata.gz: 55676bbb6a54903a047c3218334038039e8a26d3239ddec34011cef262450f6ed56a63bc0074c1094ebc5d547a845f8ffa20c0ad8ec9b4d5feed2c1eabcb7e09
7
- data.tar.gz: 9e04a30eecd17b3741f22c3efac94f7a643d73b68dd781bf15e80f37a00be65eb2c5cdb13214ad0b3016e0fe877df7478c6a1c958dd08af654d345ca27880e4e
6
+ metadata.gz: 8ee593244830ea931207b4acd726c9475543e6f8dd1bf65a3741c51f75bc1eee9fee974509fc4298005e2958500c38e2e313ed639b63f564de178cb4eb5bdb08
7
+ data.tar.gz: e3c05763d9d382a268ee5d29af29472826c86ac538b3b4364b80a36379ecb5d822aa5ced697fb836a111a212c700c24694c13f73f6acd0de6c04dcdd5dac145f
@@ -13,11 +13,7 @@ rvm:
13
13
  - 2.2.7
14
14
  - 2.3.4
15
15
  - 2.4.1
16
- - rbx-3
17
16
  - jruby-9.1.8.0
18
- matrix:
19
- allow_failures:
20
- - rvm: rbx-3
21
17
  env:
22
18
  global:
23
19
  - JRUBY_OPTS='--dev -J-Xmx1024M'
@@ -1,3 +1,20 @@
1
+ # v1.3.3 2017-05-31
2
+
3
+ ### Added
4
+
5
+ * `Changeset#extend` to exclude steps from the `#diff` output, this allows
6
+ to filter out timestamp changes prior to updates so that we can avoid
7
+ hitting the database in case of timestamp-only changes. You still can call `.map(:touch)`
8
+ if you want to have `updated_at` refreshed unconditionally (flash-gordon)
9
+
10
+ ## Fixed
11
+
12
+ * `aggregate` and `combine` works correctly with nested graph options where associations are aliased (solnic)
13
+ * Auto-mapping no longer creates intermediate struct objects for combined relations (which caused massive performance degradation in some cases) (solnic)
14
+ * Aliased associations no longer cause mapping to intermediate structs (solnic)
15
+
16
+ [Compare v1.3.2...v1.3.3](https://github.com/rom-rb/rom-repository/compare/v1.3.2...v1.3.3)
17
+
1
18
  # v1.3.2 2017-05-02
2
19
 
3
20
  ### Fixed
data/Gemfile CHANGED
@@ -11,8 +11,8 @@ group :development do
11
11
  end
12
12
 
13
13
  group :test do
14
- gem 'rom', git: 'https://github.com/rom-rb/rom.git', branch: 'master'
15
- gem 'rom-sql', git: 'https://github.com/rom-rb/rom-sql.git', branch: 'master'
14
+ gem 'rom', git: 'https://github.com/rom-rb/rom.git', branch: 'release-3.0'
15
+ gem 'rom-sql', git: 'https://github.com/rom-rb/rom-sql.git', branch: 'release-1.0'
16
16
  gem 'rspec'
17
17
  gem 'dry-struct'
18
18
  gem 'byebug', platforms: :mri
data/README.md CHANGED
@@ -1,25 +1 @@
1
- [gem]: https://rubygems.org/gems/rom-repository
2
- [travis]: https://travis-ci.org/rom-rb/rom-repository
3
- [gemnasium]: https://gemnasium.com/rom-rb/rom-repository
4
- [codeclimate]: https://codeclimate.com/github/rom-rb/rom-repository
5
- [inchpages]: http://inch-ci.org/github/rom-rb/rom-repository
6
-
7
- # rom-repository
8
-
9
- [![Gem Version](https://badge.fury.io/rb/rom-repository.svg)][gem]
10
- [![Build Status](https://travis-ci.org/rom-rb/rom-repository.svg?branch=master)][travis]
11
- [![Dependency Status](https://gemnasium.com/rom-rb/rom-repository.svg)][gemnasium]
12
- [![Code Climate](https://codeclimate.com/github/rom-rb/rom-repository/badges/gpa.svg)][codeclimate]
13
- [![Test Coverage](https://codeclimate.com/github/rom-rb/rom-repository/badges/coverage.svg)][codeclimate]
14
- [![Inline docs](http://inch-ci.org/github/rom-rb/rom-repository.svg?branch=master)][inchpages]
15
-
16
- Repositories for [rom-rb](https://github.com/rom-rb/rom) with auto-mapping, changesets and commands.
17
-
18
- Resources:
19
-
20
- * [User documentation](http://rom-rb.org/learn/repositories)
21
- * [API documentation](http://rubydoc.info/gems/rom-repository)
22
-
23
- ## License
24
-
25
- See `LICENSE` file.
1
+ # This project was moved to [rom-rb/rom](https://github.com/rom-rb/rom/tree/master/repository)
@@ -82,10 +82,20 @@ module ROM
82
82
 
83
83
  auto_struct true
84
84
 
85
+ # @!method self.auto_struct
86
+ # Get or set struct namespace
87
+ defines :struct_namespace
88
+
89
+ struct_namespace ROM::Struct
90
+
85
91
  # @!attribute [r] container
86
92
  # @return [ROM::Container] The container used to set up a repo
87
93
  param :container, allow: ROM::Container
88
94
 
95
+ # @!attribute [r] struct_namespace
96
+ # @return [Module,Class] The namespace for auto-generated structs
97
+ option :struct_namespace, default: -> { self.class.struct_namespace }
98
+
89
99
  # @!attribute [r] auto_struct
90
100
  # @return [Boolean] The container used to set up a repo
91
101
  option :auto_struct, default: -> { self.class.auto_struct }
@@ -111,7 +121,7 @@ module ROM
111
121
  def initialize(container, opts = EMPTY_HASH)
112
122
  super
113
123
 
114
- @mappers = MapperBuilder.new
124
+ @mappers = MapperBuilder.new(struct_namespace: struct_namespace)
115
125
 
116
126
  @relations = RelationRegistry.new do |registry, relations|
117
127
  self.class.relations.each do |name|
@@ -272,7 +282,7 @@ module ROM
272
282
  # end
273
283
  #
274
284
  # user
275
- # # => #<ROM::Struct[User] id=1 name="Jane">
285
+ # # => #<ROM::Struct::User id=1 name="Jane">
276
286
  #
277
287
  # @example with a rollback
278
288
  # user = transaction do |t|
@@ -25,11 +25,11 @@ module ROM
25
25
  #
26
26
  # @api private
27
27
  class Pipe < Transproc::Transformer[PipeRegistry]
28
- attr_reader :processor
28
+ extend Initializer
29
29
 
30
- def initialize(processor = self.class.transproc)
31
- @processor = processor
32
- end
30
+ param :processor, default: -> { self.class.transproc }
31
+ option :use_for_diff, optional: true, default: -> { true }
32
+ option :diff_processor, optional: true, default: -> { use_for_diff ? processor : nil }
33
33
 
34
34
  def self.[](name)
35
35
  container[name]
@@ -40,20 +40,22 @@ module ROM
40
40
  end
41
41
 
42
42
  def bind(context)
43
- if processor.is_a?(Proc)
44
- self.class.new(Pipe[-> *args { context.instance_exec(*args, &processor) }])
45
- else
46
- self
47
- end
43
+ return self unless processor.is_a?(Proc) || diff_processor.is_a?(Proc)
44
+
45
+ new(bind_processor(processor, context), diff_processor: bind_processor(diff_processor, context))
48
46
  end
49
47
 
50
- def >>(other)
51
- if processor
52
- Pipe.new(processor >> other)
48
+ def compose(other, use_for_diff: other.is_a?(Pipe) ? other.use_for_diff : false)
49
+ new_proc = processor ? processor >> other : other
50
+
51
+ if use_for_diff
52
+ diff_proc = diff_processor ? diff_processor >> other : other
53
+ new(new_proc, diff_processor: diff_proc)
53
54
  else
54
- Pipe.new(other)
55
+ new(new_proc)
55
56
  end
56
57
  end
58
+ alias_method :>>, :compose
57
59
 
58
60
  def call(data)
59
61
  if processor
@@ -62,6 +64,34 @@ module ROM
62
64
  data
63
65
  end
64
66
  end
67
+
68
+ def for_diff(data)
69
+ if diff_processor
70
+ diff_processor.call(data)
71
+ else
72
+ data
73
+ end
74
+ end
75
+
76
+ def with(opts)
77
+ if opts.empty?
78
+ self
79
+ else
80
+ Pipe.new(processor, options.merge(opts))
81
+ end
82
+ end
83
+
84
+ def new(processor, opts = EMPTY_HASH)
85
+ Pipe.new(processor, options.merge(opts))
86
+ end
87
+
88
+ def bind_processor(processor, context)
89
+ if processor.is_a?(Proc)
90
+ self[-> *args { context.instance_exec(*args, &processor) }]
91
+ else
92
+ processor
93
+ end
94
+ end
65
95
  end
66
96
  end
67
97
  end
@@ -55,11 +55,27 @@ module ROM
55
55
  # @see https://github.com/solnic/transproc Transproc
56
56
  #
57
57
  # @api public
58
- def self.map(&block)
58
+ def self.map(options = EMPTY_HASH, &block)
59
59
  if block.parameters.empty?
60
- pipes << Class.new(Pipe, &block).new
60
+ pipes << Class.new(Pipe, &block).new(options)
61
61
  else
62
- pipes << Pipe.new(block)
62
+ pipes << Pipe.new(block, options)
63
+ end
64
+ end
65
+
66
+ # Define a changeset mapping excluded from diffs
67
+ #
68
+ # @see Changeset::Stateful.map
69
+ # @see Changeset::Stateful#extend
70
+ #
71
+ # @return [Array<Pipe>, Transproc::Function>]
72
+ #
73
+ # @api public
74
+ def self.extend(*, &block)
75
+ if block
76
+ map(use_for_diff: false, &block)
77
+ else
78
+ super
63
79
  end
64
80
  end
65
81
 
@@ -104,7 +120,7 @@ module ROM
104
120
  # Apply mapping using built-in transformations and a custom block
105
121
  #
106
122
  # @example
107
- # changeset.map(:touch) { |tuple| tuple.merge(status: 'published') }
123
+ # changeset.map(:add_timestamps) { |tuple| tuple.merge(status: 'published') }
108
124
  #
109
125
  # @param [Array<Symbol>] steps A list of mapping steps
110
126
  #
@@ -112,14 +128,33 @@ module ROM
112
128
  #
113
129
  # @api public
114
130
  def map(*steps, &block)
131
+ extend(*steps, use_for_diff: true, &block)
132
+ end
133
+
134
+ # Pipe changeset's data using custom steps define on the pipe.
135
+ # You should use #map instead except updating timestamp fields.
136
+ # Calling changeset.extend builds a pipe that excludes certain
137
+ # steps for generating the diff. Currently the only place where
138
+ # it is used is update changesets with the `:touch` step, i.e.
139
+ # `changeset.extend(:touch).diff` will exclude `:updated_at`
140
+ # from the diff.
141
+ #
142
+ # @see Changeset::Stateful#map
143
+ #
144
+ # @return [Changeset]
145
+ #
146
+ # @api public
147
+ def extend(*steps, use_for_diff: false, **opts, &block)
148
+ options = { use_for_diff: use_for_diff, **opts }
149
+
115
150
  if block
116
151
  if steps.size > 0
117
- map(*steps).map(&block)
152
+ extend(*steps, options).extend(options, &block)
118
153
  else
119
- with(pipe: pipe >> Pipe.new(block).bind(self))
154
+ with(pipe: pipe.compose(Pipe.new(block).bind(self), options))
120
155
  end
121
156
  else
122
- with(pipe: steps.reduce(pipe) { |a, e| a >> pipe[e] })
157
+ with(pipe: steps.reduce(pipe.with(options)) { |a, e| a.compose(pipe[e], options) })
123
158
  end
124
159
  end
125
160
 
@@ -41,16 +41,6 @@ module ROM
41
41
  @original ||= Hash(relation.one)
42
42
  end
43
43
 
44
- # Return diff hash sent through the pipe
45
- #
46
- # @return [Hash]
47
- #
48
- # @api public
49
- def to_h
50
- pipe.call(diff)
51
- end
52
- alias_method :to_hash, :to_h
53
-
54
44
  # Return true if there's a diff between original and changeset data
55
45
  #
56
46
  # @return [TrueClass, FalseClass]
@@ -77,8 +67,9 @@ module ROM
77
67
  def diff
78
68
  @diff ||=
79
69
  begin
80
- data_tuple = __data__.to_a
81
- data_keys = __data__.keys & original.keys
70
+ data = pipe.for_diff(__data__)
71
+ data_tuple = data.to_a
72
+ data_keys = data.keys & original.keys
82
73
 
83
74
  new_tuple = data_tuple.to_a.select { |(k, _)| data_keys.include?(k) }
84
75
  ori_tuple = original.to_a.select { |(k, _)| data_keys.include?(k) }
@@ -7,8 +7,8 @@ module ROM
7
7
  class HeaderBuilder
8
8
  attr_reader :struct_builder
9
9
 
10
- def initialize(options = EMPTY_HASH)
11
- @struct_builder = StructBuilder.new
10
+ def initialize(struct_namespace: nil, **options)
11
+ @struct_builder = StructBuilder.new(struct_namespace)
12
12
  end
13
13
 
14
14
  def call(ast)
@@ -28,7 +28,11 @@ module ROM
28
28
  name = meta[:combine_name] || relation_name
29
29
 
30
30
  model = meta.fetch(:model) do
31
- struct_builder[meta.fetch(:dataset), header]
31
+ if meta[:combine_name]
32
+ false
33
+ else
34
+ struct_builder[name, header]
35
+ end
32
36
  end
33
37
 
34
38
  options = [visit(header), model: model]
@@ -10,8 +10,8 @@ module ROM
10
10
 
11
11
  attr_reader :header_builder
12
12
 
13
- def initialize
14
- @header_builder = HeaderBuilder.new
13
+ def initialize(options = EMPTY_HASH)
14
+ @header_builder = HeaderBuilder.new(options)
15
15
  end
16
16
 
17
17
  def call(ast)
@@ -17,6 +17,8 @@ module ROM
17
17
  extend Initializer
18
18
  include Relation::Materializable
19
19
 
20
+ (Kernel.private_instance_methods - %i(raise)).each(&method(:undef_method))
21
+
20
22
  include RelationProxy::Combine
21
23
  include RelationProxy::Wrap
22
24
 
@@ -73,7 +73,13 @@ module ROM
73
73
  end
74
74
  else
75
75
  if value.is_a?(Array)
76
- curried = combine_from_assoc(key, registry[key]).combine(*value)
76
+ other =
77
+ if registry.key?(key)
78
+ registry[key]
79
+ else
80
+ registry[associations[key].target]
81
+ end
82
+ curried = combine_from_assoc(key, other).combine(*value)
77
83
  result, _, keys = combine_opts_for_assoc(key)
78
84
  combine_opts[result][key] = [curried, keys]
79
85
  else
@@ -89,7 +95,7 @@ module ROM
89
95
  }
90
96
  end
91
97
 
92
- __new__(relation.combine(*nodes))
98
+ __new__(relation.graph(*nodes))
93
99
  end
94
100
 
95
101
  # Shortcut for combining with parents which infers the join keys
@@ -30,6 +30,12 @@ module ROM
30
30
  end
31
31
  alias_method :[], :call
32
32
 
33
+ attr_reader :namespace
34
+
35
+ def initialize(namespace = nil)
36
+ @namespace = namespace || ROM::Struct
37
+ end
38
+
33
39
  private
34
40
 
35
41
  def visit(ast)
@@ -66,11 +72,11 @@ module ROM
66
72
  end
67
73
 
68
74
  def build_class(name, parent, &block)
69
- Dry::Core::ClassBuilder.new(name: class_name(name), parent: parent).call(&block)
75
+ Dry::Core::ClassBuilder.new(name: class_name(name), parent: parent, namespace: namespace).call(&block)
70
76
  end
71
77
 
72
78
  def class_name(name)
73
- "ROM::Struct[#{Dry::Core::Inflector.classify(Dry::Core::Inflector.singularize(name))}]"
79
+ Dry::Core::Inflector.classify(Dry::Core::Inflector.singularize(name))
74
80
  end
75
81
  end
76
82
  end
@@ -1,5 +1,5 @@
1
1
  module ROM
2
2
  class Repository
3
- VERSION = '1.3.2'.freeze
3
+ VERSION = '1.3.3'.freeze
4
4
  end
5
5
  end
@@ -7,14 +7,21 @@ module ROM
7
7
  # They implement Hash protocol which means that they can be used
8
8
  # in places where Hash-like objects are supported.
9
9
  #
10
- # Repositories define subclasses of ROM::Struct automatically, they are not
11
- # defined as constants in any module, instead, generated mappers are configured
12
- # to use anonymous struct classes as models.
10
+ # Repositories define subclasses of ROM::Struct automatically, they are
11
+ # defined in the ROM::Struct namespace by default, but you set it up
12
+ # to use your namespace/module as well.
13
13
  #
14
14
  # Structs are based on dry-struct gem, they include `schema` with detailed information
15
15
  # about attribute types returned from relations, thus can be introspected to build
16
16
  # additional functionality when desired.
17
17
  #
18
+ # There is a caveat you should know about when working with structs. Struct classes
19
+ # have names but at the same time they're anonymous, i.e. you can't get the User struct class
20
+ # with ROM::Struct::User. ROM will create as many struct classes for User as needed,
21
+ # they all will have the same name and ROM::Struct::User will be the common parent class for
22
+ # them. Combined with the ability to provide your own namespace for structs this enables to
23
+ # pre-define the parent class.
24
+ #
18
25
  # @example accessing relation struct model
19
26
  # rom = ROM.container(:sql, 'sqlite::memory') do |conf|
20
27
  # conf.default.create_table(:users) do
@@ -30,7 +37,7 @@ module ROM
30
37
  #
31
38
  # # get auto-generated User struct
32
39
  # model = user_repo.users.mapper.model
33
- # # => ROM::Struct[User]
40
+ # # => ROM::Struct::User
34
41
  #
35
42
  # # see struct's schema attributes
36
43
  #
@@ -40,11 +47,31 @@ module ROM
40
47
  # model.schema[:name]
41
48
  # # => #<Dry::Types::Sum left=#<Dry::Types::Constrained type=#<Dry::Types::Definition primitive=NilClass options={}> options={:rule=>#<Dry::Logic::Rule::Predicate predicate=#<Method: Module(Dry::Logic::Predicates::Methods)#type?> options={:args=>[NilClass]}>} rule=#<Dry::Logic::Rule::Predicate predicate=#<Method: Module(Dry::Logic::Predicates::Methods)#type?> options={:args=>[NilClass]}>> right=#<Dry::Types::Definition primitive=String options={}> options={:meta=>{:name=>:name, :source=>ROM::Relation::Name(users)}}>
42
49
  #
50
+ # @example passing a namespace with an existing parent class
51
+ # module Entities
52
+ # class User < ROM::Struct
53
+ # def upcased_name
54
+ # name.upcase
55
+ # end
56
+ # end
57
+ # end
58
+ #
59
+ # class UserRepo < ROM::Repository[:users]
60
+ # struct_namespace Entities
61
+ # end
62
+ #
63
+ # user_repo = UserRepo.new(rom)
64
+ # user = user_repo.users.by_pk(1).one!
65
+ # user.name # => "Jane"
66
+ # user.upcased_name # => "JANE"
67
+ #
43
68
  # @see http://dry-rb.org/gems/dry-struct dry-struct
44
69
  # @see http://dry-rb.org/gems/dry-types dry-types
45
70
  #
46
71
  # @api public
47
72
  class Struct < Dry::Struct
73
+ MissingAttribute = Class.new(NameError)
74
+
48
75
  # Returns a short string representation
49
76
  #
50
77
  # @return [String]
@@ -65,15 +92,11 @@ module ROM
65
92
 
66
93
  private
67
94
 
68
- def method_missing(m, *args)
69
- inspected = inspect
70
- trace = caller
71
-
72
- # This is how MRI currently works
73
- # see func name_err_mesg_to_str in error.c
74
- name = inspected.size > 65 ? to_s : inspected
75
-
76
- raise NoMethodError.new("undefined method `#{ m }' for #{ name }", m, args).tap { |e| e.set_backtrace(trace) }
95
+ def method_missing(method, *)
96
+ super
97
+ rescue NameError => error
98
+ raise if method == :to_ary
99
+ raise MissingAttribute.new("#{ error.message } (not loaded attribute?)")
77
100
  end
78
101
  end
79
102
  end
@@ -13,9 +13,9 @@ Gem::Specification.new do |gem|
13
13
  gem.test_files = `git ls-files -- {spec}/*`.split("\n")
14
14
  gem.license = 'MIT'
15
15
 
16
- gem.add_runtime_dependency 'rom', '~> 3.2'
16
+ gem.add_runtime_dependency 'rom', '~> 3.2', '>= 3.2.3'
17
17
  gem.add_runtime_dependency 'rom-mapper', '~> 0.5'
18
- gem.add_runtime_dependency 'dry-core', '~> 0.2', '>= 0.2.1'
18
+ gem.add_runtime_dependency 'dry-core', '~> 0.3', '>= 0.3.1'
19
19
  gem.add_runtime_dependency 'dry-struct', '~> 0.1'
20
20
 
21
21
  gem.add_development_dependency 'rake', '~> 11.2'
@@ -88,7 +88,7 @@ RSpec.describe 'Using changesets' do
88
88
  it 'preprocesses data using built-in steps and custom block' do
89
89
  changeset = repo.
90
90
  changeset(:books, title: "rom-rb is awesome").
91
- map(:touch) { |tuple| tuple.merge(created_at: Time.now) }
91
+ extend(:touch) { |tuple| tuple.merge(created_at: Time.now) }
92
92
 
93
93
  command = repo.command(:create, repo.books)
94
94
  result = command.(changeset)
@@ -134,7 +134,7 @@ RSpec.describe 'Using changesets' do
134
134
 
135
135
  changeset = repo
136
136
  .changeset(book.id, title: 'rom-rb is awesome for real')
137
- .map(:touch)
137
+ .extend(:touch)
138
138
 
139
139
  expect(changeset.diff).to eql(title: 'rom-rb is awesome for real')
140
140
 
@@ -150,6 +150,7 @@ RSpec.describe 'Using changesets' do
150
150
 
151
151
  changeset = repo
152
152
  .changeset(book.id, title: 'rom-rb is awesome')
153
+ .extend(:touch)
153
154
 
154
155
  expect(changeset).to_not be_diff
155
156
 
@@ -159,5 +160,34 @@ RSpec.describe 'Using changesets' do
159
160
  expect(result.title).to eql('rom-rb is awesome')
160
161
  expect(result.updated_at).to be(nil)
161
162
  end
163
+
164
+ it 'works with mixed several class-level pipes' do
165
+ book = repo.create(title: 'rom-rb is awesome')
166
+
167
+ changeset_class = Class.new(ROM::Changeset::Update[:books]) do
168
+ map { |title: | { title: title.upcase } }
169
+ extend { |title: | { title: title.reverse } }
170
+ end
171
+
172
+ changeset = repo
173
+ .changeset(changeset_class)
174
+ .by_pk(book.id)
175
+ .data(title: 'rom-rb is really awesome')
176
+
177
+ expect(changeset.diff).to eql(title: 'ROM-RB IS REALLY AWESOME')
178
+ expect(changeset.to_h).to eql(title: 'EMOSEWA YLLAER SI BR-MOR')
179
+ end
180
+
181
+ it 'works with mixed several instance-level pipes' do
182
+ book = repo.create(title: 'rom-rb is awesome')
183
+
184
+ changeset = repo.
185
+ changeset(book.id, title: 'rom-rb is really awesome').
186
+ map { |title: | { title: title.upcase } }.
187
+ extend { |title: | { title: title.reverse } }
188
+
189
+ expect(changeset.diff).to eql(title: 'ROM-RB IS REALLY AWESOME')
190
+ expect(changeset.to_h).to eql(title: 'EMOSEWA YLLAER SI BR-MOR')
191
+ end
162
192
  end
163
193
  end
@@ -9,6 +9,15 @@ RSpec.describe ROM::Repository::Root, '#aggregate' do
9
9
  include_context 'relations'
10
10
  include_context 'seeds'
11
11
 
12
+ it 'loads a graph with aliased children and its parents' do
13
+ user = repo.aggregate(aliased_posts: :author).first
14
+
15
+ expect(user.aliased_posts.count).to be(1)
16
+ expect(user.aliased_posts[0].author.id).to be(user.id)
17
+ expect(user.aliased_posts[0].author.name).to eql(user.name)
18
+ end
19
+
20
+
12
21
  it 'exposes nodes via `node` method' do
13
22
  jane = repo.
14
23
  aggregate(:posts).
@@ -238,17 +238,45 @@ RSpec.describe 'ROM repository' do
238
238
  end
239
239
 
240
240
  describe 'projecting virtual attributes' do
241
- it 'loads auto-mapped structs' do
242
- user = repo.users.
243
- inner_join(:posts, author_id: :id).
244
- select_group { [id.qualified, name.qualified] }.
245
- select_append { int::count(:posts).as(:post_count) }.
246
- having { count(id.qualified) >= 1 }.
247
- first
248
-
249
- expect(user.id).to be(1)
250
- expect(user.name).to eql('Jane')
251
- expect(user.post_count).to be(1)
241
+ before do
242
+ ROM::Repository::StructBuilder.cache.clear
243
+ ROM::Repository::MapperBuilder.cache.clear
244
+ end
245
+
246
+ shared_context 'auto-mapping' do
247
+ it 'loads auto-mapped structs' do
248
+ user = repo.users.
249
+ inner_join(:posts, author_id: :id).
250
+ select_group { [id.qualified, name.qualified] }.
251
+ select_append { int::count(:posts).as(:post_count) }.
252
+ having { count(id.qualified) >= 1 }.
253
+ first
254
+
255
+ expect(user.id).to be(1)
256
+ expect(user.name).to eql('Jane')
257
+ expect(user.post_count).to be(1)
258
+ end
259
+ end
260
+
261
+ context 'with default namespace' do
262
+ include_context 'auto-mapping'
263
+ end
264
+
265
+ context 'with custom struct namespace' do
266
+ before do
267
+ repo_class.struct_namespace(Test)
268
+ end
269
+
270
+ include_context 'auto-mapping'
271
+
272
+ it 'uses custom namespace' do
273
+ expect(Test.const_defined?(:User)).to be(false)
274
+ user = repo.users.limit(1).one!
275
+
276
+ expect(user.name).to eql('Jane')
277
+ expect(user.class).to be < Test::User
278
+ expect(user.class.name).to eql(Test::User.name)
279
+ end
252
280
  end
253
281
  end
254
282
 
@@ -51,6 +51,7 @@ RSpec.shared_context 'relations' do
51
51
  schema(infer: true) do
52
52
  associations do
53
53
  belongs_to :user
54
+ belongs_to :users, as: :assignee
54
55
  end
55
56
  end
56
57
 
@@ -66,6 +66,14 @@ RSpec.describe ROM::Changeset do
66
66
 
67
67
  expect(changeset).to_not be_diff
68
68
  end
69
+
70
+ it 'uses piped data for diff' do
71
+ expect(relation).to receive(:one).and_return(jane)
72
+
73
+ changeset = ROM::Changeset::Update.new(relation).data(name: "Jane").map { |name: | { name: name.upcase } }
74
+
75
+ expect(changeset).to be_diff
76
+ end
69
77
  end
70
78
 
71
79
  describe '#clean?' do
@@ -191,5 +191,12 @@ RSpec.describe 'loading proxy' do
191
191
  it 'raises when method is missing' do
192
192
  expect { users_proxy.not_here }.to raise_error(NoMethodError, "undefined method `not_here' for ROM::Relation[Users]")
193
193
  end
194
+
195
+ it 'proxies Kernel methods when using with SimpleDelegator' do
196
+ proxy = Class.new(SimpleDelegator).new(users_proxy)
197
+
198
+ expect(users_proxy.select(:name)).to be_instance_of(ROM::Repository::RelationProxy)
199
+ expect(proxy.select(:name)).to be_instance_of(ROM::Repository::RelationProxy)
200
+ end
194
201
  end
195
202
  end
@@ -18,81 +18,111 @@ RSpec.describe 'struct builder', '#call' do
18
18
  [:attribute, attr_double(:name, :String)]]]]
19
19
  end
20
20
 
21
- before { builder[*input] }
21
+ context 'ROM::Struct' do
22
+ before { builder[*input] }
22
23
 
23
- it 'generates a struct for a given relation name and columns' do
24
- struct = builder.class.cache[input.hash]
24
+ it 'generates a struct for a given relation name and columns' do
25
+ struct = builder.class.cache[input.hash]
25
26
 
26
- user = struct.new(id: 1, name: 'Jane')
27
+ user = struct.new(id: 1, name: 'Jane')
27
28
 
28
- expect(user.id).to be(1)
29
- expect(user.name).to eql('Jane')
29
+ expect(user.id).to be(1)
30
+ expect(user.name).to eql('Jane')
30
31
 
31
- expect(user[:id]).to be(1)
32
- expect(user[:name]).to eql('Jane')
32
+ expect(user[:id]).to be(1)
33
+ expect(user[:name]).to eql('Jane')
33
34
 
34
- expect(Hash[user]).to eql(id: 1, name: 'Jane')
35
+ expect(Hash[user]).to eql(id: 1, name: 'Jane')
35
36
 
36
- expect(user.inspect).to eql('#<ROM::Struct[User] id=1 name="Jane">')
37
- expect(user.to_s).to match(/\A#<ROM::Struct\[User\]:0x[0-9a-f]+>\z/)
38
- end
37
+ expect(user.inspect).to eql('#<ROM::Struct::User id=1 name="Jane">')
38
+ expect(user.to_s).to match(/\A#<ROM::Struct::User:0x[0-9a-f]+>\z/)
39
+ end
39
40
 
40
- it 'stores struct in the cache' do
41
- expect(builder.class.cache[input.hash]).to be(builder[*input])
42
- end
41
+ it 'stores struct in the cache' do
42
+ expect(builder.class.cache[input.hash]).to be(builder[*input])
43
+ end
43
44
 
44
- context 'with reserved keywords as attribute names' do
45
- let(:input) do
46
- [:users, [:header, [
47
- [:attribute, attr_double(:id, :Int)],
48
- [:attribute, attr_double(:name, :String)],
49
- [:attribute, attr_double(:alias, :String)],
50
- [:attribute, attr_double(:until, :Time)]]]]
45
+ context 'with reserved keywords as attribute names' do
46
+ let(:input) do
47
+ [:users, [:header, [
48
+ [:attribute, attr_double(:id, :Int)],
49
+ [:attribute, attr_double(:name, :String)],
50
+ [:attribute, attr_double(:alias, :String)],
51
+ [:attribute, attr_double(:until, :Time)]]]]
52
+ end
53
+
54
+ it 'allows to build a struct class without complaining' do
55
+ struct = builder.class.cache[input.hash]
56
+
57
+ user = struct.new(id: 1, name: 'Jane', alias: 'JD', until: Time.new(2030))
58
+
59
+ expect(user.id).to be(1)
60
+ expect(user.name).to eql('Jane')
61
+ expect(user.alias).to eql('JD')
62
+ expect(user.until).to eql(Time.new(2030))
63
+ end
51
64
  end
52
65
 
53
- it 'allows to build a struct class without complaining' do
66
+ it 'raise a friendly error on missing keys' do
54
67
  struct = builder.class.cache[input.hash]
55
68
 
56
- user = struct.new(id: 1, name: 'Jane', alias: 'JD', until: Time.new(2030))
57
-
58
- expect(user.id).to be(1)
59
- expect(user.name).to eql('Jane')
60
- expect(user.alias).to eql('JD')
61
- expect(user.until).to eql(Time.new(2030))
69
+ expect { struct.new(id: 1) }.to raise_error(
70
+ Dry::Struct::Error, /:name is missing/
71
+ )
62
72
  end
63
73
  end
64
74
 
65
- it 'raise a friendly error on missing keys' do
66
- struct = builder.class.cache[input.hash]
75
+ context 'custom entity container' do
76
+ before do
77
+ module Test
78
+ module Custom
79
+ end
80
+ end
81
+ end
67
82
 
68
- expect { struct.new(id: 1) }.to raise_error(
69
- Dry::Struct::Error, /:name is missing/
70
- )
71
- end
83
+ let(:struct) { builder[*input] }
84
+ subject(:builder) { ROM::Repository::StructBuilder.new(Test::Custom) }
72
85
 
73
- context 'name errors' do
74
- let(:struct) { builder.class.cache[input.hash] }
86
+ it 'generates a struct class inside a given module' do
87
+ expect(struct.name).to eql('Test::Custom::User')
88
+ user = struct.new(id: 1, name: 'Jane')
75
89
 
76
- context 'missing method on instance' do
77
- it 'uses inspect and class name for small structs' do
78
- user = struct.new(id: 1, name: 'Jane')
90
+ expect(user.inspect).to eql(%q{#<Test::Custom::User id=1 name="Jane">})
91
+ end
79
92
 
80
- expect { user.missing }.
81
- to raise_error(
82
- NoMethodError,
83
- %r{undefined method `missing' for #<ROM::Struct\[User\] id=1 name="Jane">}
84
- )
93
+ it 'uses the existing class as a parent' do
94
+ class Test::Custom::User < ROM::Struct
95
+ def upcased_name
96
+ name.upcase
97
+ end
85
98
  end
86
99
 
87
- it 'uses class name in name errors' do
88
- user = struct.new(id: 1, name: 'J' * 50)
100
+ user = struct.new(id: 1, name: 'Jane')
89
101
 
90
- expect { user.missing }.
91
- to raise_error(
92
- NoMethodError,
93
- %r{undefined method `missing' for #<ROM::Struct\[User\]:0x\h+>}
94
- )
102
+ expect(user.upcased_name).to eql('JANE')
103
+ end
104
+
105
+ it 'raises a nice error on missing attributes' do
106
+ class Test::Custom::User < ROM::Struct
107
+ def upcased_middle_name
108
+ middle_name.upcase
109
+ end
95
110
  end
111
+
112
+ user = struct.new(id: 1, name: 'Jane')
113
+
114
+ expect {
115
+ user.upcased_middle_name
116
+ }.to raise_error(
117
+ ROM::Struct::MissingAttribute,
118
+ /not loaded attribute\?/
119
+ )
120
+ end
121
+
122
+ it 'works with implicit coercions' do
123
+ user = struct.new(id: 1, name: 'Jane')
124
+
125
+ expect([user].flatten).to eql([user])
96
126
  end
97
127
  end
98
128
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rom-repository
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Solnica
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-02 00:00:00.000000000 Z
11
+ date: 2017-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rom
@@ -17,6 +17,9 @@ dependencies:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
19
  version: '3.2'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 3.2.3
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -24,6 +27,9 @@ dependencies:
24
27
  - - "~>"
25
28
  - !ruby/object:Gem::Version
26
29
  version: '3.2'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 3.2.3
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: rom-mapper
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -44,20 +50,20 @@ dependencies:
44
50
  requirements:
45
51
  - - "~>"
46
52
  - !ruby/object:Gem::Version
47
- version: '0.2'
53
+ version: '0.3'
48
54
  - - ">="
49
55
  - !ruby/object:Gem::Version
50
- version: 0.2.1
56
+ version: 0.3.1
51
57
  type: :runtime
52
58
  prerelease: false
53
59
  version_requirements: !ruby/object:Gem::Requirement
54
60
  requirements:
55
61
  - - "~>"
56
62
  - !ruby/object:Gem::Version
57
- version: '0.2'
63
+ version: '0.3'
58
64
  - - ">="
59
65
  - !ruby/object:Gem::Version
60
- version: 0.2.1
66
+ version: 0.3.1
61
67
  - !ruby/object:Gem::Dependency
62
68
  name: dry-struct
63
69
  requirement: !ruby/object:Gem::Requirement