rom 5.4.2 → 6.0.0.alpha1

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 (186) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -71
  3. data/LICENSE +1 -1
  4. data/README.md +7 -6
  5. data/lib/rom/array_dataset.rb +46 -0
  6. data/lib/rom/associations/abstract.rb +217 -0
  7. data/lib/rom/associations/definitions/abstract.rb +150 -0
  8. data/lib/rom/associations/definitions/many_to_many.rb +29 -0
  9. data/lib/rom/associations/definitions/many_to_one.rb +14 -0
  10. data/lib/rom/associations/definitions/one_to_many.rb +14 -0
  11. data/lib/rom/associations/definitions/one_to_one.rb +14 -0
  12. data/lib/rom/associations/definitions/one_to_one_through.rb +14 -0
  13. data/lib/rom/associations/definitions.rb +7 -0
  14. data/lib/rom/associations/many_to_many.rb +128 -0
  15. data/lib/rom/associations/many_to_one.rb +65 -0
  16. data/lib/rom/associations/one_to_many.rb +65 -0
  17. data/lib/rom/associations/one_to_one.rb +13 -0
  18. data/lib/rom/associations/one_to_one_through.rb +13 -0
  19. data/lib/rom/associations/through_identifier.rb +41 -0
  20. data/lib/rom/attribute.rb +425 -0
  21. data/lib/rom/auto_curry.rb +70 -0
  22. data/lib/rom/cache.rb +87 -0
  23. data/lib/rom/changeset/associated.rb +110 -0
  24. data/lib/rom/changeset/create.rb +18 -0
  25. data/lib/rom/changeset/delete.rb +15 -0
  26. data/lib/rom/changeset/extensions/relation.rb +26 -0
  27. data/lib/rom/changeset/pipe.rb +81 -0
  28. data/lib/rom/changeset/pipe_registry.rb +27 -0
  29. data/lib/rom/changeset/stateful.rb +285 -0
  30. data/lib/rom/changeset/update.rb +81 -0
  31. data/lib/rom/changeset.rb +185 -0
  32. data/lib/rom/command.rb +351 -0
  33. data/lib/rom/command_compiler.rb +201 -0
  34. data/lib/rom/command_proxy.rb +36 -0
  35. data/lib/rom/commands/class_interface.rb +236 -0
  36. data/lib/rom/commands/composite.rb +55 -0
  37. data/lib/rom/commands/create.rb +15 -0
  38. data/lib/rom/commands/delete.rb +16 -0
  39. data/lib/rom/commands/graph/class_interface.rb +64 -0
  40. data/lib/rom/commands/graph/input_evaluator.rb +94 -0
  41. data/lib/rom/commands/graph.rb +88 -0
  42. data/lib/rom/commands/lazy/create.rb +35 -0
  43. data/lib/rom/commands/lazy/delete.rb +39 -0
  44. data/lib/rom/commands/lazy/update.rb +46 -0
  45. data/lib/rom/commands/lazy.rb +106 -0
  46. data/lib/rom/commands/update.rb +16 -0
  47. data/lib/rom/commands.rb +5 -0
  48. data/lib/rom/compat/auto_registration.rb +115 -0
  49. data/lib/rom/compat/auto_registration_strategies/base.rb +29 -0
  50. data/lib/rom/compat/auto_registration_strategies/custom_namespace.rb +84 -0
  51. data/lib/rom/compat/auto_registration_strategies/no_namespace.rb +33 -0
  52. data/lib/rom/compat/auto_registration_strategies/with_namespace.rb +29 -0
  53. data/lib/rom/compat/command.rb +74 -0
  54. data/lib/rom/compat/components/dsl/schema.rb +130 -0
  55. data/lib/rom/compat/components.rb +91 -0
  56. data/lib/rom/compat/global.rb +17 -0
  57. data/lib/rom/compat/mapper.rb +22 -0
  58. data/lib/rom/compat/registries.rb +47 -0
  59. data/lib/rom/compat/relation.rb +40 -0
  60. data/lib/rom/compat/schema/dsl.rb +260 -0
  61. data/lib/rom/compat/setting_proxy.rb +44 -0
  62. data/lib/rom/compat/setup.rb +151 -0
  63. data/lib/rom/compat/transformer.rb +49 -0
  64. data/lib/rom/compat.rb +22 -0
  65. data/lib/rom/components/association.rb +26 -0
  66. data/lib/rom/components/command.rb +24 -0
  67. data/lib/rom/components/core.rb +148 -0
  68. data/lib/rom/components/dataset.rb +60 -0
  69. data/lib/rom/components/dsl/association.rb +47 -0
  70. data/lib/rom/components/dsl/command.rb +60 -0
  71. data/lib/rom/components/dsl/core.rb +126 -0
  72. data/lib/rom/components/dsl/dataset.rb +33 -0
  73. data/lib/rom/components/dsl/gateway.rb +14 -0
  74. data/lib/rom/components/dsl/mapper.rb +70 -0
  75. data/lib/rom/components/dsl/relation.rb +49 -0
  76. data/lib/rom/components/dsl/schema.rb +150 -0
  77. data/lib/rom/components/dsl/view.rb +82 -0
  78. data/lib/rom/components/dsl.rb +255 -0
  79. data/lib/rom/components/gateway.rb +50 -0
  80. data/lib/rom/components/mapper.rb +29 -0
  81. data/lib/rom/components/provider.rb +160 -0
  82. data/lib/rom/components/registry.rb +154 -0
  83. data/lib/rom/components/relation.rb +41 -0
  84. data/lib/rom/components/schema.rb +61 -0
  85. data/lib/rom/components/view.rb +55 -0
  86. data/lib/rom/components.rb +55 -0
  87. data/lib/rom/configuration_dsl.rb +4 -0
  88. data/lib/rom/constants.rb +135 -0
  89. data/lib/rom/container.rb +182 -0
  90. data/lib/rom/core.rb +125 -0
  91. data/lib/rom/data_proxy.rb +97 -0
  92. data/lib/rom/enumerable_dataset.rb +70 -0
  93. data/lib/rom/gateway.rb +232 -0
  94. data/lib/rom/global.rb +56 -0
  95. data/lib/rom/header/attribute.rb +190 -0
  96. data/lib/rom/header.rb +198 -0
  97. data/lib/rom/inferrer.rb +55 -0
  98. data/lib/rom/initializer.rb +80 -0
  99. data/lib/rom/lint/enumerable_dataset.rb +56 -0
  100. data/lib/rom/lint/gateway.rb +120 -0
  101. data/lib/rom/lint/linter.rb +79 -0
  102. data/lib/rom/lint/spec.rb +22 -0
  103. data/lib/rom/lint/test.rb +98 -0
  104. data/lib/rom/loader.rb +161 -0
  105. data/lib/rom/mapper/attribute_dsl.rb +480 -0
  106. data/lib/rom/mapper/dsl.rb +107 -0
  107. data/lib/rom/mapper/model_dsl.rb +61 -0
  108. data/lib/rom/mapper.rb +99 -0
  109. data/lib/rom/mapper_compiler.rb +84 -0
  110. data/lib/rom/memory/associations/many_to_many.rb +12 -0
  111. data/lib/rom/memory/associations/many_to_one.rb +12 -0
  112. data/lib/rom/memory/associations/one_to_many.rb +12 -0
  113. data/lib/rom/memory/associations/one_to_one.rb +12 -0
  114. data/lib/rom/memory/associations.rb +6 -0
  115. data/lib/rom/memory/commands.rb +60 -0
  116. data/lib/rom/memory/dataset.rb +127 -0
  117. data/lib/rom/memory/gateway.rb +66 -0
  118. data/lib/rom/memory/mapper_compiler.rb +10 -0
  119. data/lib/rom/memory/relation.rb +91 -0
  120. data/lib/rom/memory/schema.rb +32 -0
  121. data/lib/rom/memory/storage.rb +61 -0
  122. data/lib/rom/memory/types.rb +11 -0
  123. data/lib/rom/memory.rb +7 -0
  124. data/lib/rom/model_builder.rb +103 -0
  125. data/lib/rom/open_struct.rb +112 -0
  126. data/lib/rom/pipeline.rb +111 -0
  127. data/lib/rom/plugin.rb +130 -0
  128. data/lib/rom/plugins/class_methods.rb +37 -0
  129. data/lib/rom/plugins/command/schema.rb +45 -0
  130. data/lib/rom/plugins/command/timestamps.rb +149 -0
  131. data/lib/rom/plugins/dsl.rb +53 -0
  132. data/lib/rom/plugins/relation/changeset.rb +97 -0
  133. data/lib/rom/plugins/relation/instrumentation.rb +66 -0
  134. data/lib/rom/plugins/relation/registry_reader.rb +36 -0
  135. data/lib/rom/plugins/schema/timestamps.rb +59 -0
  136. data/lib/rom/plugins.rb +100 -0
  137. data/lib/rom/processor/composer.rb +37 -0
  138. data/lib/rom/processor/transformer.rb +415 -0
  139. data/lib/rom/processor.rb +30 -0
  140. data/lib/rom/registries/associations.rb +26 -0
  141. data/lib/rom/registries/commands.rb +11 -0
  142. data/lib/rom/registries/container.rb +12 -0
  143. data/lib/rom/registries/datasets.rb +21 -0
  144. data/lib/rom/registries/gateways.rb +8 -0
  145. data/lib/rom/registries/mappers.rb +21 -0
  146. data/lib/rom/registries/nestable.rb +32 -0
  147. data/lib/rom/registries/relations.rb +8 -0
  148. data/lib/rom/registries/root.rb +203 -0
  149. data/lib/rom/registries/schemas.rb +44 -0
  150. data/lib/rom/registries/views.rb +11 -0
  151. data/lib/rom/relation/class_interface.rb +61 -0
  152. data/lib/rom/relation/combined.rb +160 -0
  153. data/lib/rom/relation/commands.rb +65 -0
  154. data/lib/rom/relation/composite.rb +53 -0
  155. data/lib/rom/relation/curried.rb +129 -0
  156. data/lib/rom/relation/graph.rb +107 -0
  157. data/lib/rom/relation/loaded.rb +136 -0
  158. data/lib/rom/relation/materializable.rb +62 -0
  159. data/lib/rom/relation/name.rb +122 -0
  160. data/lib/rom/relation/wrap.rb +64 -0
  161. data/lib/rom/relation.rb +625 -0
  162. data/lib/rom/repository/class_interface.rb +162 -0
  163. data/lib/rom/repository/relation_reader.rb +48 -0
  164. data/lib/rom/repository/root.rb +75 -0
  165. data/lib/rom/repository/session.rb +60 -0
  166. data/lib/rom/repository.rb +179 -0
  167. data/lib/rom/schema/associations_dsl.rb +222 -0
  168. data/lib/rom/schema/inferrer.rb +106 -0
  169. data/lib/rom/schema.rb +471 -0
  170. data/lib/rom/settings.rb +141 -0
  171. data/lib/rom/setup.rb +297 -0
  172. data/lib/rom/struct.rb +99 -0
  173. data/lib/rom/struct_compiler.rb +114 -0
  174. data/lib/rom/support/configurable.rb +213 -0
  175. data/lib/rom/support/inflector.rb +31 -0
  176. data/lib/rom/support/memoizable.rb +61 -0
  177. data/lib/rom/support/notifications.rb +238 -0
  178. data/lib/rom/transaction.rb +26 -0
  179. data/lib/rom/transformer.rb +46 -0
  180. data/lib/rom/types.rb +74 -0
  181. data/lib/rom/version.rb +1 -1
  182. data/lib/rom-changeset.rb +4 -0
  183. data/lib/rom-core.rb +3 -0
  184. data/lib/rom-repository.rb +4 -0
  185. data/lib/rom.rb +3 -3
  186. metadata +302 -23
data/lib/rom/setup.rb ADDED
@@ -0,0 +1,297 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+
5
+ require "rom/support/inflector"
6
+
7
+ require "rom/core"
8
+ require "rom/components/provider"
9
+
10
+ require "rom/open_struct"
11
+ require "rom/constants"
12
+ require "rom/gateway"
13
+ require "rom/loader"
14
+
15
+ module ROM
16
+ # @api public
17
+ class Setup
18
+ include ROM::Provider(
19
+ :gateway,
20
+ :dataset,
21
+ :schema,
22
+ :relation,
23
+ :association,
24
+ :mapper,
25
+ :command,
26
+ :plugin,
27
+ type: :component
28
+ )
29
+
30
+ DEFAULT_CLASS_NAMESPACE = "ROM"
31
+
32
+ CLASS_NAME_INFERRERS = {
33
+ relation: -> (name, type:, inflector:, class_namespace:, **) {
34
+ [
35
+ class_namespace,
36
+ inflector.pluralize(inflector.camelize(type)),
37
+ inflector.camelize(name)
38
+ ].compact.join("::")
39
+ },
40
+ command: -> (name, inflector:, adapter:, command_type:, class_namespace:, **) {
41
+ [
42
+ class_namespace,
43
+ inflector.classify(adapter),
44
+ "Commands",
45
+ "#{command_type}[#{inflector.pluralize(inflector.classify(name))}]"
46
+ ].join("::")
47
+ }
48
+ }.freeze
49
+
50
+ DEFAULT_CLASS_NAME_INFERRER = -> (name, type:, **opts) {
51
+ CLASS_NAME_INFERRERS.fetch(type).(name, type: type, **opts)
52
+ }.freeze
53
+
54
+ # Global defaults
55
+ setting :inflector, default: Inflector, reader: true
56
+
57
+ setting :gateways, default: EMPTY_HASH
58
+
59
+ setting :class_name_inferrer, default: DEFAULT_CLASS_NAME_INFERRER, reader: true
60
+
61
+ setting :class_namespace, default: DEFAULT_CLASS_NAMESPACE, reader: true
62
+
63
+ setting :auto_register do
64
+ setting :root_directory
65
+ setting :auto_load
66
+ setting :namespace
67
+ setting :component_dirs, default: {
68
+ relations: :relations, mappers: :mappers, commands: :commands
69
+ }
70
+ setting :inflector, default: Inflector
71
+ end
72
+
73
+ # Initialize a new configuration
74
+ #
75
+ # @return [Configuration]
76
+ #
77
+ # @api private
78
+ def initialize(...)
79
+ super()
80
+ configure(...)
81
+ end
82
+
83
+ # @return [Registry::Root] Setup component registry
84
+ # @api private
85
+ def registry
86
+ @registry ||=
87
+ begin
88
+ options = registry_options
89
+ options[:loader] = loader if config.auto_register.auto_load
90
+
91
+ super(**options)
92
+ end
93
+ end
94
+
95
+ # This is called internally when you pass a block to ROM.container
96
+ #
97
+ # @api private
98
+ def configure(*args)
99
+ # Load config from the arguments passed to the constructor.
100
+ # This *may* override defaults and it's a feature.
101
+ infer_config(*args) unless args.empty?
102
+
103
+ # Load adapters explicitly here to ensure their plugins are present for later use
104
+ load_adapters
105
+
106
+ # Allow customizations now
107
+ yield(self, config) if block_given?
108
+
109
+ # Register gateway components based on current config
110
+ register_gateways
111
+
112
+ self
113
+ end
114
+
115
+ # Enable auto-registration
116
+ #
117
+ # @param [String, Pathname] directory The root path to components
118
+ # @param [Hash] options
119
+ # @option options [Boolean,String] :namespace Toggle root namespace
120
+ # @option options [Boolean] :auto_load Toggle auto-loading via Zeitwerk
121
+ #
122
+ # @return [Configuration]
123
+ #
124
+ # @api public
125
+ def auto_register(directory, **options)
126
+ config.auto_register.update(root_directory: directory, **options)
127
+ self
128
+ end
129
+
130
+ # @api private
131
+ def register_constant(type, constant)
132
+ if config.key?(constant.config.component.type)
133
+ parent_config = config[constant.config.component.type]
134
+ const_config = constant.config.component
135
+
136
+ const_config.inherit!(parent_config).join!(parent_config)
137
+
138
+ # TODO: make this work with all components
139
+ if const_config.key?(:infer_id_from_class) && const_config.infer_id_from_class
140
+ const_config.id = const_config.inflector.component_id(constant.name)&.to_sym
141
+ end
142
+ end
143
+
144
+ components.add(type, constant: constant, config: constant.config.component)
145
+ end
146
+
147
+ # Register relation class(es) explicitly
148
+ #
149
+ # @param [Array<Relation>] *klasses One or more relation classes
150
+ #
151
+ # @api public
152
+ def register_relation(*klasses)
153
+ klasses.each do |klass|
154
+ register_constant(:relations, klass)
155
+ end
156
+
157
+ components.relations
158
+ end
159
+
160
+ # Register mapper class(es) explicitly
161
+ #
162
+ # @param [Array] *klasses One or more mapper classes
163
+ #
164
+ # @api public
165
+ def register_mapper(*klasses)
166
+ klasses.each do |klass|
167
+ register_constant(:mappers, klass)
168
+ end
169
+
170
+ components[:mappers]
171
+ end
172
+
173
+ # Register command class(es) explicitly
174
+ #
175
+ # @param [Array] *klasses One or more command classes
176
+ #
177
+ # @api public
178
+ def register_command(*klasses)
179
+ klasses.each do |klass|
180
+ register_constant(:commands, klass)
181
+ end
182
+
183
+ components.commands
184
+ end
185
+
186
+ # This is called automatically in configure block
187
+ #
188
+ # After finalization it is no longer possible to alter the configuration
189
+ #
190
+ # @api private
191
+ def finalize
192
+ # No more config changes allowed
193
+ config.freeze
194
+ yield if block_given?
195
+ loader.() if config.auto_register.key?(:root_directory)
196
+ registry
197
+ end
198
+
199
+ # Apply a plugin to the configuration
200
+ #
201
+ # @param [Mixed] plugin The plugin identifier, usually a Symbol
202
+ # @param [Hash] options Plugin options
203
+ #
204
+ # @return [Configuration]
205
+ #
206
+ # @api public
207
+ def use(plugin, options = {})
208
+ case plugin
209
+ when Array then plugin.each { |p| use(p) }
210
+ when Hash then plugin.to_a.each { |p| use(*p) }
211
+ else
212
+ plugin_registry[:configuration].fetch(plugin).apply_to(self, options)
213
+ end
214
+
215
+ self
216
+ end
217
+
218
+ private
219
+
220
+ # @api private
221
+ def plugin_registry
222
+ ROM.plugins
223
+ end
224
+
225
+ # This register gateway components based on the configuration
226
+ #
227
+ # It is private unlike the rest of register_ methods because
228
+ # it's called automatically doing configuration phase
229
+ #
230
+ # @api private
231
+ def register_gateways
232
+ config.gateways.each do |id, gateway_config|
233
+ base = gateway_config.to_h
234
+ keys = base.keys - config.gateway.keys
235
+ args = base[:args] || EMPTY_ARRAY
236
+ opts = keys.zip(base.values_at(*keys)).to_h
237
+
238
+ gateway(id, **base, args: args, opts: opts)
239
+ end
240
+ end
241
+
242
+ # This infers config using arguments passed to the constructor
243
+ #
244
+ # @api private
245
+ def infer_config(*args)
246
+ config.gateways = ROM::OpenStruct.new
247
+
248
+ gateways_config = args.first.is_a?(Hash) ? args.first : {default: args}
249
+
250
+ gateways_config.each do |name, value|
251
+ args = Array(value)
252
+
253
+ adapter, *rest = args
254
+
255
+ options =
256
+ if rest.size > 1 && rest.last.is_a?(Hash)
257
+ {adapter: adapter, args: rest[0..], **rest.last}
258
+ else
259
+ options = rest.first.is_a?(Hash) ? rest.first : {args: rest.flatten(1)}
260
+ {adapter: adapter, **options}
261
+ end
262
+
263
+ config.gateways[name] = ROM::OpenStruct.new(options)
264
+ end
265
+ end
266
+
267
+ # @api private
268
+ def load_adapters
269
+ config.gateways.map { |key| config.gateways[key] }.map(&:adapter).uniq do |adapter|
270
+ Gateway.class_from_symbol(adapter)
271
+ rescue AdapterLoadError
272
+ # TODO: we probably want to remove this. It's perfectly fine to have an adapter
273
+ # defined in another location. Auto-require was done for convenience but
274
+ # making it mandatory to have that file seems odd now.
275
+ end
276
+ end
277
+
278
+ # @api private
279
+ def plugins
280
+ config.component.plugins
281
+ end
282
+
283
+ # @api private
284
+ def loader
285
+ @loader ||= Loader.new(
286
+ config.auto_register.root_directory,
287
+ components: components,
288
+ **config.auto_register
289
+ )
290
+ end
291
+
292
+ # @api private
293
+ def registry_options
294
+ {config: config}
295
+ end
296
+ end
297
+ end
data/lib/rom/struct.rb ADDED
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/struct"
4
+
5
+ module ROM
6
+ # Simple data-struct class
7
+ #
8
+ # ROM structs are plain data structures loaded by repositories.
9
+ # They implement Hash protocol which means that they can be used
10
+ # in places where Hash-like objects are supported.
11
+ #
12
+ # Repositories define subclasses of ROM::Struct automatically, they are
13
+ # defined in the ROM::Struct namespace by default, but you set it up
14
+ # to use your namespace/module as well.
15
+ #
16
+ # Structs are based on dry-struct gem, they include `schema` with detailed information
17
+ # about attribute types returned from relations, thus can be introspected to build
18
+ # additional functionality when desired.
19
+ #
20
+ # There is a caveat you should know about when working with structs. Struct classes
21
+ # have names but at the same time they're anonymous, i.e. you can't get the User struct class
22
+ # with ROM::Struct::User. ROM will create as many struct classes for User as needed,
23
+ # they all will have the same name and ROM::Struct::User will be the common parent class for
24
+ # them. Combined with the ability to provide your own namespace for structs this enables to
25
+ # pre-define the parent class.
26
+ #
27
+ # @example accessing relation struct model
28
+ # rom = ROM.setup(:sql, 'sqlite::memory') do |conf|
29
+ # conf.default.create_table(:users) do
30
+ # primary_key :id
31
+ # column :name, String
32
+ # end
33
+ # end
34
+ #
35
+ # class UserRepo < ROM::Repository[:users]
36
+ # end
37
+ #
38
+ # user_repo = UserRepo.new(rom)
39
+ #
40
+ # # get auto-generated User struct
41
+ # model = user_repo.users.mapper.model
42
+ # # => ROM::Struct::User
43
+ #
44
+ # # see struct's schema attributes
45
+ #
46
+ # # model.schema.key(:id)
47
+ # # => #<Dry::Types[id: Nominal<Integer meta={primary_key: true, source: :users}>]>
48
+ #
49
+ # model.schema[:name]
50
+ # # => #<Dry::Types[name: Sum<Nominal<NilClass> | Nominal<String> meta={source: :users}>]>
51
+ #
52
+ # @example passing a namespace with an existing parent class
53
+ # module Entities
54
+ # class User < ROM::Struct
55
+ # def upcased_name
56
+ # name.upcase
57
+ # end
58
+ # end
59
+ # end
60
+ #
61
+ # class UserRepo < ROM::Repository[:users]
62
+ # struct_namespace Entities
63
+ # end
64
+ #
65
+ # user_repo = UserRepo.new(rom)
66
+ # user = user_repo.users.by_pk(1).one!
67
+ # user.name # => "Jane"
68
+ # user.upcased_name # => "JANE"
69
+ #
70
+ # @see http://dry-rb.org/gems/dry-struct dry-struct
71
+ # @see http://dry-rb.org/gems/dry-types dry-types
72
+ #
73
+ # @api public
74
+ class Struct < Dry::Struct
75
+ MissingAttribute = Class.new(NameError)
76
+
77
+ # Return attribute value
78
+ #
79
+ # @param [Symbol] name The attribute name
80
+ #
81
+ # @api public
82
+ def fetch(name)
83
+ __send__(name)
84
+ end
85
+
86
+ # @api private
87
+ def respond_to_missing?(*)
88
+ super
89
+ end
90
+
91
+ private
92
+
93
+ def method_missing(*)
94
+ super
95
+ rescue NameError => e
96
+ raise MissingAttribute, "#{e.message} (attribute not loaded?)"
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rom/support/inflector"
4
+ require "dry/core/class_builder"
5
+ require "dry/types/compiler"
6
+
7
+ require "rom/initializer"
8
+ require "rom/types"
9
+ require "rom/cache"
10
+ require "rom/struct"
11
+ require "rom/open_struct"
12
+ require "rom/attribute"
13
+
14
+ module ROM
15
+ # @api private
16
+ class StructCompiler < Dry::Types::Compiler
17
+ extend Initializer
18
+
19
+ param :registry, default: -> { Dry::Types }
20
+
21
+ option :cache, default: -> { Cache.new }
22
+
23
+ option :inflector, reader: true, default: -> { Inflector }
24
+
25
+ # @api private
26
+ def initialize(*)
27
+ super
28
+ @cache = cache.namespaced(:structs)
29
+ end
30
+ ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true)
31
+
32
+ # Build a struct class based on relation header ast
33
+ #
34
+ # @api private
35
+ def call(*args)
36
+ cache.fetch_or_store(args) do
37
+ name, header, ns = args
38
+ attributes = header.map(&method(:visit)).compact
39
+
40
+ if attributes.empty?
41
+ ROM::OpenStruct
42
+ else
43
+ build_class(name, ROM::Struct, ns) do |klass|
44
+ klass.attributes(attributes.to_h)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ alias_method :[], :call
50
+
51
+ private
52
+
53
+ # @api private
54
+ def visit_relation(node)
55
+ _, header, meta = node
56
+ name = meta[:combine_name] || meta[:alias]
57
+ namespace = meta.fetch(:struct_namespace)
58
+
59
+ model = meta[:model] || call(name, header, namespace)
60
+
61
+ member =
62
+ if model < Dry::Struct
63
+ model
64
+ else
65
+ Dry::Types::Nominal.new(model).constructor(&model.method(:new))
66
+ end
67
+
68
+ if meta[:combine_type] == :many
69
+ [name, Types::Array.of(member)]
70
+ else
71
+ [name, member.optional]
72
+ end
73
+ end
74
+
75
+ # @api private
76
+ def visit_attribute(node)
77
+ name, type, meta = node
78
+
79
+ [meta[:alias] && !meta[:wrapped] ? meta[:alias] : name, visit(type).meta(meta)]
80
+ end
81
+
82
+ # @api private
83
+ def visit_constructor(node)
84
+ definition, * = node
85
+
86
+ visit(definition)
87
+ end
88
+
89
+ # @api private
90
+ def visit_constrained(node)
91
+ definition, = node
92
+
93
+ visit(definition)
94
+ end
95
+
96
+ # @api private
97
+ def visit_enum(node)
98
+ type_node, * = node
99
+ visit(type_node)
100
+ end
101
+
102
+ # @api private
103
+ def build_class(name, parent, ns, &block)
104
+ Dry::Core::ClassBuilder
105
+ .new(name: class_name(name), parent: parent, namespace: ns)
106
+ .call(&block)
107
+ end
108
+
109
+ # @api private
110
+ def class_name(name)
111
+ inflector.classify(name)
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/configurable"
4
+ require "rom/constants"
5
+ require "rom/types"
6
+
7
+ module ROM
8
+ # Component settings API
9
+ #
10
+ # @see https://dry-rb.org/gems/dry-configurable
11
+ #
12
+ # @api public
13
+ module Configurable
14
+ # @api private
15
+ def self.included(klass)
16
+ super
17
+
18
+ klass.class_eval do
19
+ include(Dry::Configurable)
20
+
21
+ class << self
22
+ prepend(Methods)
23
+ prepend(Methods::DSL)
24
+ end
25
+ end
26
+ end
27
+
28
+ # @api private
29
+ def self.extended(klass)
30
+ super
31
+
32
+ klass.class_eval do
33
+ extend(Dry::Configurable)
34
+
35
+ class << self
36
+ prepend(Methods)
37
+ prepend(Methods::DSL)
38
+ end
39
+ end
40
+ end
41
+
42
+ # @api private
43
+ module ConfigMethods
44
+ include Enumerable
45
+
46
+ # @api private
47
+ def each(&block)
48
+ values.each(&block)
49
+ end
50
+
51
+ # @api private
52
+ def inherit!(other)
53
+ update(inherit(other))
54
+ end
55
+
56
+ # @api private
57
+ def inherit(other)
58
+ hash = values.merge(other.to_h.slice(*keys)) { |key, left, right|
59
+ if _constructors[key].is_a?(Constructors::Inherit)
60
+ _constructors[key].(left, right)
61
+ else
62
+ left.nil? ? right : left
63
+ end
64
+ }
65
+ merge(hash)
66
+ end
67
+
68
+ # @api private
69
+ def join!(other, direction = :left)
70
+ update(join(other, direction))
71
+ end
72
+
73
+ # @api private
74
+ def join(other, direction = :left)
75
+ hash = values.merge(other.to_h.slice(*keys)) { |key, left, right|
76
+ if _constructors[key].is_a?(Constructors::Join)
77
+ _constructors[key].(left, right, direction)
78
+ else
79
+ direction == :left ? left : right
80
+ end
81
+ }
82
+ merge(hash)
83
+ end
84
+
85
+ # @api private
86
+ def merge(other)
87
+ dup.update(values.merge(other))
88
+ end
89
+
90
+ # @api private
91
+ def empty?
92
+ values.compact.empty?
93
+ end
94
+
95
+ # @api private
96
+ def key?(key)
97
+ _settings.key?(key)
98
+ end
99
+
100
+ # @api private
101
+ def keys
102
+ _settings.keys
103
+ end
104
+
105
+ # @api private
106
+ def fetch(...)
107
+ values.fetch(...)
108
+ end
109
+
110
+ # @api private
111
+ def to_h
112
+ values
113
+ end
114
+ alias_method :to_hash, :to_h
115
+
116
+ # @api private
117
+ def freeze
118
+ _constructors
119
+ super
120
+ end
121
+
122
+ # @api private
123
+ def _constructors
124
+ @_constructors ||= _settings.map { |setting| [setting.name, setting.constructor] }.to_h
125
+ end
126
+ end
127
+
128
+ module Constructors
129
+ Default = ::Struct.new(:name) do
130
+ def call(*args)
131
+ return if args.compact.empty?
132
+
133
+ block_given? ? yield(*args) : args.first
134
+ end
135
+ alias_method :[], :call
136
+ end
137
+
138
+ class Inherit < Default
139
+ def call(*args)
140
+ super { |left, right|
141
+ case left
142
+ when nil then right
143
+ when Hash then right.merge(left)
144
+ when Array then (right.map(&:dup) + left.map(&:dup)).uniq
145
+ else
146
+ left
147
+ end
148
+ }
149
+ end
150
+ end
151
+
152
+ class Join < Default
153
+ def call(*args)
154
+ super { |left, right, direction|
155
+ case direction
156
+ when :left then [right, left]
157
+ when :right then [left, right]
158
+ else
159
+ raise ArgumentError, "+#{direction}+ direction is not supported"
160
+ end.compact.join(".")
161
+ }
162
+ end
163
+ end
164
+ end
165
+
166
+ # @api public
167
+ module Methods
168
+ # @api public
169
+ def configure(namespace = nil, &block)
170
+ if namespace
171
+ super(&nil)
172
+ block.(config[namespace])
173
+ else
174
+ super(&block)
175
+ end
176
+ self
177
+ end
178
+
179
+ # @api private
180
+ module DSL
181
+ # @api private
182
+ def setting(name, import: nil, inherit: false, join: false, default: Undefined, **options)
183
+ if import
184
+ setting_import(name, import, **options)
185
+ elsif inherit
186
+ setting(name, default: default, constructor: Constructors::Inherit.new(name), **options)
187
+ elsif join
188
+ setting(name, default: default, constructor: Constructors::Join.new(name), **options)
189
+ else
190
+ super(name, default: default, **options)
191
+ end
192
+ end
193
+
194
+ # @api private
195
+ def setting_import(name, setting)
196
+ # TODO: it would be great if this could just be import.with(name: name)
197
+ settings << setting.class.new(
198
+ name, input: setting.input, default: setting.default, **setting.options
199
+ )
200
+ end
201
+
202
+ # @api private
203
+ def settings
204
+ _settings
205
+ end
206
+ end
207
+ end
208
+
209
+ # TODO: either extend functionality of dry-configurable or don't use it here after all
210
+ Dry::Configurable::DSL.prepend(Methods::DSL)
211
+ Dry::Configurable::Config.prepend(ConfigMethods)
212
+ end
213
+ end