hanami 2.1.0 → 2.2.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -6
  3. data/FEATURES.md +1 -1
  4. data/README.md +7 -7
  5. data/hanami.gemspec +6 -6
  6. data/lib/hanami/app.rb +6 -2
  7. data/lib/hanami/config/actions.rb +1 -1
  8. data/lib/hanami/config/assets.rb +1 -1
  9. data/lib/hanami/config/db.rb +33 -0
  10. data/lib/hanami/config/logger.rb +2 -2
  11. data/lib/hanami/config.rb +37 -10
  12. data/lib/hanami/extensions/db/repo.rb +103 -0
  13. data/lib/hanami/extensions/view/context.rb +1 -1
  14. data/lib/hanami/extensions/view/part.rb +1 -1
  15. data/lib/hanami/extensions/view/slice_configured_helpers.rb +1 -1
  16. data/lib/hanami/extensions.rb +4 -0
  17. data/lib/hanami/helpers/assets_helper.rb +5 -5
  18. data/lib/hanami/helpers/form_helper/form_builder.rb +4 -6
  19. data/lib/hanami/middleware/public_errors_app.rb +2 -2
  20. data/lib/hanami/provider_registrar.rb +26 -0
  21. data/lib/hanami/providers/assets.rb +2 -20
  22. data/lib/hanami/providers/db/adapter.rb +68 -0
  23. data/lib/hanami/providers/db/adapters.rb +44 -0
  24. data/lib/hanami/providers/db/config.rb +66 -0
  25. data/lib/hanami/providers/db/sql_adapter.rb +80 -0
  26. data/lib/hanami/providers/db.rb +203 -0
  27. data/lib/hanami/providers/db_logging.rb +22 -0
  28. data/lib/hanami/providers/rack.rb +3 -3
  29. data/lib/hanami/providers/relations.rb +31 -0
  30. data/lib/hanami/providers/routes.rb +1 -13
  31. data/lib/hanami/rake_tasks.rb +9 -8
  32. data/lib/hanami/settings.rb +3 -3
  33. data/lib/hanami/slice.rb +90 -10
  34. data/lib/hanami/version.rb +1 -1
  35. data/lib/hanami/web/rack_logger.rb +3 -3
  36. data/lib/hanami.rb +3 -0
  37. data/spec/integration/container/provider_environment_spec.rb +52 -0
  38. data/spec/integration/db/auto_registration_spec.rb +39 -0
  39. data/spec/integration/db/db_inflector_spec.rb +57 -0
  40. data/spec/integration/db/db_slices_spec.rb +327 -0
  41. data/spec/integration/db/db_spec.rb +220 -0
  42. data/spec/integration/db/logging_spec.rb +238 -0
  43. data/spec/integration/db/provider_config_spec.rb +88 -0
  44. data/spec/integration/db/provider_spec.rb +35 -0
  45. data/spec/integration/db/repo_spec.rb +215 -0
  46. data/spec/integration/db/slices_importing_from_parent.rb +130 -0
  47. data/spec/integration/slices/slice_configuration_spec.rb +4 -4
  48. data/spec/integration/view/config/template_spec.rb +1 -1
  49. data/spec/integration/view/context/request_spec.rb +1 -1
  50. data/spec/support/app_integration.rb +3 -0
  51. data/spec/unit/hanami/config/db_spec.rb +38 -0
  52. data/spec/unit/hanami/config/router_spec.rb +1 -1
  53. data/spec/unit/hanami/helpers/form_helper_spec.rb +33 -2
  54. data/spec/unit/hanami/providers/db/config/default_config_spec.rb +107 -0
  55. data/spec/unit/hanami/providers/db/config_spec.rb +206 -0
  56. data/spec/unit/hanami/slice_spec.rb +33 -1
  57. data/spec/unit/hanami/version_spec.rb +1 -1
  58. metadata +62 -20
@@ -10,18 +10,6 @@ module Hanami
10
10
  # @api private
11
11
  # @since 2.0.0
12
12
  class Assets < Dry::System::Provider::Source
13
- # @api private
14
- def self.for_slice(slice)
15
- Class.new(self) do |klass|
16
- klass.instance_variable_set(:@slice, slice)
17
- end
18
- end
19
-
20
- # @api private
21
- def self.slice
22
- @slice || Hanami.app
23
- end
24
-
25
13
  # @api private
26
14
  def prepare
27
15
  require "hanami/assets"
@@ -29,18 +17,12 @@ module Hanami
29
17
 
30
18
  # @api private
31
19
  def start
32
- root = slice.app.root.join("public", "assets", Hanami::Assets.public_assets_dir(slice).to_s)
20
+ root = target.app.root.join("public", "assets", Hanami::Assets.public_assets_dir(target).to_s)
33
21
 
34
- assets = Hanami::Assets.new(config: slice.config.assets, root: root)
22
+ assets = Hanami::Assets.new(config: target.config.assets, root: root)
35
23
 
36
24
  register(:assets, assets)
37
25
  end
38
-
39
- private
40
-
41
- def slice
42
- self.class.slice
43
- end
44
26
  end
45
27
  end
46
28
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/configurable"
4
+
5
+ module Hanami
6
+ module Providers
7
+ class DB < Dry::System::Provider::Source
8
+ # @api public
9
+ # @since 2.2.0
10
+ class Adapter
11
+ include Dry::Configurable
12
+
13
+ # @api public
14
+ # @since 2.2.0
15
+ setting :plugins, mutable: true
16
+
17
+ # @api private
18
+ def initialize(...)
19
+ @skip_defaults = Hash.new(false)
20
+ end
21
+
22
+ # @api public
23
+ # @since 2.2.0
24
+ def skip_defaults(setting_name = nil)
25
+ @skip_defaults[setting_name] = true
26
+ end
27
+
28
+ # @api private
29
+ private def skip_defaults?(setting_name = nil)
30
+ @skip_defaults[setting_name]
31
+ end
32
+
33
+ # @api private
34
+ def configure_for_database(database_url)
35
+ end
36
+
37
+ # @api public
38
+ # @since 2.2.0
39
+ def plugin(**plugin_spec, &config_block)
40
+ plugins << [plugin_spec, config_block]
41
+ end
42
+
43
+ # @api public
44
+ # @since 2.2.0
45
+ def plugins
46
+ config.plugins ||= []
47
+ end
48
+
49
+ # @api private
50
+ def gateway_cache_keys
51
+ gateway_options
52
+ end
53
+
54
+ # @api private
55
+ def gateway_options
56
+ {}
57
+ end
58
+
59
+ # @api public
60
+ # @since 2.2.0
61
+ def clear
62
+ config.plugins = nil
63
+ self
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Providers
5
+ class DB < Dry::System::Provider::Source
6
+ # @api public
7
+ # @since 2.2.0
8
+ class Adapters
9
+ # @api private
10
+ # @since 2.2.0
11
+ ADAPTER_CLASSES = Hash.new(Adapter).update(
12
+ sql: SQLAdapter
13
+ ).freeze
14
+ private_constant :ADAPTER_CLASSES
15
+
16
+ extend Forwardable
17
+
18
+ def_delegators :adapters, :[], :[]=, :each, :to_h
19
+
20
+ # @api private
21
+ # @since 2.2.0
22
+ attr_reader :adapters
23
+
24
+ # @api private
25
+ # @since 2.2.0
26
+ def initialize
27
+ @adapters = Hash.new do |hsh, key|
28
+ hsh[key] = ADAPTER_CLASSES[key].new
29
+ end
30
+ end
31
+
32
+ # @api private
33
+ # @since 2.2.0
34
+ def initialize_copy(source)
35
+ @adapters = source.adapters.dup
36
+
37
+ source.adapters.each do |key, val|
38
+ @adapters[key] = val.dup
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core"
4
+
5
+ module Hanami
6
+ module Providers
7
+ class DB < Dry::System::Provider::Source
8
+ # @api public
9
+ # @since 2.2.0
10
+ class Config < Dry::Configurable::Config
11
+ include Dry::Core::Constants
12
+
13
+ # @api public
14
+ # @since 2.2.0
15
+ def adapter_name
16
+ self[:adapter]
17
+ end
18
+
19
+ # @api public
20
+ # @since 2.2.0
21
+ def adapter(name = Undefined)
22
+ return adapter_name if name.eql?(Undefined)
23
+
24
+ adapter = (adapters[name] ||= Adapter.new)
25
+ yield adapter if block_given?
26
+ adapter
27
+ end
28
+
29
+ # @api public
30
+ # @since 2.2.0
31
+ def any_adapter
32
+ adapter = (adapters[nil] ||= Adapter.new)
33
+ yield adapter if block_given?
34
+ adapter
35
+ end
36
+
37
+ # @api private
38
+ # @since 2.2.0
39
+ def gateway_cache_keys
40
+ adapters[adapter_name].gateway_cache_keys
41
+ end
42
+
43
+ # @api private
44
+ # @since 2.2.0
45
+ def gateway_options
46
+ adapters[adapter_name].gateway_options
47
+ end
48
+
49
+ # @api public
50
+ # @since 2.2.0
51
+ def each_plugin
52
+ universal_plugins = adapters[nil].plugins
53
+ adapter_plugins = adapters[adapter_name].plugins
54
+
55
+ plugins = universal_plugins + adapter_plugins
56
+
57
+ return to_enum(__method__) unless block_given?
58
+
59
+ plugins.each do |plugin_spec, config_block|
60
+ yield plugin_spec, config_block
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Providers
5
+ class DB < Dry::System::Provider::Source
6
+ # @api public
7
+ # @since 2.2.0
8
+ class SQLAdapter < Adapter
9
+ # @api public
10
+ # @since 2.2.0
11
+ setting :extensions, mutable: true
12
+
13
+ # @api public
14
+ # @since 2.2.0
15
+ def extension(*extensions)
16
+ self.extensions.concat(extensions)
17
+ end
18
+
19
+ # @api public
20
+ # @since 2.2.0
21
+ def extensions
22
+ config.extensions ||= []
23
+ end
24
+
25
+ # @api private
26
+ def configure_for_database(database_url)
27
+ return if skip_defaults?
28
+
29
+ configure_plugins
30
+ configure_extensions(database_url)
31
+ end
32
+
33
+ # @api private
34
+ private def configure_plugins
35
+ return if skip_defaults?(:plugins)
36
+
37
+ plugin relations: :instrumentation do |plugin|
38
+ plugin.notifications = target["notifications"]
39
+ end
40
+
41
+ plugin relations: :auto_restrictions
42
+ end
43
+
44
+ # @api private
45
+ private def configure_extensions(database_url)
46
+ return if skip_defaults?(:extensions)
47
+
48
+ # Extensions for all SQL databases
49
+ extension(
50
+ :caller_logging,
51
+ :error_sql,
52
+ :sql_comments
53
+ )
54
+
55
+ # Extensions for specific databases
56
+ if database_url.to_s.start_with?("postgresql://")
57
+ extension(
58
+ :pg_array,
59
+ :pg_enum,
60
+ :pg_json,
61
+ :pg_range
62
+ )
63
+ end
64
+ end
65
+
66
+ # @api private
67
+ def gateway_options
68
+ {extensions: config.extensions}
69
+ end
70
+
71
+ # @api public
72
+ # @since 2.2.0
73
+ def clear
74
+ config.extensions = nil
75
+ super
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/configurable"
4
+ require "dry/core"
5
+
6
+ module Hanami
7
+ module Providers
8
+ # @api private
9
+ # @since 2.2.0
10
+ class DB < Dry::System::Provider::Source
11
+ extend Dry::Core::Cache
12
+
13
+ include Dry::Configurable(config_class: Providers::DB::Config)
14
+
15
+ setting :database_url
16
+ setting :adapter, default: :sql
17
+ setting :adapters, mutable: true, default: Adapters.new
18
+ setting :relations_path, default: "relations"
19
+
20
+ def initialize(...)
21
+ super(...)
22
+
23
+ @configured_for_database = false
24
+ end
25
+
26
+ def finalize_config
27
+ apply_parent_config and return if apply_parent_config?
28
+
29
+ configure_for_database
30
+ end
31
+
32
+ def prepare
33
+ prepare_and_import_parent_db and return if import_from_parent?
34
+
35
+ override_rom_inflector
36
+
37
+ finalize_config
38
+
39
+ require "hanami-db"
40
+
41
+ unless database_url
42
+ raise Hanami::ComponentLoadError, "A database_url is required to start :db."
43
+ end
44
+
45
+ # Avoid making spurious connections by reusing identically configured gateways across slices
46
+ gateway = fetch_or_store(database_url, config.gateway_cache_keys) {
47
+ ROM::Gateway.setup(
48
+ config.adapter,
49
+ database_url,
50
+ **config.gateway_options
51
+ )
52
+ }
53
+
54
+ @rom_config = ROM::Configuration.new(gateway)
55
+
56
+ config.each_plugin do |plugin_spec, config_block|
57
+ if config_block
58
+ @rom_config.plugin(config.adapter, plugin_spec) do |plugin_config|
59
+ instance_exec(plugin_config, &config_block)
60
+ end
61
+ else
62
+ @rom_config.plugin(config.adapter, plugin_spec)
63
+ end
64
+ end
65
+
66
+ register "config", @rom_config
67
+ register "gateway", gateway
68
+ end
69
+
70
+ # @api private
71
+ def start
72
+ start_and_import_parent_db and return if import_from_parent?
73
+
74
+ # Set up DB logging for the whole app. We share the app's notifications bus across all
75
+ # slices, so we only need to configure the subsciprtion for DB logging just once.
76
+ target.app.start :db_logging
77
+
78
+ # Find and register relations
79
+ relations_path = target.source_path.join(config.relations_path)
80
+ relations_path.glob("*.rb").each do |relation_file|
81
+ relation_name = relation_file
82
+ .relative_path_from(relations_path)
83
+ .basename(relation_file.extname)
84
+ .to_s
85
+
86
+ relation_class = target.namespace
87
+ .const_get(:Relations) # TODO don't hardcode
88
+ .const_get(target.inflector.camelize(relation_name))
89
+
90
+ @rom_config.register_relation(relation_class)
91
+ end
92
+
93
+ # TODO: register mappers & commands
94
+
95
+ rom = ROM.container(@rom_config)
96
+
97
+ register "rom", rom
98
+ end
99
+
100
+ def stop
101
+ target["db.rom"].disconnect
102
+ end
103
+
104
+ # @api private
105
+ def database_url
106
+ return @database_url if instance_variable_defined?(:@database_url)
107
+
108
+ # For "main" slice, expect MAIN__DATABASE_URL
109
+ slice_url_var = "#{target.slice_name.name.gsub("/", "__").upcase}__DATABASE_URL"
110
+ chosen_url = config.database_url || ENV[slice_url_var] || ENV["DATABASE_URL"]
111
+ chosen_url &&= Hanami::DB::Testing.database_url(chosen_url) if Hanami.env?(:test)
112
+
113
+ @database_url = chosen_url
114
+ end
115
+
116
+ private
117
+
118
+ def parent_db_provider
119
+ return @parent_db_provider if instance_variable_defined?(:@parent_db_provider)
120
+
121
+ @parent_db_provider = target.parent && target.parent.container.providers[:db]
122
+ end
123
+
124
+ def apply_parent_config
125
+ parent_db_provider.source.finalize_config
126
+
127
+ self.class.settings.keys.each do |key|
128
+ # Preserve settings already configured locally
129
+ next if config.configured?(key)
130
+
131
+ # Skip adapter config, we handle this below
132
+ next if key == :adapters
133
+
134
+ config[key] = parent_db_provider.source.config[key]
135
+ end
136
+
137
+ parent_db_provider.source.config.adapters.each do |adapter_name, parent_adapter|
138
+ adapter = config.adapters[adapter_name]
139
+
140
+ adapter.class.settings.keys.each do |key|
141
+ next if adapter.config.configured?(key)
142
+
143
+ adapter.config[key] = parent_adapter.config[key]
144
+ end
145
+ end
146
+ end
147
+
148
+ def apply_parent_config?
149
+ target.config.db.configure_from_parent && parent_db_provider
150
+ end
151
+
152
+ def configure_for_database
153
+ return if @configured_for_database
154
+
155
+ config.adapter(config.adapter_name).configure_for_database(database_url)
156
+ @configured_for_database = true
157
+ end
158
+
159
+ def import_from_parent?
160
+ target.config.db.import_from_parent && target.parent
161
+ end
162
+
163
+ def prepare_and_import_parent_db
164
+ return unless parent_db_provider
165
+
166
+ target.parent.prepare :db
167
+ @rom_config = target.parent["db.config"]
168
+
169
+ register "config", (@rom_config = target.parent["db.config"])
170
+ register "gateway", target.parent["db.gateway"]
171
+ end
172
+
173
+ def start_and_import_parent_db
174
+ return unless parent_db_provider
175
+
176
+ target.parent.start :db
177
+
178
+ register "rom", target.parent["db.rom"]
179
+ end
180
+
181
+ # ROM 5.3 doesn't have a configurable inflector.
182
+ #
183
+ # This is a problem in Hanami because using different
184
+ # inflection rules for ROM will lead to constant loading
185
+ # errors.
186
+ def override_rom_inflector
187
+ return if ROM::Inflector == Hanami.app["inflector"]
188
+
189
+ ROM.instance_eval {
190
+ remove_const :Inflector
191
+ const_set :Inflector, Hanami.app["inflector"]
192
+ }
193
+ end
194
+ end
195
+
196
+ Dry::System.register_provider_source(
197
+ :db,
198
+ source: DB,
199
+ group: :hanami,
200
+ provider_options: {namespace: true}
201
+ )
202
+ end
203
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Providers
5
+ # @api private
6
+ # @since 2.2.0
7
+ class DBLogging < Dry::System::Provider::Source
8
+ # @api private
9
+ # @since 2.2.0
10
+ def prepare
11
+ require "dry/monitor/sql/logger"
12
+ target["notifications"].register_event :sql
13
+ end
14
+
15
+ # @api private
16
+ # @since 2.2.0
17
+ def start
18
+ Dry::Monitor::SQL::Logger.new(target["logger"]).subscribe(target["notifications"])
19
+ end
20
+ end
21
+ end
22
+ end
@@ -20,8 +20,8 @@ module Hanami
20
20
  # Explicitly register the Rack middleware events on our notifications bus. The Dry::Monitor
21
21
  # rack extension (activated above) does register these globally, but if the notifications
22
22
  # bus has been used before this provider loads, then it will have created its own separate
23
- # locally copy of all registered events as of that moment in time, which will not included
24
- # the Rack events globally reigstered above.
23
+ # local copy of all registered events as of that moment in time, which will not be included
24
+ # in the Rack events globally registered above.
25
25
  notifications = target["notifications"]
26
26
  notifications.register_event(Dry::Monitor::Rack::Middleware::REQUEST_START)
27
27
  notifications.register_event(Dry::Monitor::Rack::Middleware::REQUEST_STOP)
@@ -37,7 +37,7 @@ module Hanami
37
37
  clock: Dry::Monitor::Clock.new(unit: :microsecond)
38
38
  )
39
39
 
40
- rack_logger = Hanami::Web::RackLogger.new(target[:logger], env: target.env)
40
+ rack_logger = Hanami::Web::RackLogger.new(target[:logger], env: target.container.env)
41
41
  rack_logger.attach(monitor_middleware)
42
42
 
43
43
  register "monitor", monitor_middleware
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Providers
5
+ # @api private
6
+ # @since 2.2.0
7
+ class Relations < Dry::System::Provider::Source
8
+ def start
9
+ start_and_import_parent_relations and return if target.parent && target.config.db.import_from_parent
10
+
11
+ target.start :db
12
+
13
+ register_relations target["db.rom"]
14
+ end
15
+
16
+ private
17
+
18
+ def register_relations(rom)
19
+ rom.relations.each do |name, _|
20
+ register name, rom.relations[name]
21
+ end
22
+ end
23
+
24
+ def start_and_import_parent_relations
25
+ target.parent.start :relations
26
+
27
+ register_relations target.parent["db.rom"]
28
+ end
29
+ end
30
+ end
31
+ end
@@ -10,18 +10,6 @@ module Hanami
10
10
  # @api private
11
11
  # @since 2.0.0
12
12
  class Routes < Dry::System::Provider::Source
13
- # @api private
14
- def self.for_slice(slice)
15
- Class.new(self) do |klass|
16
- klass.instance_variable_set(:@slice, slice)
17
- end
18
- end
19
-
20
- # @api private
21
- def self.slice
22
- @slice || Hanami.app
23
- end
24
-
25
13
  # @api private
26
14
  def prepare
27
15
  require "hanami/slice/routes_helper"
@@ -33,7 +21,7 @@ module Hanami
33
21
  # router during the process of booting. This ensures the router's resolver can run strict
34
22
  # action key checks once when it runs on a fully booted slice.
35
23
  register :routes do
36
- Hanami::Slice::RoutesHelper.new(self.class.slice.router)
24
+ Hanami::Slice::RoutesHelper.new(target.router)
37
25
  end
38
26
  end
39
27
  end
@@ -32,14 +32,15 @@ Hanami::CLI::RakeTasks.register_tasks do
32
32
  # Please use them when you're in control of your deployment environment.
33
33
  #
34
34
  # If you're not in control and your deployment requires these "standard"
35
- # Rake tasks, they are here to solve this only specific problem.
36
- #
37
- # namespace :db do
38
- # task :migrate do
39
- # # TODO(@jodosha): Enable when we'll integrate with ROM
40
- # # run_hanami_command("db migrate")
41
- # end
42
- # end
35
+ # Rake tasks, they are here only to solve this specific problem.
36
+
37
+ if Hanami.bundled?("hanami-db")
38
+ namespace :db do
39
+ task :migrate do
40
+ Hanami::CLI::Commands::App::DB::Migrate.new.call
41
+ end
42
+ end
43
+ end
43
44
 
44
45
  if Hanami.bundled?("hanami-assets")
45
46
  namespace :assets do
@@ -8,7 +8,7 @@ module Hanami
8
8
  # Provides user-defined settings for an Hanami app or slice.
9
9
  #
10
10
  # Define your own settings by inheriting from this class in `config/settings.rb` within an app or
11
- # slice. Your settings will be loaded from matching ENV vars (with upper-cased names) and made
11
+ # slice. Your settings will be loaded from matching ENV vars (with upper-cased names) and be
12
12
  # registered as a component as part of the Hanami app {Hanami::Slice::ClassMethods#prepare
13
13
  # prepare} step.
14
14
  #
@@ -160,8 +160,8 @@ module Hanami
160
160
  value = store.fetch(name, Undefined)
161
161
 
162
162
  if value.eql?(Undefined)
163
- # When a key is missing entirely from the store, _read_ its value from the config instead,
164
- # which ensures its setting constructor runs (with a `nil` argument given) and raises any
163
+ # When a key is missing entirely from the store, _read_ its value from the config instead.
164
+ # This ensures its setting constructor runs (with a `nil` argument given) and raises any
165
165
  # necessary errors.
166
166
  public_send(name)
167
167
  else