rom-core 4.0.0.beta2 → 4.0.0.beta3

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: 9ea3d114fd7800efddd98a8efe19a01586625c9f
4
- data.tar.gz: 59a3cc776cfd019cd54583caf4ef8ee759d90f09
3
+ metadata.gz: c8a152a49542a83c5195f02e61f08823c7207f23
4
+ data.tar.gz: d4513f31be747f8f01e7c581084782afc3662a6a
5
5
  SHA512:
6
- metadata.gz: 4349071ed68f65de26faf701b930653f56be802e14f63b82510d04c091ad0a7d0687eb23bb0e6a22b6330d6f83ec32184811701d29cd41934140d10c00ec54e6
7
- data.tar.gz: 31885902cc6c78cf78c2f1357ee76edcf082eea550deaa293588029534a68e09d01c6a31ef14bd7e09523ad7678344184b2e3eec8f384a767ff11c94f5948f1f
6
+ metadata.gz: 652ebe10f678636a8dfe0dd78e1f31fbba58499a9e65eaf1589e46c3abc87e730ee773fdf60435146725e73ca7596e519bc3888dd5498a3d81a90783740a7b40
7
+ data.tar.gz: 95f2e25a3eac0195a6d9d35697b619b52364255b7fefcbfd1c68a647f9aefecc21a489f9a945346f6834d2936d81235b447451c9131d2b86be40a5c834144e00
data/CHANGELOG.md CHANGED
@@ -24,7 +24,7 @@ Previous `rom` gem was renamed to `rom-core`
24
24
  * Works with MRI >= 2.2
25
25
  * [BREAKING] Inferring relations from database schema **has been removed**. You need to define relations explicitly now (solnic)
26
26
  * [BREAKING] Relations have `auto_map` **turned on by default**. This means that wraps and graphs return nested data structures automatically (solnic)
27
- * [BREAKING] `Relation#combine` behavior from previous versions is now provided by `Relation#graph` (solnic)
27
+ * [BREAKING] `Relation#combine` behavior from previous versions is now provided by `Relation#combine_with` (solnic)
28
28
  * [BREAKING] `Relation#as` now returns a new relation with aliased name, use `Relation#map_with(*list-of-mapper-ids)` or `Relation#map_to(model)` if you just want to map to custom models (solnic)
29
29
  * [BREAKING] `Relation.register_as(:bar)` is removed in favor of `schema(:foo, as: :bar)` (solnic)
30
30
  * [BREAKING] `Relation.dataset(:foo)` is removed in favor of `schema(:foo)`. Passing a block still works like before (solnic)
@@ -38,6 +38,7 @@ Previous `rom` gem was renamed to `rom-core`
38
38
 
39
39
  * [BREAKING] `Relation::Curried#name` was renamed to `Relation::Curried#view` (solnic)
40
40
  * [BREAKING] `Association::Name` was removed in favor of using `Relation::Name` (solnic)
41
+ * [BREAKING] `ROM::Schema::Attribute` was renamed to `ROM::Attribute` (solnic)
41
42
  * Relations no longer use `method_missing` for accessing other relations from the registry (solnic)
42
43
 
43
44
  ## Fixed
@@ -0,0 +1,427 @@
1
+ require 'dry/equalizer'
2
+
3
+ require 'rom/initializer'
4
+ require 'rom/support/memoizable'
5
+
6
+ module ROM
7
+ # Schema attributes provide meta information about types and an API
8
+ # for additional operations. This class can be extended by adapters to provide
9
+ # database-specific features. In example rom-sql provides SQL::Attribute
10
+ # with more features like creating SQL expressions for queries.
11
+ #
12
+ # Schema attributes are accessible through canonical relation schemas and
13
+ # instance-level schemas.
14
+ #
15
+ # @api public
16
+ class Attribute
17
+ include Dry::Equalizer(:type, :options)
18
+ include Memoizable
19
+
20
+ extend Initializer
21
+
22
+ # @!attribute [r] type
23
+ # @return [Dry::Types::Definition, Dry::Types::Sum, Dry::Types::Constrained]
24
+ param :type
25
+
26
+ # @api private
27
+ def [](input)
28
+ type[input]
29
+ end
30
+
31
+ # Return true if this attribute type is a primary key
32
+ #
33
+ # @example
34
+ # class Users < ROM::Relation[:memory]
35
+ # schema do
36
+ # attribute :id, Types::Int
37
+ # attribute :name, Types::String
38
+ #
39
+ # primary_key :id
40
+ # end
41
+ # end
42
+ #
43
+ # Users.schema[:id].primary_key?
44
+ # # => true
45
+ #
46
+ # Users.schema[:name].primary_key?
47
+ # # => false
48
+ #
49
+ # @return [TrueClass,FalseClass]
50
+ #
51
+ # @api public
52
+ def primary_key?
53
+ meta[:primary_key].equal?(true)
54
+ end
55
+
56
+ # Return true if this attribute type is a foreign key
57
+ #
58
+ # @example
59
+ # class Tasks < ROM::Relation[:memory]
60
+ # schema do
61
+ # attribute :id, Types::Int
62
+ # attribute :user_id, Types.ForeignKey(:users)
63
+ # end
64
+ # end
65
+ #
66
+ # Users.schema[:user_id].foreign_key?
67
+ # # => true
68
+ #
69
+ # Users.schema[:id].foreign_key?
70
+ # # => false
71
+ #
72
+ # @return [TrueClass,FalseClass]
73
+ #
74
+ # @api public
75
+ def foreign_key?
76
+ meta[:foreign_key].equal?(true)
77
+ end
78
+
79
+ # Return true if this attribute type is a foreign key
80
+ #
81
+ # @example
82
+ # class Tasks < ROM::Relation[:memory]
83
+ # schema do
84
+ # attribute :user_id, Types::Int.meta(alias: :id)
85
+ # attribute :name, Types::String
86
+ # end
87
+ # end
88
+ #
89
+ # Users.schema[:user_id].aliased?
90
+ # # => true
91
+ #
92
+ # Users.schema[:name].aliased?
93
+ # # => false
94
+ #
95
+ # @return [TrueClass,FalseClass]
96
+ #
97
+ # @api public
98
+ def aliased?
99
+ !meta[:alias].nil?
100
+ end
101
+
102
+ # Return source relation of this attribute type
103
+ #
104
+ # @example
105
+ # class Tasks < ROM::Relation[:memory]
106
+ # schema do
107
+ # attribute :id, Types::Int
108
+ # attribute :user_id, Types.ForeignKey(:users)
109
+ # end
110
+ # end
111
+ #
112
+ # Users.schema[:id].source
113
+ # # => :tasks
114
+ #
115
+ # Users.schema[:user_id].source
116
+ # # => :tasks
117
+ #
118
+ # @return [Symbol, Relation::Name]
119
+ #
120
+ # @api public
121
+ def source
122
+ meta[:source]
123
+ end
124
+
125
+ # Return target relation of this attribute type
126
+ #
127
+ # @example
128
+ # class Tasks < ROM::Relation[:memory]
129
+ # schema do
130
+ # attribute :id, Types::Int
131
+ # attribute :user_id, Types.ForeignKey(:users)
132
+ # end
133
+ # end
134
+ #
135
+ # Users.schema[:id].target
136
+ # # => nil
137
+ #
138
+ # Users.schema[:user_id].target
139
+ # # => :users
140
+ #
141
+ # @return [NilClass, Symbol, Relation::Name]
142
+ #
143
+ # @api public
144
+ def target
145
+ meta[:target]
146
+ end
147
+
148
+ # Return the canonical name of this attribute name
149
+ #
150
+ # This *always* returns the name that is used in the datastore, even when
151
+ # an attribute is aliased
152
+ #
153
+ # @example
154
+ # class Tasks < ROM::Relation[:memory]
155
+ # schema do
156
+ # attribute :user_id, Types::Int.meta(alias: :id)
157
+ # attribute :name, Types::String
158
+ # end
159
+ # end
160
+ #
161
+ # Users.schema[:id].name
162
+ # # => :id
163
+ #
164
+ # Users.schema[:user_id].name
165
+ # # => :user_id
166
+ #
167
+ # @return [Symbol]
168
+ #
169
+ # @api public
170
+ def name
171
+ meta[:name]
172
+ end
173
+
174
+ # Return tuple key
175
+ #
176
+ # When schemas are projected with aliased attributes, we need a simple access to tuple keys
177
+ #
178
+ # @example
179
+ # class Tasks < ROM::Relation[:memory]
180
+ # schema do
181
+ # attribute :user_id, Types::Int.meta(alias: :id)
182
+ # attribute :name, Types::String
183
+ # end
184
+ # end
185
+ #
186
+ # Users.schema[:id].key
187
+ # # :id
188
+ #
189
+ # Users.schema.project(Users.schema[:id].aliased(:user_id)).key
190
+ # # :user_id
191
+ #
192
+ # @return [Symbol]
193
+ #
194
+ # @api public
195
+ def key
196
+ meta[:alias] || name
197
+ end
198
+
199
+ # Return attribute's alias
200
+ #
201
+ # @example
202
+ # class Tasks < ROM::Relation[:memory]
203
+ # schema do
204
+ # attribute :user_id, Types::Int.meta(alias: :id)
205
+ # attribute :name, Types::String
206
+ # end
207
+ # end
208
+ #
209
+ # Users.schema[:user_id].alias
210
+ # # => :user_id
211
+ #
212
+ # Users.schema[:name].alias
213
+ # # => nil
214
+ #
215
+ # @return [NilClass,Symbol]
216
+ #
217
+ # @api public
218
+ def alias
219
+ meta[:alias]
220
+ end
221
+
222
+ # Return new attribute type with provided alias
223
+ #
224
+ # @example
225
+ # class Tasks < ROM::Relation[:memory]
226
+ # schema do
227
+ # attribute :user_id, Types::Int
228
+ # attribute :name, Types::String
229
+ # end
230
+ # end
231
+ #
232
+ # aliased_user_id = Users.schema[:user_id].aliased(:id)
233
+ #
234
+ # aliased_user_id.aliased?
235
+ # # => true
236
+ #
237
+ # aliased_user_id.name
238
+ # # => :user_id
239
+ #
240
+ # aliased_user_id.alias
241
+ # # => :id
242
+ #
243
+ # @param [Symbol] name The alias
244
+ #
245
+ # @return [Attribute]
246
+ #
247
+ # @api public
248
+ def aliased(name)
249
+ meta(alias: name)
250
+ end
251
+ alias_method :as, :aliased
252
+
253
+ # Return new attribute type with an alias using provided prefix
254
+ #
255
+ # @example
256
+ # class Users < ROM::Relation[:memory]
257
+ # schema do
258
+ # attribute :id, Types::Int
259
+ # attribute :name, Types::String
260
+ # end
261
+ # end
262
+ #
263
+ # prefixed_id = Users.schema[:id].prefixed
264
+ #
265
+ # prefixed_id.aliased?
266
+ # # => true
267
+ #
268
+ # prefixed_id.name
269
+ # # => :id
270
+ #
271
+ # prefixed_id.alias
272
+ # # => :users_id
273
+ #
274
+ # prefixed_id = Users.schema[:id].prefixed(:user)
275
+ #
276
+ # prefixed_id.alias
277
+ # # => :user_id
278
+ #
279
+ # @param [Symbol] prefix The prefix (defaults to source.dataset)
280
+ #
281
+ # @return [Attribute]
282
+ #
283
+ # @api public
284
+ def prefixed(prefix = source.dataset)
285
+ aliased(:"#{prefix}_#{name}")
286
+ end
287
+
288
+ # Return if the attribute type is from a wrapped relation
289
+ #
290
+ # Wrapped attributes are used when two schemas from different relations
291
+ # are merged together. This way we can identify them easily and handle
292
+ # correctly in places like auto-mapping.
293
+ #
294
+ # @api public
295
+ def wrapped?
296
+ meta[:wrapped].equal?(true)
297
+ end
298
+
299
+ # Return attribute type wrapped for the specified relation name
300
+ #
301
+ # @param [Symbol] name The name of the source relation (defaults to source.dataset)
302
+ #
303
+ # @return [Attribute]
304
+ #
305
+ # @api public
306
+ def wrapped(name = source.dataset)
307
+ prefixed(name).meta(wrapped: true)
308
+ end
309
+
310
+ # Return attribute type with additional meta information
311
+ #
312
+ # Return meta information hash if no opts are provided
313
+ #
314
+ # @param [Hash] opts The meta options
315
+ #
316
+ # @return [Attribute]
317
+ #
318
+ # @api public
319
+ def meta(opts = nil)
320
+ if opts
321
+ self.class.new(type.meta(opts))
322
+ else
323
+ type.meta
324
+ end
325
+ end
326
+
327
+ # Return string representation of the attribute type
328
+ #
329
+ # @return [String]
330
+ #
331
+ # @api public
332
+ def inspect
333
+ %(#<#{self.class}[#{type.name}] #{meta.map { |k, v| "#{k}=#{v.inspect}" }.join(' ')}>)
334
+ end
335
+ alias_method :pretty_inspect, :inspect
336
+
337
+ # Check if the attribute type is equal to another
338
+ #
339
+ # @param [Dry::Type, Attribute]
340
+ #
341
+ # @return [TrueClass,FalseClass]
342
+ #
343
+ # @api public
344
+ def eql?(other)
345
+ other.is_a?(self.class) ? super : type.eql?(other)
346
+ end
347
+
348
+ # Return if this attribute type has additional attribute type for reading
349
+ # tuple values
350
+ #
351
+ # @return [TrueClass, FalseClass]
352
+ #
353
+ # @api private
354
+ def read?
355
+ ! meta[:read].nil?
356
+ end
357
+
358
+ # Return read type or self
359
+ #
360
+ # @return [Attribute]
361
+ #
362
+ # @api private
363
+ def to_read_type
364
+ read? ? meta[:read] : type
365
+ end
366
+
367
+ # Return nullable attribute
368
+ #
369
+ # @return [Attribute]
370
+ #
371
+ # @api public
372
+ def optional
373
+ sum = self.class.new(super, options)
374
+ read? ? sum.meta(read: meta[:read].optional) : sum
375
+ end
376
+
377
+ # @api private
378
+ def respond_to_missing?(name, include_private = false)
379
+ type.respond_to?(name) || super
380
+ end
381
+
382
+ # Return AST for the type
383
+ #
384
+ # @return [Array]
385
+ #
386
+ # @api public
387
+ def to_ast
388
+ [:attribute, [name, type.to_ast(meta: false), meta_ast]]
389
+ end
390
+
391
+ # Return AST for the read type
392
+ #
393
+ # @return [Array]
394
+ #
395
+ # @api public
396
+ def to_read_ast
397
+ [:attribute, [name, to_read_type.to_ast(meta: false), meta_ast]]
398
+ end
399
+
400
+ # @api private
401
+ def meta_ast
402
+ meta_keys = %i(wrapped alias primary_key)
403
+ ast = meta.select { |k, _| meta_keys.include?(k) }
404
+ ast[:source] = source.to_sym if source
405
+ ast
406
+ end
407
+
408
+ memoize :to_ast, :to_read_ast, :meta_ast
409
+
410
+ private
411
+
412
+ # @api private
413
+ def method_missing(meth, *args, &block)
414
+ if type.respond_to?(meth)
415
+ response = type.__send__(meth, *args, &block)
416
+
417
+ if response.is_a?(type.class)
418
+ self.class.new(response, options)
419
+ else
420
+ response
421
+ end
422
+ else
423
+ super
424
+ end
425
+ end
426
+ end
427
+ end
@@ -20,7 +20,7 @@ module ROM
20
20
  end
21
21
 
22
22
  def auto_curried_methods
23
- @__auto_curried_methods__ ||= []
23
+ @__auto_curried_methods__ ||= Set.new
24
24
  end
25
25
 
26
26
  def auto_curry(name, &block)
data/lib/rom/command.rb CHANGED
@@ -432,6 +432,15 @@ module ROM
432
432
  result.equal?(:many)
433
433
  end
434
434
 
435
+ # Check if this command is restrictible through relation
436
+ #
437
+ # @return [TrueClass,FalseClass]
438
+ #
439
+ # @api private
440
+ def restrictible?
441
+ self.class.restrictable.equal?(true)
442
+ end
443
+
435
444
  private
436
445
 
437
446
  # Hook called by Pipeline to get composite class for commands
@@ -8,17 +8,25 @@ module ROM
8
8
  class CommandProxy
9
9
  attr_reader :command, :root
10
10
 
11
- def initialize(command)
11
+ # @api private
12
+ def initialize(command, root = Dry::Core::Inflector.singularize(command.name.relation).to_sym)
12
13
  @command = command
13
- @root = Dry::Core::Inflector.singularize(command.name.relation).to_sym
14
+ @root = root
14
15
  end
15
16
 
17
+ # @api private
16
18
  def call(input)
17
19
  command.call(root => input)
18
20
  end
19
21
 
22
+ # @api private
20
23
  def >>(other)
21
24
  self.class.new(command >> other)
22
25
  end
26
+
27
+ # @api private
28
+ def restrictible?
29
+ command.restrictible?
30
+ end
23
31
  end
24
32
  end
data/lib/rom/constants.rb CHANGED
@@ -19,12 +19,20 @@ module ROM
19
19
  RelationAlreadyDefinedError = Class.new(StandardError)
20
20
  MapperAlreadyDefinedError = Class.new(StandardError)
21
21
  NoRelationError = Class.new(StandardError)
22
+ InvalidRelationName = Class.new(StandardError)
22
23
  CommandError = Class.new(StandardError)
23
24
  KeyMissing = Class.new(ROM::CommandError)
24
25
  TupleCountMismatchError = Class.new(CommandError)
25
26
  UnknownPluginError = Class.new(StandardError)
26
27
  UnsupportedRelationError = Class.new(StandardError)
27
28
  MissingAdapterIdentifierError = Class.new(StandardError)
29
+ AttributeAlreadyDefinedError = Class.new(StandardError)
30
+
31
+ class InvalidRelationName < StandardError
32
+ def initialize(relation)
33
+ super("Relation name: #{relation} is a protected word, please use another relation name")
34
+ end
35
+ end
28
36
 
29
37
  class ElementNotFoundError < KeyError
30
38
  def initialize(key, registry)
data/lib/rom/core.rb CHANGED
@@ -26,7 +26,6 @@ require 'rom/container'
26
26
  require 'rom/create_container'
27
27
 
28
28
  # register core plugins
29
- require 'rom/plugins/configuration/configuration_dsl'
30
29
  require 'rom/plugins/relation/registry_reader'
31
30
  require 'rom/plugins/relation/instrumentation'
32
31
  require 'rom/plugins/command/schema'
@@ -37,7 +36,6 @@ module ROM
37
36
 
38
37
  plugins do
39
38
  register :mappers, ROM::Mapper::ConfigurationPlugin, type: :configuration
40
- register :macros, ROM::ConfigurationPlugins::ConfigurationDSL, type: :configuration
41
39
  register :timestamps, ROM::Plugins::Schema::Timestamps, type: :schema
42
40
  register :registry_reader, ROM::Plugins::Relation::RegistryReader, type: :relation
43
41
  register :instrumentation, ROM::Plugins::Relation::Instrumentation, type: :relation
data/lib/rom/gateway.rb CHANGED
@@ -144,17 +144,6 @@ module ROM
144
144
  # noop
145
145
  end
146
146
 
147
- # Schema inference hook
148
- #
149
- # Every gateway that supports schema inference should implement this method
150
- #
151
- # @return [Array] An array with dataset names
152
- #
153
- # @api private
154
- def schema
155
- []
156
- end
157
-
158
147
  # Disconnect is optional and it's a no-op by default
159
148
  #
160
149
  # @api public
@@ -19,12 +19,10 @@ module ROM
19
19
  default_input = options.fetch(:input, input)
20
20
 
21
21
  input_handler =
22
- if default_input != Hash && relation.schema?
22
+ if default_input != Hash
23
23
  -> tuple { relation.input_schema[input[tuple]] }
24
- elsif relation.schema?
25
- relation.input_schema
26
24
  else
27
- default_input
25
+ relation.input_schema
28
26
  end
29
27
 
30
28
  super(relation, options.merge(input: input_handler))
@@ -27,6 +27,9 @@ module ROM
27
27
  end
28
28
 
29
29
  DEFAULT_DATASET_PROC = -> * { self }.freeze
30
+ INVALID_RELATIONS_NAMES = [
31
+ :relations
32
+ ].freeze
30
33
 
31
34
  # Return adapter-specific relation subclass
32
35
  #
@@ -93,6 +96,8 @@ module ROM
93
96
  ds_name = dataset || schema_opts.fetch(:dataset, default_name.dataset)
94
97
  relation = as || schema_opts.fetch(:relation, ds_name)
95
98
 
99
+ raise InvalidRelationName.new(relation) if invalid_relation_name?(relation)
100
+
96
101
  @relation_name = Name[relation, ds_name]
97
102
 
98
103
  @schema_proc = proc do |*args, &inner_block|
@@ -173,7 +178,7 @@ module ROM
173
178
  # @api public
174
179
  def view(*args, &block)
175
180
  if args.size == 1 && block.arity > 0
176
- raise ArgumentError, "header must be set as second argument"
181
+ raise ArgumentError, "schema attribute names must be provided as the second argument"
177
182
  end
178
183
 
179
184
  name, new_schema_fn, relation_block =
@@ -247,7 +252,7 @@ module ROM
247
252
  ancestor_methods = ancestors.reject { |klass| klass == self }
248
253
  .map(&:instance_methods).flatten(1)
249
254
 
250
- instance_methods - ancestor_methods + auto_curried_methods
255
+ instance_methods - ancestor_methods + auto_curried_methods.to_a
251
256
  end
252
257
 
253
258
  # @api private
@@ -277,6 +282,12 @@ module ROM
277
282
  def name
278
283
  super || superclass.name
279
284
  end
285
+
286
+ private
287
+
288
+ def invalid_relation_name?(relation)
289
+ INVALID_RELATIONS_NAMES.include?(relation.to_sym)
290
+ end
280
291
  end
281
292
  end
282
293
  end
@@ -103,13 +103,19 @@ module ROM
103
103
  end
104
104
 
105
105
  # @api public
106
- def to_ast
107
- [:relation, [name.to_sym, attr_ast + node_ast, meta_ast]]
106
+ def command(type, *args)
107
+ if type == :create
108
+ super
109
+ else
110
+ raise NotImplementedError, "#{self.class}#command doesn't work with #{type.inspect} command type yet"
111
+ end
108
112
  end
109
113
 
114
+ private
115
+
110
116
  # @api private
111
- def node_ast
112
- nodes.map(&:to_ast)
117
+ def decorate?(other)
118
+ super || other.is_a?(Wrap)
113
119
  end
114
120
  end
115
121
  end
@@ -2,18 +2,38 @@ module ROM
2
2
  class Relation
3
3
  # Extensions for relation classes which provide access to commands
4
4
  #
5
- # @api private
5
+ # @api public
6
6
  module Commands
7
+ # Return a command for the relation
8
+ #
9
+ # @example
10
+ # users.command(:create)
11
+ #
12
+ # @param type [Symbol] The command type (:create, :update or :delete)
13
+ # @option :mapper [ROM::Mapper] An optional mapper applied to the command result
14
+ # @option :use [Array<Symbol>] A list of command plugins
15
+ # @option :result [:one, :many] Whether the command result has one or more rows.
16
+ # :one is default
17
+ #
18
+ # @return [ROM::Command]
19
+ #
7
20
  # @api public
8
21
  def command(type, mapper: nil, use: EMPTY_ARRAY, **opts)
9
- command = commands[type, adapter, to_ast, use, opts]
22
+ base_command = commands[type, adapter, to_ast, use, opts]
10
23
 
11
- if mapper
12
- command >> mappers[mapper]
13
- elsif mappers.any? && !command.is_a?(CommandProxy)
14
- mappers.reduce(command) { |a, (_, e)| a >> e }
15
- elsif auto_struct? || auto_map?
16
- command >> self.mapper
24
+ command =
25
+ if mapper
26
+ base_command >> mappers[mapper]
27
+ elsif mappers.any? && !base_command.is_a?(CommandProxy)
28
+ mappers.reduce(base_command) { |a, (_, e)| a >> e }
29
+ elsif auto_struct? || auto_map?
30
+ base_command >> self.mapper
31
+ else
32
+ base_command
33
+ end
34
+
35
+ if command.restrictible?
36
+ command.new(self)
17
37
  else
18
38
  command
19
39
  end