rom-repository 0.3.1 → 1.0.0.beta1

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