rom-core 4.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +603 -0
  3. data/LICENSE +20 -0
  4. data/README.md +18 -0
  5. data/lib/rom-core.rb +1 -0
  6. data/lib/rom/array_dataset.rb +44 -0
  7. data/lib/rom/association_set.rb +16 -0
  8. data/lib/rom/associations/abstract.rb +135 -0
  9. data/lib/rom/associations/definitions.rb +5 -0
  10. data/lib/rom/associations/definitions/abstract.rb +116 -0
  11. data/lib/rom/associations/definitions/many_to_many.rb +24 -0
  12. data/lib/rom/associations/definitions/many_to_one.rb +11 -0
  13. data/lib/rom/associations/definitions/one_to_many.rb +11 -0
  14. data/lib/rom/associations/definitions/one_to_one.rb +11 -0
  15. data/lib/rom/associations/definitions/one_to_one_through.rb +11 -0
  16. data/lib/rom/associations/many_to_many.rb +81 -0
  17. data/lib/rom/associations/many_to_one.rb +37 -0
  18. data/lib/rom/associations/one_to_many.rb +37 -0
  19. data/lib/rom/associations/one_to_one.rb +8 -0
  20. data/lib/rom/associations/one_to_one_through.rb +8 -0
  21. data/lib/rom/associations/through_identifier.rb +39 -0
  22. data/lib/rom/auto_curry.rb +55 -0
  23. data/lib/rom/cache.rb +46 -0
  24. data/lib/rom/command.rb +488 -0
  25. data/lib/rom/command_compiler.rb +239 -0
  26. data/lib/rom/command_proxy.rb +24 -0
  27. data/lib/rom/command_registry.rb +141 -0
  28. data/lib/rom/commands.rb +3 -0
  29. data/lib/rom/commands/class_interface.rb +270 -0
  30. data/lib/rom/commands/composite.rb +53 -0
  31. data/lib/rom/commands/create.rb +13 -0
  32. data/lib/rom/commands/delete.rb +14 -0
  33. data/lib/rom/commands/graph.rb +88 -0
  34. data/lib/rom/commands/graph/class_interface.rb +62 -0
  35. data/lib/rom/commands/graph/input_evaluator.rb +62 -0
  36. data/lib/rom/commands/lazy.rb +99 -0
  37. data/lib/rom/commands/lazy/create.rb +23 -0
  38. data/lib/rom/commands/lazy/delete.rb +27 -0
  39. data/lib/rom/commands/lazy/update.rb +34 -0
  40. data/lib/rom/commands/result.rb +96 -0
  41. data/lib/rom/commands/update.rb +14 -0
  42. data/lib/rom/configuration.rb +114 -0
  43. data/lib/rom/configuration_dsl.rb +87 -0
  44. data/lib/rom/configuration_dsl/command.rb +41 -0
  45. data/lib/rom/configuration_dsl/command_dsl.rb +35 -0
  46. data/lib/rom/configuration_dsl/relation.rb +26 -0
  47. data/lib/rom/configuration_plugin.rb +17 -0
  48. data/lib/rom/constants.rb +64 -0
  49. data/lib/rom/container.rb +147 -0
  50. data/lib/rom/core.rb +46 -0
  51. data/lib/rom/create_container.rb +60 -0
  52. data/lib/rom/data_proxy.rb +94 -0
  53. data/lib/rom/enumerable_dataset.rb +68 -0
  54. data/lib/rom/environment.rb +70 -0
  55. data/lib/rom/gateway.rb +184 -0
  56. data/lib/rom/global.rb +58 -0
  57. data/lib/rom/global/plugin_dsl.rb +47 -0
  58. data/lib/rom/initializer.rb +64 -0
  59. data/lib/rom/lint/enumerable_dataset.rb +54 -0
  60. data/lib/rom/lint/gateway.rb +120 -0
  61. data/lib/rom/lint/linter.rb +78 -0
  62. data/lib/rom/lint/spec.rb +20 -0
  63. data/lib/rom/lint/test.rb +98 -0
  64. data/lib/rom/mapper_registry.rb +24 -0
  65. data/lib/rom/memory.rb +4 -0
  66. data/lib/rom/memory/associations.rb +4 -0
  67. data/lib/rom/memory/associations/many_to_many.rb +10 -0
  68. data/lib/rom/memory/associations/many_to_one.rb +10 -0
  69. data/lib/rom/memory/associations/one_to_many.rb +10 -0
  70. data/lib/rom/memory/associations/one_to_one.rb +10 -0
  71. data/lib/rom/memory/commands.rb +56 -0
  72. data/lib/rom/memory/dataset.rb +97 -0
  73. data/lib/rom/memory/gateway.rb +64 -0
  74. data/lib/rom/memory/relation.rb +62 -0
  75. data/lib/rom/memory/schema.rb +23 -0
  76. data/lib/rom/memory/storage.rb +59 -0
  77. data/lib/rom/memory/types.rb +9 -0
  78. data/lib/rom/pipeline.rb +105 -0
  79. data/lib/rom/plugin.rb +25 -0
  80. data/lib/rom/plugin_base.rb +45 -0
  81. data/lib/rom/plugin_registry.rb +197 -0
  82. data/lib/rom/plugins/command/schema.rb +37 -0
  83. data/lib/rom/plugins/configuration/configuration_dsl.rb +21 -0
  84. data/lib/rom/plugins/relation/instrumentation.rb +51 -0
  85. data/lib/rom/plugins/relation/registry_reader.rb +44 -0
  86. data/lib/rom/plugins/schema/timestamps.rb +58 -0
  87. data/lib/rom/registry.rb +71 -0
  88. data/lib/rom/relation.rb +548 -0
  89. data/lib/rom/relation/class_interface.rb +282 -0
  90. data/lib/rom/relation/commands.rb +23 -0
  91. data/lib/rom/relation/composite.rb +46 -0
  92. data/lib/rom/relation/curried.rb +103 -0
  93. data/lib/rom/relation/graph.rb +197 -0
  94. data/lib/rom/relation/loaded.rb +127 -0
  95. data/lib/rom/relation/materializable.rb +66 -0
  96. data/lib/rom/relation/name.rb +111 -0
  97. data/lib/rom/relation/view_dsl.rb +64 -0
  98. data/lib/rom/relation/wrap.rb +83 -0
  99. data/lib/rom/relation_registry.rb +10 -0
  100. data/lib/rom/schema.rb +437 -0
  101. data/lib/rom/schema/associations_dsl.rb +195 -0
  102. data/lib/rom/schema/attribute.rb +419 -0
  103. data/lib/rom/schema/dsl.rb +164 -0
  104. data/lib/rom/schema/inferrer.rb +66 -0
  105. data/lib/rom/schema_plugin.rb +27 -0
  106. data/lib/rom/setup.rb +68 -0
  107. data/lib/rom/setup/auto_registration.rb +74 -0
  108. data/lib/rom/setup/auto_registration_strategies/base.rb +16 -0
  109. data/lib/rom/setup/auto_registration_strategies/custom_namespace.rb +63 -0
  110. data/lib/rom/setup/auto_registration_strategies/no_namespace.rb +20 -0
  111. data/lib/rom/setup/auto_registration_strategies/with_namespace.rb +18 -0
  112. data/lib/rom/setup/finalize.rb +103 -0
  113. data/lib/rom/setup/finalize/finalize_commands.rb +60 -0
  114. data/lib/rom/setup/finalize/finalize_mappers.rb +56 -0
  115. data/lib/rom/setup/finalize/finalize_relations.rb +135 -0
  116. data/lib/rom/support/configurable.rb +85 -0
  117. data/lib/rom/support/memoizable.rb +58 -0
  118. data/lib/rom/support/notifications.rb +103 -0
  119. data/lib/rom/transaction.rb +24 -0
  120. data/lib/rom/types.rb +26 -0
  121. data/lib/rom/version.rb +5 -0
  122. metadata +289 -0
@@ -0,0 +1,197 @@
1
+ require 'rom/relation/loaded'
2
+ require 'rom/relation/composite'
3
+ require 'rom/relation/materializable'
4
+ require 'rom/relation/commands'
5
+ require 'rom/pipeline'
6
+
7
+ module ROM
8
+ class Relation
9
+ # Compose relations using join-keys
10
+ #
11
+ # @example
12
+ # class Users < ROM::Relation[:memory]
13
+ # end
14
+ #
15
+ # class Tasks < ROM::Relation[:memory]
16
+ # def for_users(users)
17
+ # restrict(user: users.map { |user| user[:name] })
18
+ # end
19
+ # end
20
+ #
21
+ # rom.relations[:users] << { name: 'Jane' }
22
+ # rom.relations[:tasks] << { user: 'Jane', title: 'Do something' }
23
+ #
24
+ # rom.relations[:users].combine(rom.relations[:tasks].for_users)
25
+ #
26
+ # @api public
27
+ class Graph
28
+ include Materializable
29
+ include Commands
30
+ include Pipeline
31
+ include Pipeline::Proxy
32
+
33
+ # Root aka parent relation
34
+ #
35
+ # @return [Relation::Lazy]
36
+ #
37
+ # @api private
38
+ attr_reader :root
39
+
40
+ # Child relation nodes
41
+ #
42
+ # @return [Array<Relation::Lazy>]
43
+ #
44
+ # @api private
45
+ attr_reader :nodes
46
+
47
+ alias_method :left, :root
48
+ alias_method :right, :nodes
49
+
50
+ # @api private
51
+ def self.build(root, nodes)
52
+ if nodes.any? { |node| node.instance_of?(Composite) }
53
+ raise UnsupportedRelationError,
54
+ "Combining with composite relations is not supported"
55
+ else
56
+ new(root, nodes)
57
+ end
58
+ end
59
+
60
+ # @api private
61
+ def initialize(root, nodes)
62
+ root_ns = root.options[:struct_namespace]
63
+ @root = root
64
+ @nodes = nodes.map { |node| node.struct_namespace(root_ns) }
65
+ end
66
+
67
+ # @api public
68
+ def with_nodes(nodes)
69
+ self.class.new(root, nodes)
70
+ end
71
+
72
+ # Return if this is a graph relation
73
+ #
74
+ # @return [true]
75
+ #
76
+ # @api private
77
+ def graph?
78
+ true
79
+ end
80
+
81
+ # Combine this graph with more nodes
82
+ #
83
+ # @param [Array<Relation::Lazy>]
84
+ #
85
+ # @return [Graph]
86
+ #
87
+ # @api public
88
+ def graph(*others)
89
+ self.class.new(root, nodes + others)
90
+ end
91
+
92
+ # @api public
93
+ def combine(*args)
94
+ self.class.new(root, nodes + root.combine(*args).nodes)
95
+ end
96
+
97
+ # Materialize this relation graph
98
+ #
99
+ # @return [Loaded]
100
+ #
101
+ # @api public
102
+ def call(*args)
103
+ left = root.with(auto_struct: false).call(*args)
104
+
105
+ right =
106
+ if left.empty?
107
+ nodes.map { |node| Loaded.new(node, EMPTY_ARRAY) }
108
+ else
109
+ nodes.map { |node| node.call(left) }
110
+ end
111
+
112
+ if auto_map?
113
+ Loaded.new(self, mapper.([left, right]))
114
+ else
115
+ Loaded.new(self, [left, right])
116
+ end
117
+ end
118
+
119
+ # @api public
120
+ def map_with(*args)
121
+ self.class.new(root.map_with(*args), nodes)
122
+ end
123
+
124
+ # @api public
125
+ def map_to(klass)
126
+ self.class.new(root.map_to(klass), nodes)
127
+ end
128
+
129
+ # Return a new graph with adjusted node returned from a block
130
+ #
131
+ # @example with a node identifier
132
+ # aggregate(:tasks).node(:tasks) { |tasks| tasks.prioritized }
133
+ #
134
+ # @example with a nested path
135
+ # aggregate(tasks: :tags).node(tasks: :tags) { |tags| tags.where(name: 'red') }
136
+ #
137
+ # @param [Symbol] name The node relation name
138
+ #
139
+ # @yieldparam [RelationProxy] The relation node
140
+ # @yieldreturn [RelationProxy] The new relation node
141
+ #
142
+ # @return [RelationProxy]
143
+ #
144
+ # @api public
145
+ def node(name, &block)
146
+ if name.is_a?(Symbol) && !nodes.map { |n| n.name.key }.include?(name)
147
+ raise ArgumentError, "#{name.inspect} is not a valid aggregate node name"
148
+ end
149
+
150
+ new_nodes = nodes.map { |node|
151
+ case name
152
+ when Symbol
153
+ name == node.name.key ? yield(node) : node
154
+ when Hash
155
+ other, *rest = name.flatten(1)
156
+ if other == node.name.key
157
+ nodes.detect { |n| n.name.key == other }.node(*rest, &block)
158
+ else
159
+ node
160
+ end
161
+ else
162
+ node
163
+ end
164
+ }
165
+
166
+ with_nodes(new_nodes)
167
+ end
168
+
169
+ # @api public
170
+ def to_ast
171
+ [:relation, [name.to_sym, attr_ast + node_ast, meta_ast]]
172
+ end
173
+
174
+ # @api private
175
+ def node_ast
176
+ nodes.map(&:to_ast)
177
+ end
178
+
179
+ # @api private
180
+ def mapper
181
+ mappers[to_ast]
182
+ end
183
+
184
+ private
185
+
186
+ # @api private
187
+ def decorate?(other)
188
+ super || other.is_a?(Composite) || other.is_a?(Curried)
189
+ end
190
+
191
+ # @api private
192
+ def composite_class
193
+ Relation::Composite
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,127 @@
1
+ module ROM
2
+ class Relation
3
+ # Materializes a relation and exposes interface to access the data
4
+ #
5
+ # @api public
6
+ class Loaded
7
+ include Enumerable
8
+
9
+ # Coerce loaded relation to an array
10
+ #
11
+ # @return [Array]
12
+ #
13
+ # @api public
14
+ alias_method :to_ary, :to_a
15
+
16
+ # Source relation
17
+ #
18
+ # @return [Relation]
19
+ #
20
+ # @api private
21
+ attr_reader :source
22
+
23
+ # Materialized relation
24
+ #
25
+ # @return [Object]
26
+ #
27
+ # @api private
28
+ attr_reader :collection
29
+
30
+ # @api private
31
+ def initialize(source, collection = source.to_a)
32
+ @source = source
33
+ @collection = collection
34
+ end
35
+
36
+ # Yield relation tuples
37
+ #
38
+ # @yield [Hash]
39
+ #
40
+ # @api public
41
+ def each(&block)
42
+ return to_enum unless block
43
+ collection.each { |tuple| yield(tuple) }
44
+ end
45
+
46
+ # Returns a single tuple from the relation if there is one.
47
+ #
48
+ # @raise [ROM::TupleCountMismatchError] if the relation contains more than
49
+ # one tuple
50
+ #
51
+ # @api public
52
+ def one
53
+ if collection.count > 1
54
+ raise(
55
+ TupleCountMismatchError,
56
+ 'The relation consists of more than one tuple'
57
+ )
58
+ else
59
+ collection.first
60
+ end
61
+ end
62
+
63
+ # Like [one], but additionally raises an error if the relation is empty.
64
+ #
65
+ # @raise [ROM::TupleCountMismatchError] if the relation does not contain
66
+ # exactly one tuple
67
+ #
68
+ # @api public
69
+ def one!
70
+ one || raise(
71
+ TupleCountMismatchError,
72
+ 'The relation does not contain any tuples'
73
+ )
74
+ end
75
+
76
+ # Return a list of values under provided key
77
+ #
78
+ # @example
79
+ # all_users = rom.relations[:users].call
80
+ # all_users.pluck(:name)
81
+ # # ["Jane", "Joe"]
82
+ #
83
+ # @param [Symbol] key The key name
84
+ #
85
+ # @return [Array]
86
+ # @raises KeyError when provided key doesn't exist in any of the tuples
87
+ #
88
+ # @api public
89
+ def pluck(key)
90
+ map { |tuple| tuple.fetch(key) }
91
+ end
92
+
93
+ # Pluck primary key values
94
+ #
95
+ # This method *may not work* with adapters that don't provide relations
96
+ # that have primary key configured
97
+ #
98
+ # @example
99
+ # users = rom.relations[:users].call
100
+ # users.primary_keys
101
+ # # [1, 2, 3]
102
+ #
103
+ # @return [Array]
104
+ #
105
+ # @api public
106
+ def primary_keys
107
+ pluck(source.primary_key)
108
+ end
109
+
110
+ # Return if loaded relation is empty
111
+ #
112
+ # @return [TrueClass,FalseClass]
113
+ #
114
+ # @api public
115
+ def empty?
116
+ collection.empty?
117
+ end
118
+
119
+ # Return a loaded relation with a new collection
120
+ #
121
+ # @api public
122
+ def new(collection)
123
+ self.class.new(source, collection)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,66 @@
1
+ module ROM
2
+ class Relation
3
+ # Interface for objects that can be materialized into a loaded relation
4
+ #
5
+ # @api public
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
+ # Coerce the relation to an array
15
+ #
16
+ # @return [Array]
17
+ #
18
+ # @api public
19
+ def to_a
20
+ call.to_a
21
+ end
22
+ alias_method :to_ary, :to_a
23
+
24
+ # Yield relation tuples
25
+ #
26
+ # @yield [Hash,Object]
27
+ #
28
+ # @api public
29
+ def each(&block)
30
+ return to_enum unless block
31
+ to_a.each { |tuple| yield(tuple) }
32
+ end
33
+
34
+ # Delegate to loaded relation and return one object
35
+ #
36
+ # @return [Object]
37
+ #
38
+ # @see Loaded#one
39
+ #
40
+ # @api public
41
+ def one
42
+ call.one
43
+ end
44
+
45
+ # Delegate to loaded relation and return one object
46
+ #
47
+ # @return [Object]
48
+ #
49
+ # @see Loaded#one
50
+ #
51
+ # @api public
52
+ def one!
53
+ call.one!
54
+ end
55
+
56
+ # Return first tuple from a relation coerced to an array
57
+ #
58
+ # @return [Object]
59
+ #
60
+ # @api public
61
+ def first
62
+ to_a.first
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,111 @@
1
+ require 'dry/equalizer'
2
+ require 'concurrent/map'
3
+
4
+ module ROM
5
+ class Relation
6
+ # Relation name container
7
+ #
8
+ # This is a simple struct with two fields.
9
+ # It handles both relation registration name (i.e. Symbol) and dataset name.
10
+ # The reason we need it is a simplification of passing around these two objects.
11
+ # It is quite common to have a dataset named differently from a relation
12
+ # built on top if you are dealing with a legacy DB and often you need both
13
+ # to support things such as associations (rom-sql as an example).
14
+ #
15
+ # @api private
16
+ class Name
17
+ include Dry::Equalizer(:relation, :dataset)
18
+
19
+ # Coerce an object to a Name instance
20
+ #
21
+ # @return [ROM::Relation::Name]
22
+ #
23
+ # @api private
24
+ def self.[](*args)
25
+ cache.fetch_or_store(args.hash) do
26
+ relation, dataset, aliaz = args
27
+
28
+ if relation.is_a?(Name)
29
+ relation
30
+ else
31
+ new(relation, dataset, aliaz)
32
+ end
33
+ end
34
+ end
35
+
36
+ # @api private
37
+ def self.cache
38
+ @cache ||= Concurrent::Map.new
39
+ end
40
+
41
+ # Relation registration name
42
+ #
43
+ # @return [Symbol]
44
+ #
45
+ # @api private
46
+ attr_reader :relation
47
+
48
+ # Underlying dataset name
49
+ #
50
+ # @return [Symbol]
51
+ #
52
+ # @api private
53
+ attr_reader :dataset
54
+
55
+ attr_reader :aliaz
56
+
57
+ attr_reader :key
58
+
59
+ # @api private
60
+ def initialize(relation, dataset = relation, aliaz = nil)
61
+ @relation = relation
62
+ @dataset = dataset || relation
63
+ @key = aliaz || relation
64
+ @aliaz = aliaz
65
+ end
66
+
67
+ # @api private
68
+ def as(aliaz)
69
+ self.class[relation, dataset, aliaz]
70
+ end
71
+
72
+ # @api private
73
+ def aliased?
74
+ !aliaz.nil?
75
+ end
76
+
77
+ # Return relation name
78
+ #
79
+ # @return [String]
80
+ #
81
+ # @api private
82
+ def to_s
83
+ if aliaz
84
+ "#{relation} on #{dataset} as #{aliaz}"
85
+ elsif relation == dataset
86
+ relation.to_s
87
+ else
88
+ "#{relation} on #{dataset}"
89
+ end
90
+ end
91
+
92
+ # Alias for registration key implicitly called by ROM::Registry
93
+ #
94
+ # @return [Symbol]
95
+ #
96
+ # @api private
97
+ def to_sym
98
+ relation
99
+ end
100
+
101
+ # Return inspected relation
102
+ #
103
+ # @return [String]
104
+ #
105
+ # @api private
106
+ def inspect
107
+ "#{self.class.name}(#{to_s})"
108
+ end
109
+ end
110
+ end
111
+ end