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