rom-core 4.0.0.beta1

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