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.
@@ -23,10 +23,8 @@ module ROM
23
23
 
24
24
  if response.is_a?(Loaded)
25
25
  response
26
- elsif relation.is_a?(Loaded)
27
- relation.new(response)
28
26
  else
29
- Loaded.new(relation, response)
27
+ relation.new(response)
30
28
  end
31
29
  end
32
30
  alias_method :[], :call
@@ -20,7 +20,7 @@ module ROM
20
20
  param :relation
21
21
 
22
22
  option :view, type: Types::Strict::Symbol
23
- option :arity, type: Types::Strict::Int, default: -> { -1 }
23
+ option :arity, type: Types::Strict::Int
24
24
  option :curry_args, default: -> { EMPTY_ARRAY }
25
25
 
26
26
  # Load relation if args match the arity
@@ -29,22 +29,18 @@ module ROM
29
29
  #
30
30
  # @api public
31
31
  def call(*args)
32
- if arity != -1
33
- all_args = curry_args + args
32
+ all_args = curry_args + args
34
33
 
35
- if all_args.empty?
36
- raise ArgumentError, "curried #{relation.class}##{view} relation was called without any arguments"
37
- end
34
+ if all_args.empty?
35
+ raise ArgumentError, "curried #{relation.class}##{view} relation was called without any arguments"
36
+ end
38
37
 
39
- if args.empty?
40
- self
41
- elsif arity == all_args.size
42
- Loaded.new(relation.__send__(view, *all_args))
43
- else
44
- __new__(relation, curry_args: all_args)
45
- end
38
+ if args.empty?
39
+ self
40
+ elsif arity == all_args.size
41
+ Loaded.new(relation.__send__(view, *all_args))
46
42
  else
47
- super
43
+ __new__(relation, curry_args: all_args)
48
44
  end
49
45
  end
50
46
  alias_method :[], :call
@@ -6,6 +6,7 @@ require 'rom/relation/loaded'
6
6
  require 'rom/relation/composite'
7
7
  require 'rom/relation/materializable'
8
8
  require 'rom/pipeline'
9
+ require 'rom/support/memoizable'
9
10
 
10
11
  module ROM
11
12
  class Relation
@@ -15,6 +16,8 @@ module ROM
15
16
  class Graph
16
17
  extend Initializer
17
18
 
19
+ include Memoizable
20
+
18
21
  param :root
19
22
 
20
23
  param :nodes
@@ -70,6 +73,11 @@ module ROM
70
73
  mappers[to_ast]
71
74
  end
72
75
 
76
+ # @api private
77
+ memoize def to_ast
78
+ [:relation, [name.relation, attr_ast + nodes.map(&:to_ast), meta_ast]]
79
+ end
80
+
73
81
  private
74
82
 
75
83
  # @api private
@@ -4,13 +4,6 @@ module ROM
4
4
  #
5
5
  # @api public
6
6
  module Materializable
7
- # @abstract
8
- #
9
- # @api public
10
- def call(*)
11
- raise NotImplementedError, "#{self.class}#call must be implemented"
12
- end
13
-
14
7
  # Coerce the relation to an array
15
8
  #
16
9
  # @return [Array]
@@ -71,7 +71,7 @@ module ROM
71
71
 
72
72
  # @api private
73
73
  def aliased?
74
- !aliaz.nil?
74
+ aliaz && aliaz != relation
75
75
  end
76
76
 
77
77
  # Return relation name
@@ -80,7 +80,7 @@ module ROM
80
80
  #
81
81
  # @api private
82
82
  def to_s
83
- if aliaz
83
+ if aliased?
84
84
  "#{relation} on #{dataset} as #{aliaz}"
85
85
  elsif relation == dataset
86
86
  relation.to_s
@@ -1,4 +1,5 @@
1
1
  require 'rom/relation/graph'
2
+ require 'rom/relation/combined'
2
3
 
3
4
  module ROM
4
5
  class Relation
@@ -6,18 +7,6 @@ module ROM
6
7
  #
7
8
  # @api public
8
9
  class Wrap < Graph
9
- extend Initializer
10
-
11
- include Materializable
12
- include Pipeline
13
- include Pipeline::Proxy
14
-
15
- param :root
16
- param :nodes
17
-
18
- alias_method :left, :root
19
- alias_method :right, :nodes
20
-
21
10
  # @api public
22
11
  def wrap(*args)
23
12
  self.class.new(root, nodes + root.wrap(*args).nodes)
@@ -39,21 +28,6 @@ module ROM
39
28
  raise NotImplementedError
40
29
  end
41
30
 
42
- # @api private
43
- def to_ast
44
- @__ast__ ||= [:relation, [name.relation, attr_ast + nodes_ast, meta_ast]]
45
- end
46
-
47
- # @api private
48
- def attr_ast
49
- root.attr_ast
50
- end
51
-
52
- # @api private
53
- def nodes_ast
54
- nodes.map(&:to_ast)
55
- end
56
-
57
31
  # Return if this is a wrap relation
58
32
  #
59
33
  # @return [true]
@@ -62,6 +36,13 @@ module ROM
62
36
  def wrap?
63
37
  true
64
38
  end
39
+
40
+ private
41
+
42
+ # @api private
43
+ def decorate?(other)
44
+ super || other.is_a?(Combined)
45
+ end
65
46
  end
66
47
  end
67
48
  end
data/lib/rom/relation.rb CHANGED
@@ -14,6 +14,7 @@ require 'rom/command_registry'
14
14
 
15
15
  require 'rom/relation/loaded'
16
16
  require 'rom/relation/curried'
17
+ require 'rom/relation/commands'
17
18
  require 'rom/relation/composite'
18
19
  require 'rom/relation/combined'
19
20
  require 'rom/relation/wrap'
@@ -55,9 +56,69 @@ module ROM
55
56
 
56
57
  extend Dry::Core::ClassAttributes
57
58
 
58
- defines :adapter, :gateway, :schema_opts, :schema_class,
59
+ defines :adapter, :schema_opts, :schema_class,
59
60
  :schema_attr_class, :schema_inferrer, :schema_dsl,
60
- :wrap_class, :auto_map, :auto_struct, :struct_namespace
61
+ :wrap_class
62
+
63
+ # @!method self.gateway
64
+ # Manage the gateway
65
+ #
66
+ # @overload gateway
67
+ # Return the gateway key that the relation is associated with
68
+ # @return [Symbol]
69
+ #
70
+ # @overload gateway(gateway_key)
71
+ # Link the relation to a gateway. Change this setting if the
72
+ # relation is defined on a non-default gateway
73
+ #
74
+ # @example
75
+ # class Users < ROM::Relation[:sql]
76
+ # gateway :custom
77
+ # end
78
+ #
79
+ # @param [Symbol] gateway_key
80
+ defines :gateway
81
+
82
+ # @!method self.auto_map
83
+ # Whether or not a relation and its compositions should be auto-mapped
84
+ #
85
+ # @overload auto_map
86
+ # Return auto_map setting value
87
+ # @return [Boolean]
88
+ #
89
+ # @overload auto_map(value)
90
+ # Set auto_map value
91
+ defines :auto_map
92
+
93
+ # @!method self.auto_struct
94
+ # Whether or not tuples should be auto-mapped to structs
95
+ #
96
+ # @overload auto_struct
97
+ # Return auto_struct setting value
98
+ # @return [Boolean]
99
+ #
100
+ # @overload auto_struct(value)
101
+ # Set auto_struct value
102
+ defines :auto_struct
103
+
104
+ # @!method self.struct_namespace
105
+ # Get or set a namespace for auto-generated struct classes.
106
+ # By default, new struct classes are created within ROM::Struct
107
+ #
108
+ # @example using custom namespace
109
+ # class Users < ROM::Relation[:sql]
110
+ # struct_namespace Entities
111
+ # end
112
+ #
113
+ # users.by_pk(1).one! # => #<Entities::User id=1 name="Jane Doe">
114
+ #
115
+ # @overload struct_namespace
116
+ # @return [Module] Default struct namespace
117
+ #
118
+ # @overload struct_namespace(namespace)
119
+ # @param [Module] namespace
120
+ #
121
+ defines :struct_namespace
61
122
 
62
123
  gateway :default
63
124
 
@@ -67,7 +128,7 @@ module ROM
67
128
 
68
129
  schema_opts EMPTY_HASH
69
130
  schema_dsl Schema::DSL
70
- schema_attr_class Schema::Attribute
131
+ schema_attr_class Attribute
71
132
  schema_class Schema
72
133
  schema_inferrer Schema::DEFAULT_INFERRER
73
134
 
@@ -109,12 +170,12 @@ module ROM
109
170
  # @!attribute [r] auto_map
110
171
  # @return [TrueClass,FalseClass] Whether or not a relation and its compositions should be auto-mapped
111
172
  # @api private
112
- option :auto_map, reader: true, default: -> { self.class.auto_map }
173
+ option :auto_map, default: -> { self.class.auto_map }
113
174
 
114
175
  # @!attribute [r] auto_struct
115
176
  # @return [TrueClass,FalseClass] Whether or not tuples should be auto-mapped to structs
116
177
  # @api private
117
- option :auto_struct, reader: true, default: -> { self.class.auto_struct }
178
+ option :auto_struct, default: -> { self.class.auto_struct }
118
179
 
119
180
  # @!attribute [r] struct_namespace
120
181
  # @return [Module] Custom struct namespace
@@ -146,7 +207,7 @@ module ROM
146
207
  # tasks_with_users[:title, :tasks]
147
208
  # # => #<ROM::SQL::Attribute[String] primary_key=false name=:title source=ROM::Relation::Name(tasks)>
148
209
  #
149
- # @return [Schema::Attribute]
210
+ # @return [Attribute]
150
211
  #
151
212
  # @api public
152
213
  def [](name)
@@ -176,7 +237,7 @@ module ROM
176
237
  #
177
238
  # @overload combine(*associations)
178
239
  # Composes relations using configured associations
179
-
240
+ #
180
241
  # @example
181
242
  # users.combine(:tasks, :posts)
182
243
  # @param *associations [Array<Symbol>] A list of association names
@@ -334,17 +395,19 @@ module ROM
334
395
  #
335
396
  # @api public
336
397
  def new(dataset, new_opts = EMPTY_HASH)
337
- if new_opts.empty?
338
- opts = options
339
- elsif new_opts.key?(:schema)
340
- opts = options.reject { |k, _| k == :input_schema || k == :output_schema }.merge(new_opts)
341
- else
342
- opts = options.merge(new_opts)
343
- end
398
+ opts =
399
+ if new_opts.empty?
400
+ options
401
+ elsif new_opts.key?(:schema)
402
+ options.merge(new_opts).reject { |k, _| k == :input_schema || k == :output_schema }
403
+ else
404
+ options.merge(new_opts)
405
+ end
344
406
 
345
407
  self.class.new(dataset, opts)
346
408
  end
347
409
 
410
+ undef_method :with
348
411
  # Returns a new instance with the same dataset but new options
349
412
  #
350
413
  # @example
@@ -417,7 +480,7 @@ module ROM
417
480
  # Map tuples to the provided custom model class
418
481
  #
419
482
  # @example
420
- # users.as(MyUserModel)
483
+ # users.map_with(MyUserModel)
421
484
  #
422
485
  # @param [Class>] model Your custom model class
423
486
  #
@@ -1,7 +1,7 @@
1
1
  require 'dry/equalizer'
2
2
 
3
3
  require 'rom/types'
4
- require 'rom/schema/attribute'
4
+ require 'rom/attribute'
5
5
  require 'rom/schema/associations_dsl'
6
6
 
7
7
  module ROM
@@ -45,7 +45,7 @@ module ROM
45
45
  # @api public
46
46
  def attribute(name, type, options = EMPTY_HASH)
47
47
  if attributes.key?(name)
48
- ::Kernel.raise ::ROM::Schema::AttributeAlreadyDefinedError,
48
+ ::Kernel.raise ::ROM::AttributeAlreadyDefinedError,
49
49
  "Attribute #{ name.inspect } already defined"
50
50
  end
51
51
 
@@ -93,6 +93,8 @@ module ROM
93
93
  def build_type(name, type, options = EMPTY_HASH)
94
94
  if options[:read]
95
95
  type.meta(name: name, source: relation, read: options[:read])
96
+ elsif type.optional? && !type.meta[:read] && type.right.meta[:read]
97
+ type.meta(name: name, source: relation, read: type.right.meta[:read].optional)
96
98
  else
97
99
  type.meta(name: name, source: relation)
98
100
  end
@@ -11,7 +11,10 @@ module ROM
11
11
 
12
12
  MissingAttributesError = Class.new(StandardError) do
13
13
  def initialize(name, attributes)
14
- super("missing attributes in #{name.inspect} schema: #{attributes.map(&:inspect).join(', ')}")
14
+ super(
15
+ "Following attributes in #{Relation::Name[name].relation.inspect} schema cannot "\
16
+ "be inferred and have to be defined explicitly: #{attributes.map(&:inspect).join(', ')}"
17
+ )
15
18
  end
16
19
  end
17
20
 
data/lib/rom/schema.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'dry/equalizer'
2
2
 
3
- require 'rom/schema/attribute'
3
+ require 'rom/constants'
4
+ require 'rom/attribute'
4
5
  require 'rom/schema/dsl'
5
6
  require 'rom/schema/inferrer'
6
7
  require 'rom/association_set'
@@ -39,8 +40,10 @@ module ROM
39
40
  registry = event[:registry]
40
41
 
41
42
  registry.each do |_, relation|
42
- relation.schema.finalize_associations!(relations: registry)
43
- relation.schema.finalize!
43
+ unless relation.schema.frozen?
44
+ relation.schema.finalize_associations!(relations: registry)
45
+ relation.schema.finalize!
46
+ end
44
47
  end
45
48
  end
46
49
 
@@ -58,8 +61,6 @@ module ROM
58
61
 
59
62
  DEFAULT_INFERRER = Inferrer.new(enabled: false).freeze
60
63
 
61
- AttributeAlreadyDefinedError = Class.new(StandardError)
62
-
63
64
  extend Initializer
64
65
 
65
66
  include Dry::Equalizer(:name, :attributes, :associations)
@@ -101,7 +102,7 @@ module ROM
101
102
  # Define a relation schema from plain rom types
102
103
  #
103
104
  # Resulting schema will decorate plain rom types with adapter-specific types
104
- # By default `Schema::Attribute` will be used
105
+ # By default `Attribute` will be used
105
106
  #
106
107
  # @param [Relation::Name, Symbol] name The schema name, typically ROM::Relation::Name
107
108
  #
@@ -147,7 +148,7 @@ module ROM
147
148
 
148
149
  # Iterate over schema's attributes
149
150
  #
150
- # @yield [Schema::Attribute]
151
+ # @yield [Attribute]
151
152
  #
152
153
  # @api public
153
154
  def each(&block)
@@ -197,7 +198,7 @@ module ROM
197
198
 
198
199
  # Project a schema to include only specified attributes
199
200
  #
200
- # @param [*Array<Symbol, Schema::Attribute>] names Attribute names
201
+ # @param [*Array<Symbol, Attribute>] names Attribute names
201
202
  #
202
203
  # @return [Schema]
203
204
  #
@@ -260,7 +261,7 @@ module ROM
260
261
 
261
262
  # Return FK attribute for a given relation name
262
263
  #
263
- # @return [Schema::Attribute]
264
+ # @return [Attribute]
264
265
  #
265
266
  # @api public
266
267
  def foreign_key(relation)
@@ -269,7 +270,7 @@ module ROM
269
270
 
270
271
  # Return primary key attributes
271
272
  #
272
- # @return [Array<Schema::Attribute>]
273
+ # @return [Array<Attribute>]
273
274
  #
274
275
  # @api public
275
276
  def primary_key
@@ -292,7 +293,7 @@ module ROM
292
293
  #
293
294
  # This returns a new schema instance
294
295
  #
295
- # @param [*Array<Schema::Attribute>]
296
+ # @param [*Array<Attribute>]
296
297
  #
297
298
  # @return [Schema]
298
299
  #
@@ -303,7 +304,7 @@ module ROM
303
304
 
304
305
  # Return a new schema with uniq attributes
305
306
  #
306
- # @param [*Array<Schema::Attribute>]
307
+ # @param [*Array<Attribute>]
307
308
  #
308
309
  # @return [Schema]
309
310
  #
@@ -36,13 +36,14 @@ module ROM
36
36
  private
37
37
 
38
38
  def check_duplicate_registered_mappers
39
- mappers_register_as = mapper_classes.map(&:register_as).compact
40
- mappers_register_as.select { |register_as| mappers_register_as.count(register_as) > 1 }
39
+ mapper_relation_register = mapper_classes.map {|mapper_class| [mapper_class.relation, mapper_class.register_as].compact }
40
+ return if mapper_relation_register.uniq.count == mapper_classes.count
41
+ mapper_relation_register.select { |relation_register_as| mapper_relation_register.count(relation_register_as) > 1 }
41
42
  .uniq
42
43
  .each do |duplicated_mappers|
43
44
  raise MapperAlreadyDefinedError,
44
- "Mapper with `register_as #{duplicated_mappers.inspect}` registered more " \
45
- "than once"
45
+ "Mapper with `register_as #{duplicated_mappers.last.inspect}` registered more " \
46
+ "than once for relation #{duplicated_mappers.first.inspect}"
46
47
  end
47
48
  end
48
49
 
@@ -71,19 +71,17 @@ module ROM
71
71
  # where klass' gateway points to non-existant repo
72
72
  gateway = @gateways.fetch(klass.gateway)
73
73
 
74
- if klass.schema_proc && !klass.schema
75
- plugins = schema_plugins
74
+ plugins = schema_plugins
76
75
 
77
- resolved_schema = klass.schema_proc.call do
78
- plugins.each { |plugin| app_plugin(plugin) }
79
- end
80
-
81
- klass.set_schema!(resolved_schema)
76
+ schema = klass.schema_proc.call do
77
+ plugins.each { |plugin| app_plugin(plugin) }
82
78
  end
83
79
 
80
+ klass.set_schema!(schema) if klass.schema.nil?
81
+
84
82
  notifications.trigger(
85
83
  'configuration.relations.schema.allocated',
86
- schema: klass.schema, gateway: gateway, registry: registry
84
+ schema: schema, gateway: gateway, registry: registry
87
85
  )
88
86
 
89
87
  relation_plugins.each do |plugin|
@@ -92,10 +90,9 @@ module ROM
92
90
 
93
91
  notifications.trigger(
94
92
  'configuration.relations.schema.set',
95
- schema: resolved_schema, relation: klass, adapter: klass.adapter
93
+ schema: schema, relation: klass, adapter: klass.adapter
96
94
  )
97
95
 
98
- schema = klass.schema
99
96
  rel_key = schema.name.to_sym
100
97
  dataset = gateway.dataset(schema.name.dataset).instance_exec(klass, &klass.dataset)
101
98
 
@@ -3,16 +3,61 @@ require 'dry/equalizer'
3
3
  require 'rom/constants'
4
4
 
5
5
  module ROM
6
+ # Notification subsystem
7
+ #
8
+ # This is an abstract event bus that implements a simple pub/sub protocol.
9
+ # The Notifications module is used in the setup process to decouple
10
+ # different modules from each other.
11
+ #
12
+ # @example
13
+ # class Setup
14
+ # extend ROM::Notifications
15
+ #
16
+ # register_event('setup.before_setup')
17
+ # register_event('setup.after_setup')
18
+ #
19
+ # def initialize
20
+ # @bus = Notifications.event_bus(:setup)
21
+ # end
22
+ #
23
+ # def setup
24
+ # @bus.trigger('setup.before_setup', at: Time.now)
25
+ # # ...
26
+ # @bus.trigger('setup.after_setup', at: Time.now)
27
+ # end
28
+ # end
29
+ #
30
+ # class Plugin
31
+ # extend ROM::Notifications::Listener
32
+ #
33
+ # subscribe('setup.after_setup') do |event|
34
+ # puts "Loaded at #{event.at.iso8601}"
35
+ # end
36
+ # end
37
+ #
6
38
  module Notifications
7
39
  LISTENERS_HASH = Hash.new { |h, k| h[k] = [] }
8
40
 
9
41
  module Publisher
42
+ # Subscribe to events.
43
+ # If the query parameter is provided, filters events by payload.
44
+ #
45
+ # @param [String] event_id The event key
46
+ # @param [Hash] query An optional event filter
47
+ # @yield [block] The callback
48
+ # @return [Object] self
49
+ #
10
50
  # @api public
11
51
  def subscribe(event_id, query = EMPTY_HASH, &block)
12
52
  listeners[event_id] << [block, query]
13
53
  self
14
54
  end
15
55
 
56
+ # Trigger an event
57
+ #
58
+ # @param [String] event_id The event key
59
+ # @param [Hash] payload An optional payload
60
+ #
16
61
  # @api public
17
62
  def trigger(event_id, payload = EMPTY_HASH)
18
63
  event = events[event_id]
@@ -23,20 +68,40 @@ module ROM
23
68
  end
24
69
  end
25
70
 
71
+ # Event object
72
+ #
73
+ # @api public
26
74
  class Event
27
75
  include Dry::Equalizer(:id, :payload)
28
76
 
77
+ # @attr_reader [String] id Event ID
29
78
  attr_reader :id
30
79
 
80
+ # @api public
31
81
  def initialize(id, payload = EMPTY_HASH)
32
82
  @id = id
33
83
  @payload = payload
34
84
  end
35
85
 
86
+ # Get data from the payload
87
+ #
88
+ # @param [String,Symbol] name
89
+ #
90
+ # @api public
36
91
  def [](name)
37
92
  @payload.fetch(name)
38
93
  end
39
94
 
95
+ # Get or set a payload
96
+ #
97
+ # @overload
98
+ # @return [Hash] payload
99
+ #
100
+ # @overload payload(data)
101
+ # @param [Hash] data A new payload
102
+ # @return [Event] A copy of the event with the provided payload
103
+ #
104
+ # @api public
40
105
  def payload(data = nil)
41
106
  if data
42
107
  self.class.new(id, @payload.merge(data))
@@ -45,10 +110,17 @@ module ROM
45
110
  end
46
111
  end
47
112
 
113
+ # Trigger the event
114
+ #
115
+ # @param [#call] listener
116
+ # @param [Hash] query
117
+ #
118
+ # @api private
48
119
  def trigger(listener, query = EMPTY_HASH)
49
120
  listener.(self) if trigger?(query)
50
121
  end
51
122
 
123
+ # @api private
52
124
  def trigger?(query)
53
125
  query.empty? || query.all? { |key, value| @payload[key] == value }
54
126
  end
@@ -56,6 +128,11 @@ module ROM
56
128
 
57
129
  extend Publisher
58
130
 
131
+ # Register an event
132
+ #
133
+ # @param [String] id A unique event key
134
+ # @param [Hash] info
135
+ #
59
136
  # @api public
60
137
  def register_event(id, info = EMPTY_HASH)
61
138
  Notifications.events[id] = Event.new(id, info)
@@ -71,6 +148,11 @@ module ROM
71
148
  @__listeners__ ||= LISTENERS_HASH.dup
72
149
  end
73
150
 
151
+ # Build an event bus
152
+ #
153
+ # @param [Symbol] id Bus key
154
+ # @return [Notifications::EventBus] A new bus
155
+ #
74
156
  # @api public
75
157
  def self.event_bus(id)
76
158
  EventBus.new(id, events: events.dup, listeners: listeners.dup)
@@ -78,12 +160,22 @@ module ROM
78
160
 
79
161
  # @api public
80
162
  module Listener
163
+ # Subscribe to events
164
+ #
165
+ # @param [String] event_id The event key
166
+ # @param [Hash] query An optional event filter
167
+ # @return [Object] self
168
+ #
81
169
  # @api public
82
170
  def subscribe(event_id, query = EMPTY_HASH, &block)
83
171
  Notifications.listeners[event_id] << [block, query]
84
172
  end
85
173
  end
86
174
 
175
+ # Event bus
176
+ #
177
+ # An event bus stores listeners (callbacks) and events
178
+ #
87
179
  # @api public
88
180
  class EventBus
89
181
  include Publisher
data/lib/rom/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module ROM
2
2
  module Core
3
- VERSION = '4.0.0.beta2'.freeze
3
+ VERSION = '4.0.0.beta3'.freeze
4
4
  end
5
5
  end