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
@@ -0,0 +1,15 @@
1
+ module ROM
2
+ class Changeset
3
+ class Delete < Changeset
4
+ # @api private
5
+ def command
6
+ command_compiler.(command_type, relation, mapper: false)
7
+ end
8
+
9
+ # @api private
10
+ def default_command_type
11
+ :delete
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,10 +1,13 @@
1
1
  require 'transproc/registry'
2
+ require 'transproc/transformer'
2
3
 
3
4
  module ROM
4
5
  class Changeset
5
- class Pipe
6
+ class Pipe < Transproc::Transformer
6
7
  extend Transproc::Registry
7
8
 
9
+ import Transproc::HashTransformations
10
+
8
11
  attr_reader :processor
9
12
 
10
13
  def self.add_timestamps(data)
@@ -16,15 +19,19 @@ module ROM
16
19
  data.merge(updated_at: Time.now)
17
20
  end
18
21
 
19
- def initialize(processor = nil)
22
+ def initialize(processor = self.class.transproc)
20
23
  @processor = processor
21
24
  end
22
25
 
26
+ def [](name)
27
+ self.class[name]
28
+ end
29
+
23
30
  def >>(other)
24
31
  if processor
25
- self.class.new(processor >> other)
32
+ Pipe.new(processor >> other)
26
33
  else
27
- self.class.new(other)
34
+ Pipe.new(other)
28
35
  end
29
36
  end
30
37
 
@@ -71,12 +71,22 @@ module ROM
71
71
  def diff
72
72
  @diff ||=
73
73
  begin
74
- new_tuple = data.to_a
74
+ new_tuple = __data__.to_a
75
75
  ori_tuple = original.to_a
76
76
 
77
77
  Hash[new_tuple - (new_tuple & ori_tuple)]
78
78
  end
79
79
  end
80
+
81
+ # @api private
82
+ def command
83
+ command_compiler.(command_type, relation, mapper: false).curry(to_h) if diff?
84
+ end
85
+
86
+ # @api private
87
+ def default_command_type
88
+ :update
89
+ end
80
90
  end
81
91
  end
82
92
  end
@@ -1,4 +1,5 @@
1
- require 'concurrent/map'
1
+ require 'dry/core/inflector'
2
+ require 'dry/core/cache'
2
3
 
3
4
  require 'rom/commands'
4
5
  require 'rom/repository/command_proxy'
@@ -13,7 +14,7 @@ module ROM
13
14
  #
14
15
  # @api private
15
16
  class CommandCompiler
16
- SUPPORTED_TYPES = %i[create update delete].freeze
17
+ extend Dry::Core::Cache
17
18
 
18
19
  # Return a specific command type for a given adapter and relation AST
19
20
  #
@@ -35,38 +36,31 @@ module ROM
35
36
  #
36
37
  # @api private
37
38
  def self.[](*args)
38
- cache.fetch_or_store(args.hash) do
39
- container, type, adapter, ast, plugins = args
39
+ fetch_or_store(args.hash) do
40
+ container, type, adapter, ast, plugins, options = args
40
41
 
41
- unless SUPPORTED_TYPES.include?(type)
42
- raise ArgumentError, "#{type.inspect} is not a supported command type"
43
- end
44
-
45
- graph_opts = new(type, adapter, container, registry, plugins).visit(ast)
42
+ graph_opts = new(type, adapter, container, registry, plugins, options).visit(ast)
46
43
 
47
44
  command = ROM::Commands::Graph.build(registry, graph_opts)
48
45
 
49
46
  if command.graph?
50
47
  CommandProxy.new(command)
51
- else
48
+ elsif command.lazy?
52
49
  command.unwrap
50
+ else
51
+ command
53
52
  end
54
53
  end
55
54
  end
56
55
 
57
- # @api private
58
- def self.cache
59
- @__cache__ ||= Concurrent::Map.new
60
- end
61
-
62
56
  # @api private
63
57
  def self.registry
64
58
  @__registry__ ||= Hash.new { |h, k| h[k] = {} }
65
59
  end
66
60
 
67
- # @!attribute [r] type
68
- # @return [Symbol] The command type
69
- attr_reader :type
61
+ # @!attribute [r] id
62
+ # @return [Symbol] The command type registry identifier
63
+ attr_reader :id
70
64
 
71
65
  # @!attribute [r] adapter
72
66
  # @return [Symbol] The adapter identifier ie :sql or :http
@@ -84,12 +78,25 @@ module ROM
84
78
  # @return [Array<Symbol>] a list of optional plugins that will be enabled for commands
85
79
  attr_reader :plugins
86
80
 
81
+ # @!attribute [r] options
82
+ # @return [Hash] Additional command options
83
+ attr_reader :options
84
+
87
85
  # @api private
88
- def initialize(type, adapter, container, registry, plugins)
89
- @type = Commands.const_get(Inflector.classify(type))[adapter]
86
+ def initialize(id, adapter, container, registry, plugins, options)
87
+ @id = id
88
+ @adapter = adapter
90
89
  @registry = registry
91
90
  @container = container
92
91
  @plugins = Array(plugins)
92
+ @options = options
93
+ end
94
+
95
+ # @api private
96
+ def type
97
+ @_type ||= Commands.const_get(Dry::Core::Inflector.classify(id))[adapter]
98
+ rescue NameError
99
+ nil
93
100
  end
94
101
 
95
102
  # @api private
@@ -109,15 +116,20 @@ module ROM
109
116
  if meta[:combine_type] == :many
110
117
  name
111
118
  else
112
- { Inflector.singularize(name).to_sym => name }
119
+ { Dry::Core::Inflector.singularize(name).to_sym => name }
113
120
  end
114
121
 
115
- register_command(name, type, meta, parent_relation)
122
+ if type
123
+ register_command(name, type, meta, parent_relation)
116
124
 
117
- if other.size > 0
118
- [mapping, [type, other]]
125
+ if other.size > 0
126
+ [mapping, [type, other]]
127
+ else
128
+ [mapping, type]
129
+ end
119
130
  else
120
- [mapping, type]
131
+ registry[name][id] = container.commands[name][id].with(options)
132
+ [mapping, id]
121
133
  end
122
134
  end
123
135
 
@@ -149,7 +161,7 @@ module ROM
149
161
  relation = container.relations[rel_name]
150
162
 
151
163
  type.create_class(rel_name, type) do |klass|
152
- klass.result(meta.fetch(:combine_type, :one))
164
+ klass.result(meta.fetch(:combine_type, result))
153
165
 
154
166
  if meta[:combine_type]
155
167
  setup_associates(klass, relation, meta, parent_relation)
@@ -161,6 +173,15 @@ module ROM
161
173
  end
162
174
  end
163
175
 
176
+ # Return default result type
177
+ #
178
+ # @return [Symbol]
179
+ #
180
+ # @api private
181
+ def result
182
+ options.fetch(:result, :one)
183
+ end
184
+
164
185
  # Sets up `associates` plugin for a given command class and relation
165
186
  #
166
187
  # @param [Class] klass The command class
@@ -168,13 +189,11 @@ module ROM
168
189
  #
169
190
  # @api private
170
191
  def setup_associates(klass, relation, meta, parent_relation)
171
- klass.use(:associates)
172
-
173
192
  assoc_name =
174
193
  if relation.associations.key?(parent_relation)
175
194
  parent_relation
176
195
  else
177
- singular_name = Inflector.singularize(parent_relation).to_sym
196
+ singular_name = Dry::Core::Inflector.singularize(parent_relation).to_sym
178
197
  singular_name if relation.associations.key?(singular_name)
179
198
  end
180
199
 
@@ -207,7 +226,9 @@ module ROM
207
226
 
208
227
  klass.extend_for_relation(relation) if klass.restrictable
209
228
 
210
- plugins.each { |plugin| klass.use(plugin) }
229
+ plugins.each do |plugin|
230
+ klass.use(plugin)
231
+ end
211
232
  end
212
233
  end
213
234
  end
@@ -1,3 +1,5 @@
1
+ require 'dry/core/inflector'
2
+
1
3
  module ROM
2
4
  class Repository
3
5
  # TODO: look into making command graphs work without the root key in the input
@@ -7,7 +9,7 @@ module ROM
7
9
 
8
10
  def initialize(command)
9
11
  @command = command
10
- @root = Inflector.singularize(command.name.relation).to_sym
12
+ @root = Dry::Core::Inflector.singularize(command.name.relation).to_sym
11
13
  end
12
14
 
13
15
  def call(input)
@@ -49,11 +49,11 @@ module ROM
49
49
  node.map { |attribute| visit(attribute, meta) }
50
50
  end
51
51
 
52
- def visit_attribute(name, meta = {})
52
+ def visit_attribute(attr, meta = {})
53
53
  if meta[:wrap]
54
- [name, from: :"#{meta[:dataset]}_#{name}"]
54
+ [attr.name, from: attr.alias]
55
55
  else
56
- [name]
56
+ [attr.name]
57
57
  end
58
58
  end
59
59
  end
@@ -1,4 +1,4 @@
1
- require 'rom/support/cache'
1
+ require 'dry/core/cache'
2
2
  require 'rom/mapper'
3
3
  require 'rom/repository/header_builder'
4
4
 
@@ -6,7 +6,7 @@ module ROM
6
6
  class Repository
7
7
  # @api private
8
8
  class MapperBuilder
9
- extend Cache
9
+ extend Dry::Core::Cache
10
10
 
11
11
  attr_reader :header_builder
12
12
 
@@ -1,4 +1,4 @@
1
- require 'rom/support/options'
1
+ require 'rom/initializer'
2
2
  require 'rom/relation/materializable'
3
3
 
4
4
  require 'rom/repository/relation_proxy/combine'
@@ -14,30 +14,30 @@ module ROM
14
14
  #
15
15
  # @api public
16
16
  class RelationProxy
17
- include Options
17
+ extend Initializer
18
18
  include Relation::Materializable
19
19
 
20
20
  include RelationProxy::Combine
21
21
  include RelationProxy::Wrap
22
22
 
23
- option :name, type: Symbol
24
- option :mappers, reader: true, default: proc { MapperBuilder.new }
25
- option :meta, reader: true, type: Hash, default: EMPTY_HASH
26
- option :registry, type: RelationRegistry, default: proc { RelationRegistry.new }, reader: true
23
+ RelationRegistryType = Types.Definition(RelationRegistry).constrained(type: RelationRegistry)
27
24
 
28
25
  # @!attribute [r] relation
29
26
  # @return [Relation, Relation::Composite, Relation::Graph, Relation::Curried] The decorated relation object
30
- attr_reader :relation
27
+ param :relation
31
28
 
32
- # @!attribute [r] name
33
- # @return [ROM::Relation::Name] The relation name object
34
- attr_reader :name
29
+ option :name, type: Types::Strict::Symbol
30
+ option :mappers, reader: true, default: proc { MapperBuilder.new }
31
+ option :meta, reader: true, default: proc { EMPTY_HASH }
32
+ option :registry, type: RelationRegistryType, default: proc { RelationRegistry.new }, reader: true
35
33
 
36
- # @api private
37
- def initialize(relation, options = {})
38
- super
39
- @relation = relation
40
- @name = relation.name.with(options[:name])
34
+ # Relation name
35
+ #
36
+ # @return [ROM::Relation::Name]
37
+ #
38
+ # @api public
39
+ def name
40
+ @name == Dry::Initializer::UNDEFINED ? relation.name : relation.name.with(@name)
41
41
  end
42
42
 
43
43
  # Materializes wrapped relation and sends it through a mapper
@@ -105,15 +105,6 @@ module ROM
105
105
  relation.is_a?(Relation::Composite)
106
106
  end
107
107
 
108
- # Returns meta info for the wrapped relation
109
- #
110
- # @return [Hash]
111
- #
112
- # @api private
113
- def meta
114
- options[:meta]
115
- end
116
-
117
108
  # @return [Symbol] The wrapped relation's adapter identifier ie :sql or :http
118
109
  #
119
110
  # @api private
@@ -129,11 +120,9 @@ module ROM
129
120
  def to_ast
130
121
  @to_ast ||=
131
122
  begin
132
- attr_ast = (attributes - wraps_attributes).map { |name|
133
- [:attribute, name]
134
- }
123
+ attr_ast = schema.map { |attr| [:attribute, attr] }
135
124
 
136
- meta = options[:meta].merge(dataset: base_name.dataset)
125
+ meta = self.meta.merge(dataset: base_name.dataset)
137
126
  meta.delete(:wraps)
138
127
 
139
128
  header = attr_ast + nodes_ast + wraps_ast
@@ -147,19 +136,21 @@ module ROM
147
136
  relation.respond_to?(meth) || super
148
137
  end
149
138
 
139
+ # @api public
140
+ def inspect
141
+ %(#<#{relation.class} name=#{name} dataset=#{dataset.inspect}>)
142
+ end
143
+
150
144
  private
151
145
 
152
146
  # @api private
153
- def base_name
154
- relation.base_name
147
+ def schema
148
+ meta[:wrap] ? relation.schema.wrap.qualified : relation.schema.reject(&:wrapped?)
155
149
  end
156
150
 
157
151
  # @api private
158
- def wraps_attributes
159
- @wrap_attributes ||= wraps.flat_map { |wrap|
160
- prefix = wrap.base_name.dataset
161
- wrap.attributes.map { |name| :"#{prefix}_#{name}" }
162
- }
152
+ def base_name
153
+ relation.base_name
163
154
  end
164
155
 
165
156
  # @api private
@@ -1,3 +1,5 @@
1
+ require 'dry/core/inflector'
2
+
1
3
  module ROM
2
4
  class Repository
3
5
  class RelationProxy
@@ -56,12 +58,30 @@ module ROM
56
58
 
57
59
  combine_opts = Hash.new { |h, k| h[k] = {} }
58
60
 
59
- options.each do |(type, relations)|
60
- if relations
61
- combine_opts[type] = combine_opts_from_relations(relations)
61
+ options.each do |key, value|
62
+ if key == :one || key == :many
63
+ if value.is_a?(Hash)
64
+ value.each do |name, spec|
65
+ if spec.is_a?(Array)
66
+ combine_opts[key][name] = spec
67
+ else
68
+ _, (curried, keys) = combine_opts_from_relations(spec).to_a[0]
69
+ combine_opts[key][name] = [curried, keys]
70
+ end
71
+ end
72
+ else
73
+ _, (curried, keys) = combine_opts_from_relations(value).to_a[0]
74
+ combine_opts[key][curried.combine_tuple_key(key)] = [curried, keys]
75
+ end
62
76
  else
63
- result, curried, keys = combine_opts_for_assoc(type)
64
- combine_opts[result][type] = [curried, keys]
77
+ if value.is_a?(Array)
78
+ curried = combine_from_assoc(key, registry[key]).combine(*value)
79
+ result, _, keys = combine_opts_for_assoc(key)
80
+ combine_opts[result][key] = [curried, keys]
81
+ else
82
+ result, curried, keys = combine_opts_for_assoc(key, value)
83
+ combine_opts[result][key] = [curried, keys]
84
+ end
65
85
  end
66
86
  end
67
87
 
@@ -97,18 +117,21 @@ module ROM
97
117
  when Hash
98
118
  parents.each_with_object({}) { |(name, parent), r|
99
119
  keys = combine_keys(parent, relation, :parent)
100
- r[name] = [parent, keys]
120
+ curried = combine_from_assoc_with_fallback(name, parent, keys)
121
+ r[name] = [curried, keys]
101
122
  }
102
123
  when Array
103
124
  parents.each_with_object({}) { |parent, r|
104
- tuple_key = parent.combine_tuple_key(type)
105
125
  keys = combine_keys(parent, relation, :parent)
106
- r[tuple_key] = [parent, keys]
126
+ tuple_key = parent.combine_tuple_key(type)
127
+ curried = combine_from_assoc_with_fallback(parent.name, parent, keys)
128
+ r[tuple_key] = [curried, keys]
107
129
  }
108
130
  else
109
- tuple_key = parents.combine_tuple_key(type)
110
131
  keys = combine_keys(parents, relation, :parent)
111
- { tuple_key => [parents, keys] }
132
+ tuple_key = parents.combine_tuple_key(type)
133
+ curried = combine_from_assoc_with_fallback(parents.name, parents, keys)
134
+ { tuple_key => [curried, keys] }
112
135
  end
113
136
  end
114
137
 
@@ -138,18 +161,21 @@ module ROM
138
161
  when Hash
139
162
  children.each_with_object({}) { |(name, child), r|
140
163
  keys = combine_keys(relation, child, :children)
141
- r[name] = [child, keys]
164
+ curried = combine_from_assoc_with_fallback(name, child, keys)
165
+ r[name] = [curried, keys]
142
166
  }
143
167
  when Array
144
- parents.each_with_object({}) { |child, r|
145
- tuple_key = parent.combine_tuple_key(type)
168
+ children.each_with_object({}) { |child, r|
146
169
  keys = combine_keys(relation, child, :children)
147
- r[tuple_key] = [parent, keys]
170
+ tuple_key = child.combine_tuple_key(type)
171
+ curried = combine_from_assoc_with_fallback(child.name, child, keys)
172
+ r[tuple_key] = [curried, keys]
148
173
  }
149
174
  else
150
- tuple_key = children.combine_tuple_key(type)
151
175
  keys = combine_keys(relation, children, :children)
152
- { tuple_key => [children, keys] }
176
+ curried = combine_from_assoc_with_fallback(children.name, children, keys)
177
+ tuple_key = children.combine_tuple_key(type)
178
+ { tuple_key => [curried, keys] }
153
179
  end
154
180
  end
155
181
 
@@ -185,15 +211,19 @@ module ROM
185
211
  # and this mapping is used by `combine` to build a full relation graph
186
212
  #
187
213
  # @api private
188
- def combine_opts_from_relations(relations)
189
- relations.each_with_object({}) do |(name, (other, keys)), h|
190
- h[name] =
191
- if other.curried?
192
- [other, keys]
193
- else
194
- rel = combine_from_assoc(name, other) { other.combine_method(relation, keys) }
195
- [rel, keys]
196
- end
214
+ def combine_opts_from_relations(*relations)
215
+ relations.each_with_object({}) do |spec, h|
216
+ # We assume it's a child relation
217
+ keys = combine_keys(relation, spec, :children)
218
+ rel = combine_from_assoc_with_fallback(spec.name, spec, keys)
219
+ h[spec.name.relation] = [rel, keys]
220
+ end
221
+ end
222
+
223
+ # @api private
224
+ def combine_from_assoc_with_fallback(name, other, keys)
225
+ combine_from_assoc(name, other) do
226
+ other.combine_method(relation, keys)
197
227
  end
198
228
  end
199
229
 
@@ -205,6 +235,7 @@ module ROM
205
235
  #
206
236
  # @api private
207
237
  def combine_from_assoc(name, other, &fallback)
238
+ return other if other.curried?
208
239
  associations.try(name) { |assoc| other.for_combine(assoc) } or fallback.call
209
240
  end
210
241
 
@@ -214,9 +245,10 @@ module ROM
214
245
  # This is used when a flat list of association names was passed to `combine`
215
246
  #
216
247
  # @api private
217
- def combine_opts_for_assoc(name)
248
+ def combine_opts_for_assoc(name, opts = nil)
218
249
  assoc = relation.associations[name]
219
250
  curried = registry[assoc.target.relation].for_combine(assoc)
251
+ curried = curried.combine(opts) unless opts.nil?
220
252
  keys = assoc.combine_keys(__registry__)
221
253
  [assoc.result, curried, keys]
222
254
  end
@@ -258,7 +290,7 @@ module ROM
258
290
  # @api private
259
291
  def combine_tuple_key(result)
260
292
  if result == :one
261
- Inflector.singularize(base_name.relation).to_sym
293
+ Dry::Core::Inflector.singularize(base_name.relation).to_sym
262
294
  else
263
295
  base_name.relation
264
296
  end