rom-repository 1.3.2 → 1.3.3

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