dynamic-active-model-rails 0.12.0 → 0.14.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aade59eb21651ddf7c6c7f50de964e3d3e1e6809e82e9d832cb05cfe6917df65
4
- data.tar.gz: a0a3e87f460a462d3096193fdb4b2c405799903bf347c74944ce83b258c76afb
3
+ metadata.gz: e99654c467d82a550320a4952955c2ac035a218940ba178f4aab57ea6036c438
4
+ data.tar.gz: fc7f43e364ced45bd2d52e0ea918ae4498ea690d3414a1c830c20f03495dc836
5
5
  SHA512:
6
- metadata.gz: 562618b697479ecfcd9aff7cd04c8e9e40488064925dba63d76828ea5255c337355d0272d8e666d2f69b2b8e22f0fc5a67ffee86153075501ee34aab18b52de1
7
- data.tar.gz: eb5fe6bbd793f081d53f2dfd5826226ffc10806a3e6afb922e9eeffc49f1f391b2009246049f72506b27d38b3982ebbb79ab4d193be0567958c42738e1b5a91f
6
+ metadata.gz: edd5f4bacf38a6a2e3fa610da7f6fe4aa01e781d7675feeb39164e97085842fbdaeb85d6ba8bc027b09672a58adaec9f875db1cdf352868a5c91b38ecece64cb
7
+ data.tar.gz: 6e5616c8bb2b5ca5c3fc7868f51d6b22b9c40ec0bfb6fd3d8c9942e57fc321eb7c445b1a09628416c71aee958b0c926c2cf7ca2239cce482fa9f5de48b5fcbbf
data/README.md CHANGED
@@ -13,6 +13,21 @@ gem 'dynamic-active-model-rails'
13
13
 
14
14
  `dynamic-active-model-rails` is released in lockstep with `dynamic-active-model` and pins the same version.
15
15
 
16
+ ## Generators
17
+
18
+ ```bash
19
+ bin/rails generate dynamic_active_model:install # DB, app/models/db/
20
+ bin/rails generate dynamic_active_model:install grant_db # GrantDB, app/models/grant_db/
21
+ bin/rails generate dynamic_active_model:database cars --connection cars
22
+ bin/rails generate dynamic_active_model:extension grant_db users # app/models/grant_db/users.ext.rb
23
+ ```
24
+
25
+ - **`install [NAME]`** creates `config/initializers/dynamic_active_model.rb`, declaring the first database (default `db`), and its models folder.
26
+ - **`database NAME`** adds another `add_database` line to the initializer and creates the folder. It refuses a namespace the app already declares.
27
+ - **`extension DATABASE TABLE`** creates an extension file. `DATABASE` can be a name (`grant_db`) or a namespace (`GrantDB`). The generator uses the app's configuration, so a custom `extensions_path:` or `extensions_suffix:` is respected.
28
+
29
+ `--connection NAME` points the database at a `database.yml` entry. The generators warn if the current environment has no such entry. Without `--connection`, the database shares `ApplicationRecord`'s connection.
30
+
16
31
  ## Configuration
17
32
 
18
33
  ```ruby
@@ -26,6 +41,7 @@ DynamicActiveModel::Rails.configure do |config|
26
41
  # A Symbol names a database.yml entry; a URL string or config hash also works
27
42
  config.add_database :cars, :cars do |db|
28
43
  db.skip_tables 'legacy_*', /^tmp_/
44
+ db.include_tables 'cars', 'makes', 'owner*' # whitelist; default is every table
29
45
  db.foreign_key :cars, :owner_id, :owner
30
46
  db.table_class_name :status, 'StatusCode'
31
47
  end
@@ -48,6 +64,7 @@ Namespaces always end in `DB`, so a database's namespace never collides with an
48
64
  |---|---|---|
49
65
  | `add_database :cars` | `CarsDB` | `app/models/cars_db/` |
50
66
  | `add_database :grant_db` | `GrantDB` | `app/models/grant_db/` |
67
+ | `add_database :db` | `DB` | `app/models/db/` |
51
68
  | `add_database :cars, module_name: 'Inventory'` | `Inventory` | `app/models/inventory/` |
52
69
 
53
70
  The `cars_db` → `CarsDB` mapping is registered with the Rails autoloader only, not with the global inflector, so `'cars_db'.camelize` elsewhere in your app is unaffected.
@@ -59,6 +76,8 @@ The `cars_db` → `CarsDB` mapping is registered with the Rails autoloader only,
59
76
  | `connection` (2nd argument) | `nil` | `nil` shares the parent class's connection. Otherwise it's passed to `establish_connection`: a database.yml entry name (Symbol), URL or hash. |
60
77
  | `module_name:` | `"<Name>DB"` | Namespace override. |
61
78
  | `parent_class:` | `'ApplicationRecord'` | Superclass of the generated abstract base class, given as a name so it can be reloaded. |
79
+ | `extensions_path:` | `app/models/<folder>` | Directory of extension files, absolute or relative to `Rails.root`. The default folder may be absent; a configured path must exist. |
80
+ | `extensions_suffix:` | `'.ext.rb'` | Suffix of extension files. The autoloader ignores files with this suffix. |
62
81
 
63
82
  ## Extending Models
64
83
 
@@ -121,6 +140,37 @@ development:
121
140
  migrations_paths: db/cars_migrate
122
141
  ```
123
142
 
143
+ ## Migrating from the Setup DSL
144
+
145
+ If your app followed the core gem's [manual setup](../docs/manual-rails-setup.md):
146
+
147
+ 1. Replace `gem 'dynamic-active-model'` with `gem 'dynamic-active-model-rails'`.
148
+ 2. Delete `app/models/db.rb`. Move its settings into an initializer:
149
+
150
+ ```ruby
151
+ # config/initializers/dynamic_active_model.rb
152
+ DynamicActiveModel::Rails.configure do |config|
153
+ config.add_database :db do |db| # DB, app/models/db/
154
+ db.skip_tables 'versions'
155
+ end
156
+ end
157
+ ```
158
+
159
+ | Setup DSL | Rails gem |
160
+ |---|---|
161
+ | `parent_class ApplicationRecord` | default (omit) |
162
+ | `connection_options 'secondary'` | `add_database :db, :secondary` |
163
+ | `extensions_path '...'` | default for `app/models/db/`; otherwise `extensions_path:` |
164
+ | `extensions_suffix '.x.rb'` | `extensions_suffix:` |
165
+ | `skip_tables [...]` / `skip_table` | `db.skip_tables` |
166
+ | `foreign_key t, col, name` | `db.foreign_key t, col, name` |
167
+ | `table_class_name t, name` | `db.table_class_name t, name` |
168
+
169
+ 3. Remove the `DB` inflection and the `app/models/db` ignore from `config/application.rb`. The gem registers both.
170
+ 4. Move setup that ran after `create_models!` into `ActiveSupport.on_load(:db) { ... }`.
171
+
172
+ `.ext.rb` files stay where they are.
173
+
124
174
  ## Development
125
175
 
126
176
  From this directory:
@@ -3,8 +3,8 @@
3
3
  module DynamicActiveModel
4
4
  module Rails
5
5
  # Teaches the Rails autoloader about a database's folder: cars_db/ maps to
6
- # CarsDB (only in Zeitwerk, not the global inflector), and .ext.rb files are
7
- # left to DynamicActiveModel instead of being autoloaded.
6
+ # CarsDB (only in Zeitwerk, not the global inflector), and extension files
7
+ # (*.ext.rb by default) are left to DynamicActiveModel instead of being autoloaded.
8
8
  class AutoloaderSetup
9
9
  # @param autoloaders [Rails::Autoloaders]
10
10
  # @param definition [DatabaseDefinition]
@@ -26,7 +26,7 @@ module DynamicActiveModel
26
26
 
27
27
  # @return [String] Glob matching the database's extension files
28
28
  def extension_glob
29
- File.join(@definition.extensions_path(@root), '*.ext.rb')
29
+ File.join(@definition.extensions_path(@root), "*#{@definition.extensions_suffix}")
30
30
  end
31
31
  end
32
32
  end
@@ -20,6 +20,12 @@ module DynamicActiveModel
20
20
  # @return [Array<String, Regexp>] Tables (or patterns) to skip
21
21
  attr_reader :skipped_tables
22
22
 
23
+ # @return [Array<String, Regexp>] Tables (or patterns) to model; empty means all
24
+ attr_reader :included_tables
25
+
26
+ # @return [String] File suffix of extension files
27
+ attr_reader :extensions_suffix
28
+
23
29
  # @return [Hash] Custom relationship names by table and foreign key
24
30
  attr_reader :relationships
25
31
 
@@ -30,11 +36,18 @@ module DynamicActiveModel
30
36
  # @param connection [Symbol, String, Hash, nil] See Configuration#add_database
31
37
  # @param module_name [String, nil] Namespace override, e.g. "Inventory"
32
38
  # @param parent_class [String] Superclass for the generated base class
33
- def initialize(name, connection = nil, module_name: nil, parent_class: 'ApplicationRecord')
39
+ # @param extensions_path [String, nil] Extensions directory, absolute or relative to
40
+ # the app root; defaults to app/models/<folder>, which may be absent
41
+ # @param extensions_suffix [String] File suffix of extension files
42
+ def initialize(name, connection = nil, module_name: nil, parent_class: 'ApplicationRecord',
43
+ extensions_path: nil, extensions_suffix: '.ext.rb')
34
44
  @connection = connection
35
45
  @module_name = module_name || default_module_name(name)
36
46
  @parent_class_name = parent_class.to_s
47
+ @extensions_path = extensions_path&.to_s
48
+ @extensions_suffix = extensions_suffix
37
49
  @skipped_tables = []
50
+ @included_tables = []
38
51
  @relationships = {}
39
52
  @table_class_names = {}
40
53
  end
@@ -50,9 +63,14 @@ module DynamicActiveModel
50
63
  end
51
64
 
52
65
  # @param root [Pathname, String] Application root
53
- # @return [String] Directory holding this database's .ext.rb files
66
+ # @return [String] Absolute directory holding this database's extension files
54
67
  def extensions_path(root)
55
- File.join(root.to_s, 'app', 'models', folder)
68
+ File.expand_path(@extensions_path || File.join('app', 'models', folder), root.to_s)
69
+ end
70
+
71
+ # @return [Boolean] Whether extensions_path was configured, so it must exist
72
+ def custom_extensions_path?
73
+ !@extensions_path.nil?
56
74
  end
57
75
 
58
76
  # Skips tables; strings may use * wildcards
@@ -62,6 +80,14 @@ module DynamicActiveModel
62
80
  @skipped_tables.concat(tables.flatten)
63
81
  end
64
82
 
83
+ # Models only these tables; strings may use * wildcards. ActiveRecord's
84
+ # internal tables still need their exact name.
85
+ # @param tables [Array<String, Regexp>]
86
+ # @return [void]
87
+ def include_tables(*tables)
88
+ @included_tables.concat(tables.flatten)
89
+ end
90
+
65
91
  # Names the relationship for a foreign key column
66
92
  # @param table_name [String, Symbol]
67
93
  # @param foreign_key [String, Symbol] Column name
@@ -81,11 +107,11 @@ module DynamicActiveModel
81
107
 
82
108
  private
83
109
 
84
- # :cars => "CarsDB", :grant_db => "GrantDB"
110
+ # :cars => "CarsDB", :grant_db => "GrantDB", :db => "DB"
85
111
  # @param name [Symbol, String]
86
112
  # @return [String]
87
113
  def default_module_name(name)
88
- "#{name.to_s.underscore.delete_suffix('_db').camelize}DB"
114
+ "#{name.to_s.underscore.sub(/(?:\A|_)db\z/, '').camelize}DB"
89
115
  end
90
116
  end
91
117
  end
@@ -61,15 +61,22 @@ module DynamicActiveModel
61
61
  def new_database
62
62
  Database.new(namespace, definition.connection, parent_class: parent_class).tap do |database|
63
63
  definition.skipped_tables.each { |table| database.skip_table(Explorer.skip_table_matcher(table)) }
64
+ definition.included_tables.each { |table| database.include_table(Explorer.skip_table_matcher(table)) }
64
65
  definition.table_class_names.each { |table, class_name| database.table_class_name(table, class_name) }
65
66
  end
66
67
  end
67
68
 
68
- # Applies the .ext.rb files in the database's models folder
69
+ # Applies the database's extension files. The default folder is optional;
70
+ # a configured extensions_path must exist.
69
71
  # @return [void]
72
+ # @raise [DynamicActiveModel::Error] If a configured extensions_path is missing
70
73
  def load_extensions
71
74
  path = definition.extensions_path(@root)
72
- @database.update_all_models(path) if File.directory?(path)
75
+ if File.directory?(path)
76
+ @database.update_all_models(path, definition.extensions_suffix)
77
+ elsif definition.custom_extensions_path?
78
+ raise DynamicActiveModel::Error, "extensions_path #{path} for #{definition.module_name} does not exist"
79
+ end
73
80
  end
74
81
 
75
82
  # @return [Module] The namespace models are defined in
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require_relative '../database_arguments'
5
+
6
+ module DynamicActiveModel
7
+ module Generators
8
+ # rails g dynamic_active_model:database NAME [--connection=NAME]
9
+ class DatabaseGenerator < ::Rails::Generators::Base
10
+ include DatabaseArguments
11
+
12
+ desc 'Declares another database in config/initializers/dynamic_active_model.rb ' \
13
+ '(e.g. cars => CarsDB in app/models/cars_db/).'
14
+
15
+ argument :name, type: :string, banner: 'NAME'
16
+
17
+ def check_initializer
18
+ return if File.exist?(File.join(destination_root, INITIALIZER))
19
+
20
+ raise Thor::Error, "#{INITIALIZER} not found; run rails g dynamic_active_model:install first"
21
+ end
22
+
23
+ def check_not_declared
24
+ raise Thor::Error, "#{definition.module_name} is already declared" if declared?
25
+ end
26
+
27
+ def add_database
28
+ inject_into_file INITIALIZER, " #{add_database_line}\n", before: /^end\b/
29
+ end
30
+
31
+ def create_models_folder
32
+ create_extensions_folder
33
+ end
34
+
35
+ def check_connection
36
+ warn_about_missing_connection
37
+ end
38
+
39
+ private
40
+
41
+ # @return [Boolean] Whether the app already declares this namespace
42
+ def declared?
43
+ DynamicActiveModel::Rails.configuration.definitions.any? { |db| db.module_name == definition.module_name }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicActiveModel
4
+ module Generators
5
+ # Shared --connection option and helpers for generators that declare a database
6
+ module DatabaseArguments
7
+ INITIALIZER = 'config/initializers/dynamic_active_model.rb'
8
+
9
+ def self.included(base)
10
+ base.class_option :connection,
11
+ type: :string,
12
+ desc: "database.yml entry to connect to (default: share ApplicationRecord's connection)"
13
+ end
14
+
15
+ private
16
+
17
+ # @return [DynamicActiveModel::Rails::DatabaseDefinition] Naming for the NAME argument
18
+ def definition
19
+ @definition ||= DynamicActiveModel::Rails::DatabaseDefinition.new(name)
20
+ end
21
+
22
+ # @return [String] e.g. "config.add_database :cars, :cars"
23
+ def add_database_line
24
+ args = [name.underscore.to_sym.inspect]
25
+ args << options[:connection].to_sym.inspect if options[:connection]
26
+ "config.add_database #{args.join(', ')}"
27
+ end
28
+
29
+ # Creates the database's models folder for its extension files
30
+ # @return [void]
31
+ def create_extensions_folder
32
+ create_file File.join(definition.extensions_path(destination_root), '.keep')
33
+ end
34
+
35
+ # Warns when --connection names a database.yml entry the current environment lacks
36
+ # @return [void]
37
+ def warn_about_missing_connection
38
+ return unless options[:connection]
39
+ return if ActiveRecord::Base.configurations.configs_for(env_name: ::Rails.env, name: options[:connection])
40
+
41
+ say_status :warning, "config/database.yml has no #{options[:connection]} entry for #{::Rails.env}", :yellow
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module DynamicActiveModel
6
+ module Generators
7
+ # rails g dynamic_active_model:extension DATABASE TABLE
8
+ class ExtensionGenerator < ::Rails::Generators::Base
9
+ source_root File.expand_path('templates', __dir__)
10
+ desc 'Creates an extension file for a table of a declared database ' \
11
+ '(e.g. grant_db users => app/models/grant_db/users.ext.rb).'
12
+
13
+ argument :database, type: :string, banner: 'DATABASE'
14
+ argument :table, type: :string, banner: 'TABLE'
15
+
16
+ def create_extension
17
+ template 'extension.rb.tt', File.join(definition.extensions_path(destination_root), file_name)
18
+ end
19
+
20
+ private
21
+
22
+ # The declared database named by DATABASE, as a name (grant_db) or namespace (GrantDB)
23
+ # @return [DynamicActiveModel::Rails::DatabaseDefinition]
24
+ # @raise [Thor::Error] If no such database is declared
25
+ def definition
26
+ @definition ||= declared.find { |db| [database, default_module_name].include?(db.module_name) } ||
27
+ raise(Thor::Error, "no database #{database} is declared " \
28
+ "(declared: #{declared.map(&:module_name).join(', ')})")
29
+ end
30
+
31
+ # @return [Array<DynamicActiveModel::Rails::DatabaseDefinition>]
32
+ def declared
33
+ DynamicActiveModel::Rails.configuration.definitions
34
+ end
35
+
36
+ # @return [String] Namespace DATABASE maps to by default, e.g. "GrantDB"
37
+ def default_module_name
38
+ DynamicActiveModel::Rails::DatabaseDefinition.new(database).module_name
39
+ end
40
+
41
+ # @return [String] e.g. "users.ext.rb"
42
+ def file_name
43
+ "#{table}#{definition.extensions_suffix}"
44
+ end
45
+
46
+ # @return [String] e.g. "GrantDB::User"
47
+ def model_name
48
+ "#{definition.module_name}::#{definition.table_class_names[table] || table.classify}"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Extends <%= model_name %> (table <%= table %>). Columns and associations that
4
+ # follow foreign key conventions are detected from the schema; add the rest here:
5
+ # through/polymorphic associations, validations, scopes and methods.
6
+ update_model do
7
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require_relative '../database_arguments'
5
+
6
+ module DynamicActiveModel
7
+ module Generators
8
+ # rails g dynamic_active_model:install [NAME] [--connection=NAME]
9
+ class InstallGenerator < ::Rails::Generators::Base
10
+ include DatabaseArguments
11
+
12
+ source_root File.expand_path('templates', __dir__)
13
+ desc 'Creates config/initializers/dynamic_active_model.rb declaring a first database ' \
14
+ '(default: db, i.e. DB in app/models/db/).'
15
+
16
+ argument :name, type: :string, default: 'db', banner: 'NAME'
17
+
18
+ def create_initializer
19
+ template 'dynamic_active_model.rb.tt', INITIALIZER
20
+ end
21
+
22
+ def create_models_folder
23
+ create_extensions_folder
24
+ end
25
+
26
+ def check_connection
27
+ warn_about_missing_connection
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Each database's tables become ActiveRecord models in a <Name>DB namespace. Models
4
+ # are built on first use and rebuilt after code reloads and migrations. Per-table
5
+ # extensions go in app/models/<name>_db/<table_name>.ext.rb.
6
+ # https://github.com/dougyouch/dynamic-active-model/tree/master/dynamic-active-model-rails
7
+ #
8
+ # Add databases with bin/rails g dynamic_active_model:database NAME, or by hand:
9
+ # config.add_database :cars, :cars do |db| # CarsDB, "cars" entry in database.yml
10
+ # db.skip_tables 'legacy_*'
11
+ # end
12
+ DynamicActiveModel::Rails.configure do |config|
13
+ <%= add_database_line %>
14
+ end
15
+
16
+ # Setup that touches the models (e.g. has_paper_trail) belongs in a load hook,
17
+ # which runs after every build of <%= definition.module_name %>'s models:
18
+ # ActiveSupport.on_load(<%= definition.load_hook.inspect %>) do
19
+ # get_model!(:users).has_paper_trail
20
+ # end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynamic-active-model-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Doug Youch
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 0.12.0
32
+ version: 0.14.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 0.12.0
39
+ version: 0.14.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: railties
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -69,6 +69,12 @@ files:
69
69
  - lib/dynamic-active-model/rails/lazy_namespace.rb
70
70
  - lib/dynamic-active-model/rails/railtie.rb
71
71
  - lib/dynamic-active-model/rails/schema_change_hook.rb
72
+ - lib/generators/dynamic_active_model/database/database_generator.rb
73
+ - lib/generators/dynamic_active_model/database_arguments.rb
74
+ - lib/generators/dynamic_active_model/extension/extension_generator.rb
75
+ - lib/generators/dynamic_active_model/extension/templates/extension.rb.tt
76
+ - lib/generators/dynamic_active_model/install/install_generator.rb
77
+ - lib/generators/dynamic_active_model/install/templates/dynamic_active_model.rb.tt
72
78
  homepage: https://github.com/dougyouch/dynamic-active-model/tree/master/dynamic-active-model-rails
73
79
  licenses:
74
80
  - MIT