rom-core 4.0.0.beta2 → 4.0.0.beta3

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