rom-repository 0.3.1 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +11 -13
  3. data/CHANGELOG.md +25 -0
  4. data/Gemfile +13 -3
  5. data/lib/rom/repository.rb +57 -19
  6. data/lib/rom/repository/changeset.rb +89 -26
  7. data/lib/rom/repository/changeset/create.rb +34 -0
  8. data/lib/rom/repository/changeset/delete.rb +15 -0
  9. data/lib/rom/repository/changeset/pipe.rb +11 -4
  10. data/lib/rom/repository/changeset/update.rb +11 -1
  11. data/lib/rom/repository/command_compiler.rb +51 -30
  12. data/lib/rom/repository/command_proxy.rb +3 -1
  13. data/lib/rom/repository/header_builder.rb +3 -3
  14. data/lib/rom/repository/mapper_builder.rb +2 -2
  15. data/lib/rom/repository/relation_proxy.rb +26 -35
  16. data/lib/rom/repository/relation_proxy/combine.rb +59 -27
  17. data/lib/rom/repository/root.rb +4 -6
  18. data/lib/rom/repository/session.rb +55 -0
  19. data/lib/rom/repository/struct_builder.rb +29 -17
  20. data/lib/rom/repository/version.rb +1 -1
  21. data/lib/rom/struct.rb +11 -20
  22. data/rom-repository.gemspec +4 -3
  23. data/spec/integration/command_macros_spec.rb +5 -2
  24. data/spec/integration/command_spec.rb +0 -6
  25. data/spec/integration/multi_adapter_spec.rb +8 -5
  26. data/spec/integration/repository_spec.rb +58 -2
  27. data/spec/integration/root_repository_spec.rb +9 -2
  28. data/spec/integration/typed_structs_spec.rb +31 -0
  29. data/spec/shared/database.rb +5 -1
  30. data/spec/shared/relations.rb +3 -1
  31. data/spec/shared/repo.rb +13 -1
  32. data/spec/shared/structs.rb +39 -0
  33. data/spec/spec_helper.rb +7 -5
  34. data/spec/support/mutant.rb +10 -0
  35. data/spec/unit/changeset/map_spec.rb +42 -0
  36. data/spec/unit/changeset_spec.rb +32 -6
  37. data/spec/unit/relation_proxy_spec.rb +27 -9
  38. data/spec/unit/repository/changeset_spec.rb +125 -0
  39. data/spec/unit/repository/inspect_spec.rb +18 -0
  40. data/spec/unit/repository/session_spec.rb +251 -0
  41. data/spec/unit/session_spec.rb +54 -0
  42. data/spec/unit/struct_builder_spec.rb +45 -1
  43. metadata +41 -17
  44. data/lib/rom/repository/struct_attributes.rb +0 -46
  45. data/spec/unit/header_builder_spec.rb +0 -73
  46. data/spec/unit/plugins/view_spec.rb +0 -29
  47. data/spec/unit/sql/relation_spec.rb +0 -54
  48. data/spec/unit/struct_spec.rb +0 -22
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: db1338c97747a9adc2b9f00febe402d61ac1a6fd
4
- data.tar.gz: bd6ca3140fe897a0d6b9cccc3082e0fdaf63ed9a
3
+ metadata.gz: a5e3e3120dbe44e4f2f116cfe7aaaba6b2ef0a7e
4
+ data.tar.gz: 1e29da4f4c4c2c8c6cf7d9a70c794365419bc18d
5
5
  SHA512:
6
- metadata.gz: 04f9d30b32805886118b5eb36451aeba065cc510d1a92d091c1e99a41585eb821cf69b68fa56b1f049610b390d1403316a662dacfe4ba32b7f65ba6ac60b8e84
7
- data.tar.gz: 48a3b4136e064e00a562a584555765e8fb47a05388457b3cb31ab3b762dda4dd221299a1b621dfaf3705167412af644c8244c7ebbde22da285a8985f3afb712b
6
+ metadata.gz: b40d4d1a1c984998a0f58287cf586dd151251f9e9cabddcf22fff0a23e944f7113a1ffef5be76e6c44560d99d9c4d7bcecbb78f074f5079057ac71f7da0708a8
7
+ data.tar.gz: 8ec19211a0103e67f33578166bb781c1d42271d04e5561451e023aaccc4722d90b9e2fa3061cc1a021efca5c5f13fad00b105f9d72cfb66c1a4f86a679422c13
data/.travis.yml CHANGED
@@ -1,28 +1,26 @@
1
1
  language: ruby
2
- sudo: false
2
+ dist: trusty
3
+ sudo: required
3
4
  cache: bundler
4
5
  bundler_args: --without yard guard benchmarks tools
5
6
  before_script:
6
7
  - psql -c 'create database rom_repository' -U postgres
7
8
  script: "bundle exec rake ci"
9
+ after_success:
10
+ - '[ "${TRAVIS_JOB_NUMBER#*.}" = "1" ] && [ "$TRAVIS_BRANCH" = "master" ] && bundle exec codeclimate-test-reporter'
8
11
  rvm:
9
- - 2.1
10
- - 2.2
11
- - 2.3.1
12
- - rbx-2
13
- - jruby-9.0.5.0
14
- - ruby-head
12
+ - 2.4.0
13
+ - 2.3.3
14
+ - 2.2.6
15
+ - rbx-3
16
+ - jruby-9.1.6.0
15
17
  env:
16
18
  global:
17
- - CODECLIMATE_REPO_TOKEN=173b95dae5c6ac281bc36a4b212291a89fed9b520b39a86bafdf603692250e60
18
19
  - JRUBY_OPTS='--dev -J-Xmx1024M'
20
+ - COVERAGE='true'
19
21
  matrix:
20
22
  allow_failures:
21
- - rvm: ruby-head
22
- - rvm: jruby-head
23
- include:
24
- - rvm: jruby-head
25
- before_install: gem install bundler --no-ri --no-rdoc
23
+ - rvm: rbx-3
26
24
  notifications:
27
25
  webhooks:
28
26
  urls:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,28 @@
1
+ # v1.0.0 to-be-released
2
+
3
+ ### Added
4
+
5
+ * New `Repository#session` API for building transactions using changesets (solnic)
6
+ * Support for inferring typed structs based on relation schemas (solnic)
7
+ * `aggregate` and `combine` support nested association specs, ie `combine(users: [tasks: :tags])` (beauby)
8
+ * Changesets support data as arrays too (solnic)
9
+ * Changesets support custom command types via `Changeset#with(command_type: :my_command)` (solnic)
10
+ * `Changeset::Delete` was added and is accessible via `repo.changeset(delete: some_relation.by_pk(1))` (solnic)
11
+ * Ability to define custom changeset classes that can be instantiated via `repo.changeset(MyChangesetClass[:rel_name]).data(some_data)` where `MyChangesetClass` inherits from a core changeset class (solnic)
12
+ * `Changeset.map` which accepts a block and exposes a DSL for data transformations (all [transproc hash methods](https://github.com/solnic/transproc)) are available (solnic)
13
+ * `Changeset#associate` method that accepts another changeset or parent data and an association identifier, ie `post_changeset.associate(user, :author)` (solnic)
14
+
15
+ ### Changed
16
+
17
+ * `ROM::Struct` is now based on `Dry::Struct` (solnic)
18
+ * rom-support dependency was removed (flash-gordon)
19
+
20
+ ### Fixed
21
+
22
+ * Auto-mapping to structs ignores FK attributes consistently (solnic)
23
+
24
+ [Compare v0.3.1...v1.0.0](https://github.com/rom-rb/rom-repository/compare/v0.3.1...v1.0.0)
25
+
1
26
  # v0.3.1 2016-07-27
2
27
 
3
28
  Fixed gemspec so that we don't exclude all files with 'bin' in their name, geez (solnic)
data/Gemfile CHANGED
@@ -4,8 +4,11 @@ gemspec
4
4
 
5
5
  gem 'inflecto'
6
6
 
7
+ gem 'rom', git: 'https://github.com/rom-rb/rom.git', branch: 'master'
8
+ gem 'rom-mapper', git: 'https://github.com/rom-rb/rom-mapper.git', branch: 'master'
9
+
7
10
  group :development, :test do
8
- gem 'rom-sql', '~> 0.8'
11
+ gem 'rom-sql', git: 'https://github.com/rom-rb/rom-sql.git', branch: 'master'
9
12
  end
10
13
 
11
14
  group :development do
@@ -16,18 +19,25 @@ end
16
19
 
17
20
  group :test do
18
21
  gem 'rspec'
22
+ gem 'dry-struct'
19
23
  gem 'byebug', platforms: :mri
20
24
  gem 'pg', platforms: [:mri, :rbx]
21
25
  gem 'jdbc-postgres', platforms: :jruby
22
- gem 'codeclimate-test-reporter', require: nil
26
+
27
+ platform :mri do
28
+ gem 'codeclimate-test-reporter', require: false
29
+ gem 'simplecov'
30
+ end
23
31
  end
24
32
 
25
33
  group :benchmarks do
26
34
  gem 'hotch', platforms: :mri
27
35
  gem 'benchmark-ips'
28
- gem 'activerecord', '~> 4.2'
36
+ gem 'activerecord', '~> 5.0'
29
37
  end
30
38
 
31
39
  group :tools do
32
40
  gem 'pry'
41
+ gem 'mutant'
42
+ gem 'mutant-rspec'
33
43
  end
@@ -1,5 +1,4 @@
1
- require 'rom/support/deprecations'
2
- require 'rom/support/options'
1
+ require 'dry/core/deprecations'
3
2
 
4
3
  require 'rom/repository/class_interface'
5
4
  require 'rom/repository/mapper_builder'
@@ -8,6 +7,7 @@ require 'rom/repository/command_compiler'
8
7
 
9
8
  require 'rom/repository/root'
10
9
  require 'rom/repository/changeset'
10
+ require 'rom/repository/session'
11
11
 
12
12
  module ROM
13
13
  # Abstract repository class to inherit from
@@ -49,14 +49,6 @@ module ROM
49
49
  #
50
50
  # @api public
51
51
  class Repository
52
- # @deprecated
53
- class Base < Repository
54
- def self.inherited(klass)
55
- super
56
- Deprecations.announce(self, 'inherit from Repository instead')
57
- end
58
- end
59
-
60
52
  extend ClassInterface
61
53
 
62
54
  # @!attribute [r] container
@@ -71,6 +63,9 @@ module ROM
71
63
  # @return [MapperBuilder] The auto-generated mappers for repo relations
72
64
  attr_reader :mappers
73
65
 
66
+ # @api private
67
+ attr_reader :command_compiler
68
+
74
69
  # Initializes a new repo by establishing configured relation proxies from
75
70
  # the passed container
76
71
  #
@@ -93,6 +88,7 @@ module ROM
93
88
  relations[name] = proxy
94
89
  end
95
90
  end
91
+ @command_compiler = method(:command)
96
92
  end
97
93
 
98
94
  # @overload command(type, relation)
@@ -166,23 +162,60 @@ module ROM
166
162
  #
167
163
  # @api public
168
164
  def changeset(*args)
165
+ opts = { command_compiler: command_compiler }
166
+
169
167
  if args.size == 2
170
168
  name, data = args
171
169
  elsif args.size == 3
172
170
  name, pk, data = args
171
+ elsif args.size == 1
172
+ type = args[0]
173
+
174
+ if type.is_a?(Class) && type < Changeset
175
+ return type.new(relations[type.relation], opts)
176
+ else
177
+ type, relation = args[0].to_a[0]
178
+ end
173
179
  else
174
- raise ArgumentError, 'Repository#changeset accepts 2 or 3 arguments'
180
+ raise ArgumentError, 'Repository#changeset accepts 1-3 arguments'
175
181
  end
176
182
 
177
- relation = relations[name]
178
-
179
- if pk
180
- Changeset::Update.new(relation, data, primary_key: pk)
183
+ if type
184
+ if type.equal?(:delete)
185
+ Changeset::Delete.new(relation, opts)
186
+ end
181
187
  else
182
- Changeset::Create.new(relation, data)
188
+ relation = relations[name]
189
+
190
+ if pk
191
+ Changeset::Update.new(relation, opts.merge(__data__: data, primary_key: pk))
192
+ else
193
+ Changeset::Create.new(relation, opts.merge(__data__: data))
194
+ end
183
195
  end
184
196
  end
185
197
 
198
+ # TODO: document me, please
199
+ #
200
+ # @api public
201
+ def session(&block)
202
+ session = Session.new(self)
203
+ yield(session)
204
+ transaction { session.commit! }
205
+ end
206
+
207
+ # TODO: document me, please
208
+ #
209
+ # @api public
210
+ def transaction(&block)
211
+ container.gateways[:default].transaction(&block)
212
+ end
213
+
214
+ # @api public
215
+ def inspect
216
+ %(#<#{self.class} relations=[#{self.class.relations.map(&:inspect).join(' ')}]>)
217
+ end
218
+
186
219
  private
187
220
 
188
221
  # Local command cache
@@ -205,12 +238,17 @@ module ROM
205
238
 
206
239
  if mapper
207
240
  mapper_instance = container.mappers[relation.name.relation][mapper]
208
- else
241
+ elsif mapper.nil?
209
242
  mapper_instance = mappers[ast]
210
243
  end
211
244
 
212
- command = CommandCompiler[container, type, adapter, ast, use]
213
- command >> mapper_instance
245
+ command = CommandCompiler[container, type, adapter, ast, use, opts]
246
+
247
+ if mapper_instance
248
+ command >> mapper_instance
249
+ else
250
+ command.new(relation)
251
+ end
214
252
  end
215
253
 
216
254
  # @api private
@@ -1,11 +1,24 @@
1
- require 'rom/support/constants'
2
- require 'rom/support/options'
1
+ require 'dry/core/class_attributes'
2
+ require 'dry/core/cache'
3
3
 
4
+ require 'rom/initializer'
4
5
  require 'rom/repository/changeset/pipe'
5
6
 
6
7
  module ROM
7
8
  class Changeset
8
- include Options
9
+ extend Initializer
10
+ extend Dry::Core::Cache
11
+ extend Dry::Core::ClassAttributes
12
+
13
+ defines :relation
14
+
15
+ # @!attribute [r] relation
16
+ # @return [Relation] The changeset relation
17
+ param :relation
18
+
19
+ # @!attribute [r] __data__
20
+ # @return [Hash] The relation data
21
+ option :__data__, reader: true, optional: true, default: proc { nil }
9
22
 
10
23
  # @!attribute [r] pipe
11
24
  # @return [Changeset::Pipe] data transformation pipe
@@ -13,13 +26,33 @@ module ROM
13
26
  changeset.class.default_pipe
14
27
  }
15
28
 
16
- # @!attribute [r] relation
17
- # @return [Relation] The changeset relation
18
- attr_reader :relation
29
+ # @!attribute [r] command_compiler
30
+ # @return [Proc] a proc that can compile a command (typically provided by a repo)
31
+ option :command_compiler, reader: true, optional: true
19
32
 
20
- # @!attribute [r] data
21
- # @return [Hash] The relation data
22
- attr_reader :data
33
+ # @!attribute [r] command_type
34
+ # @return [Symbol] a custom command identifier
35
+ option :command_type, reader: true, optional: true, default: -> changeset { changeset.default_command_type }
36
+
37
+ # Create a changeset class preconfigured for a specific relation
38
+ #
39
+ # @example
40
+ # class NewUserChangeset < ROM::Changeset::Create[:users]
41
+ # end
42
+ #
43
+ # user_repo.changeset(NewUserChangeset).data(name: 'Jane')
44
+ #
45
+ # @api public
46
+ def self.[](relation_name)
47
+ fetch_or_store(relation_name) {
48
+ Class.new(self) { relation(relation_name) }
49
+ }
50
+ end
51
+
52
+ # @api public
53
+ def self.map(&block)
54
+ @pipe = Class.new(Pipe, &block).new
55
+ end
23
56
 
24
57
  # Build default pipe object
25
58
  #
@@ -27,14 +60,7 @@ module ROM
27
60
  #
28
61
  # @return [Pipe]
29
62
  def self.default_pipe
30
- Pipe.new
31
- end
32
-
33
- # @api private
34
- def initialize(relation, data, options = EMPTY_HASH)
35
- @relation = relation
36
- @data = data
37
- super
63
+ @pipe || Pipe.new
38
64
  end
39
65
 
40
66
  # Pipe changeset's data using custom steps define on the pipe
@@ -44,8 +70,12 @@ module ROM
44
70
  # @return [Changeset]
45
71
  #
46
72
  # @api public
47
- def map(*steps)
48
- with(pipe: steps.reduce(pipe) { |a, e| a >> pipe.class[e] })
73
+ def map(*steps, &block)
74
+ if block
75
+ __data__.map { |*args| yield(*args) }
76
+ else
77
+ with(pipe: steps.reduce(pipe) { |a, e| a >> pipe[e] })
78
+ end
49
79
  end
50
80
 
51
81
  # Coerce changeset to a hash
@@ -56,10 +86,22 @@ module ROM
56
86
  #
57
87
  # @api public
58
88
  def to_h
59
- pipe.call(data)
89
+ pipe.call(__data__)
60
90
  end
61
91
  alias_method :to_hash, :to_h
62
92
 
93
+ # Coerce changeset to an array
94
+ #
95
+ # This will send the data through the pipe
96
+ #
97
+ # @return [Array]
98
+ #
99
+ # @api public
100
+ def to_a
101
+ result == :one ? [to_h] : __data__.map { |element| pipe.call(element) }
102
+ end
103
+ alias_method :to_ary, :to_a
104
+
63
105
  # Return a new changeset with updated options
64
106
  #
65
107
  # @param [Hash] new_options The new options
@@ -68,23 +110,43 @@ module ROM
68
110
  #
69
111
  # @api private
70
112
  def with(new_options)
71
- self.class.new(relation, data, options.merge(new_options))
113
+ self.class.new(relation, options.merge(new_options))
114
+ end
115
+
116
+ # Return changeset with data
117
+ #
118
+ # @param [Hash] data
119
+ #
120
+ # @return [Changeset]
121
+ #
122
+ # @api public
123
+ def data(data)
124
+ with(__data__: data)
125
+ end
126
+
127
+ # Return command result type
128
+ #
129
+ # @return [Symbol]
130
+ #
131
+ # @api private
132
+ def result
133
+ __data__.is_a?(Hash) ? :one : :many
72
134
  end
73
135
 
74
136
  private
75
137
 
76
138
  # @api private
77
139
  def respond_to_missing?(meth, include_private = false)
78
- super || data.respond_to?(meth)
140
+ super || __data__.respond_to?(meth)
79
141
  end
80
142
 
81
143
  # @api private
82
144
  def method_missing(meth, *args, &block)
83
- if data.respond_to?(meth)
84
- response = data.__send__(meth, *args, &block)
145
+ if __data__.respond_to?(meth)
146
+ response = __data__.__send__(meth, *args, &block)
85
147
 
86
- if response.is_a?(Hash)
87
- self.class.new(relation, response, options)
148
+ if response.is_a?(__data__.class)
149
+ with(__data__: response)
88
150
  else
89
151
  response
90
152
  end
@@ -97,3 +159,4 @@ end
97
159
 
98
160
  require 'rom/repository/changeset/create'
99
161
  require 'rom/repository/changeset/update'
162
+ require 'rom/repository/changeset/delete'
@@ -4,6 +4,15 @@ module ROM
4
4
  #
5
5
  # @api public
6
6
  class Create < Changeset
7
+ # @!attribute [r] association
8
+ # @return [Array] Associated changesets with its association name
9
+ option :association, reader: true, optional: true
10
+
11
+ # @api public
12
+ def associate(other, assoc)
13
+ with(association: [other, assoc])
14
+ end
15
+
7
16
  # Return false
8
17
  #
9
18
  # @return [FalseClass]
@@ -21,6 +30,31 @@ module ROM
21
30
  def create?
22
31
  true
23
32
  end
33
+
34
+ # @api private
35
+ def command
36
+ if options[:association]
37
+ other, assoc = options[:association]
38
+
39
+ if other.is_a?(Changeset)
40
+ create_command.curry(self) >> other.command.with_association(assoc)
41
+ else
42
+ create_command.with_association(assoc).curry(self, other)
43
+ end
44
+ else
45
+ create_command.curry(self)
46
+ end
47
+ end
48
+
49
+ # @api private
50
+ def create_command
51
+ command_compiler.(command_type, relation, mapper: false, result: result)
52
+ end
53
+
54
+ # @api private
55
+ def default_command_type
56
+ :create
57
+ end
24
58
  end
25
59
  end
26
60
  end