hanami 2.1.1 → 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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/README.md +7 -7
  4. data/hanami.gemspec +6 -6
  5. data/lib/hanami/app.rb +5 -1
  6. data/lib/hanami/config/db.rb +33 -0
  7. data/lib/hanami/config.rb +36 -9
  8. data/lib/hanami/extensions/db/repo.rb +103 -0
  9. data/lib/hanami/extensions.rb +4 -0
  10. data/lib/hanami/helpers/form_helper/form_builder.rb +2 -4
  11. data/lib/hanami/provider_registrar.rb +26 -0
  12. data/lib/hanami/providers/assets.rb +2 -20
  13. data/lib/hanami/providers/db/adapter.rb +68 -0
  14. data/lib/hanami/providers/db/adapters.rb +44 -0
  15. data/lib/hanami/providers/db/config.rb +66 -0
  16. data/lib/hanami/providers/db/sql_adapter.rb +80 -0
  17. data/lib/hanami/providers/db.rb +203 -0
  18. data/lib/hanami/providers/db_logging.rb +22 -0
  19. data/lib/hanami/providers/rack.rb +1 -1
  20. data/lib/hanami/providers/relations.rb +31 -0
  21. data/lib/hanami/providers/routes.rb +1 -13
  22. data/lib/hanami/rake_tasks.rb +8 -7
  23. data/lib/hanami/slice.rb +84 -4
  24. data/lib/hanami/version.rb +1 -1
  25. data/lib/hanami.rb +3 -0
  26. data/spec/integration/container/provider_environment_spec.rb +52 -0
  27. data/spec/integration/db/auto_registration_spec.rb +39 -0
  28. data/spec/integration/db/db_inflector_spec.rb +57 -0
  29. data/spec/integration/db/db_slices_spec.rb +327 -0
  30. data/spec/integration/db/db_spec.rb +220 -0
  31. data/spec/integration/db/logging_spec.rb +238 -0
  32. data/spec/integration/db/provider_config_spec.rb +88 -0
  33. data/spec/integration/db/provider_spec.rb +35 -0
  34. data/spec/integration/db/repo_spec.rb +215 -0
  35. data/spec/integration/db/slices_importing_from_parent.rb +130 -0
  36. data/spec/integration/slices/slice_configuration_spec.rb +4 -4
  37. data/spec/support/app_integration.rb +3 -0
  38. data/spec/unit/hanami/config/db_spec.rb +38 -0
  39. data/spec/unit/hanami/config/router_spec.rb +1 -1
  40. data/spec/unit/hanami/helpers/form_helper_spec.rb +31 -0
  41. data/spec/unit/hanami/providers/db/config/default_config_spec.rb +107 -0
  42. data/spec/unit/hanami/providers/db/config_spec.rb +206 -0
  43. data/spec/unit/hanami/slice_spec.rb +32 -0
  44. data/spec/unit/hanami/version_spec.rb +1 -1
  45. metadata +61 -19
@@ -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
@@ -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
@@ -33,13 +33,14 @@ Hanami::CLI::RakeTasks.register_tasks do
33
33
  #
34
34
  # If you're not in control and your deployment requires these "standard"
35
35
  # Rake tasks, they are here only to solve this 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
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
data/lib/hanami/slice.rb CHANGED
@@ -101,6 +101,16 @@ module Hanami
101
101
  Hanami.app
102
102
  end
103
103
 
104
+ # Returns true if the slice is Hanami.app
105
+ #
106
+ # @return [Boolean]
107
+ #
108
+ # @api public
109
+ # @since 2.2.0
110
+ def app?
111
+ eql?(app)
112
+ end
113
+
104
114
  # Returns the slice's config.
105
115
  #
106
116
  # A slice's config is copied from the app config at time of first access.
@@ -203,6 +213,16 @@ module Hanami
203
213
  config.root || app.root.join(SLICES_DIR, slice_name.to_s)
204
214
  end
205
215
 
216
+ # Returns the slice's root component directory, accounting for App as a special case.
217
+ #
218
+ # @return [Pathname]
219
+ #
220
+ # @api public
221
+ # @since 2.2.0
222
+ def source_path
223
+ app? ? root.join(APP_DIR) : root
224
+ end
225
+
206
226
  # Returns the slice's configured inflector.
207
227
  #
208
228
  # Unless explicitly re-configured for the slice, this will be the app's inflector.
@@ -507,6 +527,12 @@ module Hanami
507
527
  container.register_provider(...)
508
528
  end
509
529
 
530
+ # @api public
531
+ # @since 2.1.0
532
+ def configure_provider(*args, **kwargs, &block)
533
+ container.register_provider(*args, **kwargs, from: :hanami, &block)
534
+ end
535
+
510
536
  # @overload start(provider_name)
511
537
  # Starts a provider.
512
538
  #
@@ -559,6 +585,13 @@ module Hanami
559
585
  container.key?(...)
560
586
  end
561
587
 
588
+ # Required for the slice to act as a provider target
589
+ # @api public
590
+ # @since 2.2.0
591
+ def registered?(...)
592
+ container.registered?(...)
593
+ end
594
+
562
595
  # Returns an array of keys for all currently registered components in the container.
563
596
  #
564
597
  # For a prepared slice, this will be the set of components that have been previously resolved.
@@ -842,6 +875,7 @@ module Hanami
842
875
  def prepare_container_base_config
843
876
  container.config.name = slice_name.to_sym
844
877
  container.config.root = root
878
+ container.config.provider_registrar = ProviderRegistrar.for_slice(self)
845
879
  container.config.provider_dirs = [File.join("config", "providers")]
846
880
  container.config.registrations_dir = File.join("config", "registrations")
847
881
 
@@ -893,12 +927,29 @@ module Hanami
893
927
  # point we're still in the process of preparing.
894
928
  if routes
895
929
  require_relative "providers/routes"
896
- register_provider(:routes, source: Providers::Routes.for_slice(self))
930
+ register_provider(:routes, source: Providers::Routes)
897
931
  end
898
932
 
899
933
  if assets_dir? && Hanami.bundled?("hanami-assets")
900
934
  require_relative "providers/assets"
901
- register_provider(:assets, source: Providers::Assets.for_slice(self))
935
+ register_provider(:assets, source: Providers::Assets)
936
+ end
937
+
938
+ if Hanami.bundled?("hanami-db")
939
+ # Explicit require here to ensure the provider source registers itself, to allow the user
940
+ # to configure it within their own concrete provider file.
941
+ require_relative "providers/db"
942
+
943
+ if register_db_provider?
944
+ # Only register providers if the user hasn't provided their own
945
+ if !container.providers[:db]
946
+ register_provider(:db, namespace: true, source: Providers::DB)
947
+ end
948
+
949
+ if !container.providers[:relations]
950
+ register_provider(:relations, namespace: true, source: Providers::Relations)
951
+ end
952
+ end
902
953
  end
903
954
  end
904
955
 
@@ -1028,8 +1079,37 @@ module Hanami
1028
1079
  private_constant :ROUTER_NOT_FOUND_HANDLER
1029
1080
 
1030
1081
  def assets_dir?
1031
- assets_path = app.eql?(self) ? root.join("app", "assets") : root.join("assets")
1032
- assets_path.directory?
1082
+ source_path.join("assets").directory?
1083
+ end
1084
+
1085
+ def register_db_provider?
1086
+ concrete_db_provider? ||
1087
+ db_config_dir? ||
1088
+ relations_dir? ||
1089
+ db_source_dir? ||
1090
+ import_db_from_parent?
1091
+ end
1092
+
1093
+ def concrete_db_provider?
1094
+ root.join(CONFIG_DIR, "providers", "db.rb").exist?
1095
+ end
1096
+
1097
+ def db_config_dir?
1098
+ root.join("config", "db").directory?
1099
+ end
1100
+
1101
+ def relations_dir?
1102
+ source_path.join("relations").directory?
1103
+ end
1104
+
1105
+ def db_source_dir?
1106
+ source_path.join("db").directory?
1107
+ end
1108
+
1109
+ def import_db_from_parent?
1110
+ parent &&
1111
+ config.db.import_from_parent &&
1112
+ parent.container.providers[:db]
1033
1113
  end
1034
1114
 
1035
1115
  # rubocop:enable Metrics/AbcSize
@@ -7,7 +7,7 @@ module Hanami
7
7
  # @api private
8
8
  module Version
9
9
  # @api public
10
- VERSION = "2.1.1"
10
+ VERSION = "2.2.0.beta1"
11
11
 
12
12
  # @since 0.9.0
13
13
  # @api private
data/lib/hanami.rb CHANGED
@@ -17,6 +17,9 @@ module Hanami
17
17
  # @since 2.0.0
18
18
  def self.loader
19
19
  @loader ||= Zeitwerk::Loader.for_gem.tap do |loader|
20
+ loader.inflector.inflect "db" => "DB"
21
+ loader.inflector.inflect "db_logging" => "DBLogging"
22
+ loader.inflector.inflect "sql_adapter" => "SQLAdapter"
20
23
  loader.ignore(
21
24
  "#{loader.dirs.first}/hanami/{constants,boot,errors,extensions/router/errors,prepare,rake_tasks,setup}.rb"
22
25
  )
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe "Container / Provider environment", :app_integration do
4
+ let!(:app) {
5
+ module TestApp
6
+ class App < Hanami::App
7
+ class << self
8
+ attr_accessor :test_provider_target
9
+ end
10
+ end
11
+ end
12
+
13
+ before_prepare if respond_to?(:before_prepare)
14
+
15
+ Hanami.app.prepare
16
+ Hanami.app
17
+ }
18
+
19
+ context "app provider" do
20
+ before do
21
+ Hanami.app.register_provider :test_provider, namespace: true do
22
+ start do
23
+ Hanami.app.test_provider_target = target
24
+ end
25
+ end
26
+ end
27
+
28
+ it "exposes the app as the provider target" do
29
+ Hanami.app.start :test_provider
30
+ expect(Hanami.app.test_provider_target).to be Hanami.app
31
+ end
32
+ end
33
+
34
+ context "slice provider" do
35
+ def before_prepare
36
+ Hanami.app.register_slice :main
37
+ end
38
+
39
+ before do
40
+ Main::Slice.register_provider :test_provider, namespace: true do
41
+ start do
42
+ Hanami.app.test_provider_target = target
43
+ end
44
+ end
45
+ end
46
+
47
+ it "exposes the slice as the provider target" do
48
+ Main::Slice.start :test_provider
49
+ expect(Hanami.app.test_provider_target).to be Main::Slice
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe "DB / auto-registration", :app_integration do
4
+ before do
5
+ @env = ENV.to_h
6
+ allow(Hanami::Env).to receive(:loaded?).and_return(false)
7
+ end
8
+
9
+ after do
10
+ ENV.replace(@env)
11
+ end
12
+
13
+ it "does not auto-register files in entities/, structs/, or db/" do
14
+ with_tmp_directory(@dir = Dir.mktmpdir) do
15
+ write "config/app.rb", <<~RUBY
16
+ require "hanami"
17
+
18
+ module TestApp
19
+ class App < Hanami::App
20
+ end
21
+ end
22
+ RUBY
23
+
24
+ write "app/db/changesets/update_posts.rb", ""
25
+ write "app/entities/post.rb", ""
26
+ write "app/structs/post.rb", ""
27
+
28
+ ENV["DATABASE_URL"] = "sqlite::memory"
29
+
30
+ require "hanami/boot"
31
+
32
+ expect(Hanami.app.keys).not_to include(*[
33
+ "db.changesets.update_posts",
34
+ "entities.post",
35
+ "structs.post"
36
+ ])
37
+ end
38
+ end
39
+ end