dynamic-active-model 0.7.2 → 0.9.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: 2664e9a3b74032d4296a54416912c82bf4f23ce605867215b95676d889d791ed
4
- data.tar.gz: 9a85b302c4ed61002b9379f2b1b9812632d2c51696dfa2e1e4b02ec1546eac89
3
+ metadata.gz: a9f8e2e549471211eb31b49e3e20b8a72e1c47eb2e9c857bcb4a85c11e470558
4
+ data.tar.gz: 70b7edd5ecf3dbe9c164593f1866eb2bd12a9cb89b5c609898052b8ad3f5eee2
5
5
  SHA512:
6
- metadata.gz: 475f4bf0369216319169d75505800bc795cc7077b3aaa9e599eaf6b3c167093bc423bd280ad8d4435887b0883ff75b9463cfecd047fa1646d1b7a11ec737aa87
7
- data.tar.gz: 1d6081d4fa6c1d36d8e9d9b68392219d85ff2cc50eade08f879c5f56fbb61fa46ec3c1ea6ba475453142f430822fd4361540fdb3f1834ddff18b456d59a72528
6
+ metadata.gz: 90b12783def828937bef5898a020c6e8639fbf72c83a8649f5227077fce087d1fc619bd25f472b70b83dcf6a551bc97386f57c86b76f4fdc401fbe8c92336c11
7
+ data.tar.gz: c3316c0eb2a01b52e3b2628c913ce1d1d53c6ba7f67a34cbfb879caa820509d5ab676e3a2f78c771ca40a3405f960e65bea4ca9674540fca3192808d32053926
@@ -45,8 +45,11 @@ module DynamicActiveModel
45
45
  # @param table_name [String] Name of the table with the foreign key
46
46
  # @param foreign_key [String] Name of the foreign key column
47
47
  # @param relationship_name [String, nil] Custom name for the relationship
48
+ # @raise [ModelNotFound] If no model exists for the table
48
49
  def add_foreign_key(table_name, foreign_key, relationship_name = nil)
49
- @foreign_keys[table_name].add(foreign_key, relationship_name)
50
+ @foreign_keys
51
+ .fetch(table_name.to_s) { raise ModelNotFound, "no model found for table #{table_name}" }
52
+ .add(foreign_key, relationship_name)
50
53
  end
51
54
 
52
55
  # Builds all relationships between models based on foreign keys and constraints
@@ -122,29 +125,29 @@ module DynamicActiveModel
122
125
 
123
126
  # Adds a has_many relationship to a model
124
127
  # @param relationship_name [String] Name of the relationship
125
- # @param model [Class] The model with the foreign key
126
- # @param has_many_model [Class] The model being referenced
128
+ # @param model [Class] The referenced (parent) model that gets the association
129
+ # @param has_many_model [Class] The model with the foreign key
127
130
  # @param foreign_key [String] The foreign key column name
128
131
  def add_has_many(relationship_name, model, has_many_model, foreign_key)
129
132
  model.has_many(
130
133
  generate_has_many_association_name(relationship_name, model, has_many_model),
131
134
  class_name: has_many_model.name,
132
135
  foreign_key: foreign_key,
133
- primary_key: has_many_model.primary_key
136
+ primary_key: model.primary_key
134
137
  )
135
138
  end
136
139
 
137
140
  # Adds a has_one relationship to a model
138
141
  # @param relationship_name [String] Name of the relationship
139
- # @param model [Class] The model with the foreign key
140
- # @param has_one_model [Class] The model being referenced
142
+ # @param model [Class] The referenced (parent) model that gets the association
143
+ # @param has_one_model [Class] The model with the foreign key
141
144
  # @param foreign_key [String] The foreign key column name
142
145
  def add_has_one(relationship_name, model, has_one_model, foreign_key)
143
146
  model.has_one(
144
147
  generate_has_one_association_name(relationship_name, model, has_one_model),
145
148
  class_name: has_one_model.name,
146
149
  foreign_key: foreign_key,
147
- primary_key: has_one_model.primary_key
150
+ primary_key: model.primary_key
148
151
  )
149
152
  end
150
153
 
@@ -161,8 +164,8 @@ module DynamicActiveModel
161
164
 
162
165
  # Generates an appropriate name for a has_many association
163
166
  # @param relationship_name [String] Original relationship name
164
- # @param model [Class] The model with the foreign key
165
- # @param has_many_model [Class] The model being referenced
167
+ # @param model [Class] The referenced (parent) model that gets the association
168
+ # @param has_many_model [Class] The model with the foreign key
166
169
  # @return [Symbol] The generated association name
167
170
  def generate_has_many_association_name(relationship_name, model, has_many_model)
168
171
  name =
@@ -176,8 +179,8 @@ module DynamicActiveModel
176
179
 
177
180
  # Generates an appropriate name for a has_one association
178
181
  # @param relationship_name [String] Original relationship name
179
- # @param model [Class] The model with the foreign key
180
- # @param has_one_model [Class] The model being referenced
182
+ # @param model [Class] The referenced (parent) model that gets the association
183
+ # @param has_one_model [Class] The model with the foreign key
181
184
  # @return [Symbol] The generated association name
182
185
  def generate_has_one_association_name(relationship_name, model, has_one_model)
183
186
  name =
@@ -208,7 +211,7 @@ module DynamicActiveModel
208
211
  def join_table?(model)
209
212
  model.primary_key.nil? &&
210
213
  model.columns.size == 2 &&
211
- model.columns.all? { |column| column.name =~ /#{ForeignKey.id_suffix}$/ }
214
+ model.columns.all? { |column| column.name.end_with?(ForeignKey.id_suffix) }
212
215
  end
213
216
  end
214
217
  end
@@ -94,15 +94,16 @@ module DynamicActiveModel
94
94
  @table_class_names[table_name.to_s] = class_name
95
95
  end
96
96
 
97
- # Creates ActiveRecord models for all included tables
98
- # @return [Array] List of created model classes
97
+ # Creates ActiveRecord models for all included tables that don't have one yet
98
+ # @return [Array] List of model classes
99
+ # @raise [ClassNameConflict] If two tables map to the same class name
99
100
  def create_models!
100
101
  @factory.base_class.connection.tables.each do |table_name|
101
- next if skip_table?(table_name)
102
- next unless include_table?(table_name)
102
+ next unless create_model?(table_name)
103
103
 
104
104
  @models << @factory.create(table_name, @table_class_names[table_name])
105
105
  end
106
+ @models
106
107
  end
107
108
 
108
109
  # @return [Array] List of all skipped tables and patterns
@@ -153,7 +154,7 @@ module DynamicActiveModel
153
154
  if file
154
155
  updater = ModelUpdater.new(model)
155
156
  ctx = updater.instance_eval { binding }
156
- eval(File.read(file), ctx, file, 1)
157
+ eval(File.read(file), ctx, file, 1) # rubocop:disable Security/Eval -- extension files are trusted code
157
158
  end
158
159
  model.class_eval(&block) if block
159
160
  model
@@ -174,6 +175,13 @@ module DynamicActiveModel
174
175
 
175
176
  private
176
177
 
178
+ # Checks if a model should be created for a table
179
+ # @param table_name [String] Name of the table
180
+ # @return [Boolean] Whether the table is included and has no model yet
181
+ def create_model?(table_name)
182
+ !skip_table?(table_name) && include_table?(table_name) && get_model(table_name).nil?
183
+ end
184
+
177
185
  # Checks if a table should be skipped
178
186
  # @param table_name [String] Name of the table
179
187
  # @return [Boolean] Whether the table should be skipped
@@ -28,9 +28,11 @@ module DynamicActiveModel
28
28
  # @param connection_options [Hash] Database connection options
29
29
  # @param skip_tables [Array<String, Regexp>] Tables to exclude from model creation
30
30
  # @param relationships [Hash] Custom foreign key relationships to add
31
+ # @param table_class_names [Hash] Custom class names by table name
31
32
  # @return [Database] The configured database instance
32
- def self.explore(base_module, connection_options, skip_tables = [], relationships = {})
33
- database = create_models!(base_module, connection_options, skip_tables)
33
+ # @raise [ClassNameConflict] If two tables map to the same class name
34
+ def self.explore(base_module, connection_options, skip_tables = [], relationships = {}, table_class_names = {})
35
+ database = create_models!(base_module, connection_options, skip_tables, table_class_names)
34
36
  build_relationships!(database, relationships)
35
37
  database
36
38
  end
@@ -39,17 +41,25 @@ module DynamicActiveModel
39
41
  # @param base_module [Module] The namespace for created models
40
42
  # @param connection_options [Hash] Database connection options
41
43
  # @param skip_tables [Array<String, Regexp>] Tables to exclude from model creation
44
+ # @param table_class_names [Hash] Custom class names by table name
42
45
  # @return [Database] The configured database instance
43
- def self.create_models!(base_module, connection_options, skip_tables)
46
+ def self.create_models!(base_module, connection_options, skip_tables, table_class_names = {})
44
47
  database = Database.new(base_module, connection_options)
45
- skip_tables.each do |table_name|
46
- table_name = Regexp.new("^#{table_name}") if table_name.include?('*')
47
- database.skip_table(table_name)
48
- end
48
+ skip_tables.each { |table| database.skip_table(skip_table_matcher(table)) }
49
+ table_class_names.each { |table_name, class_name| database.table_class_name(table_name, class_name) }
49
50
  database.create_models!
50
51
  database
51
52
  end
52
53
 
54
+ # Converts a table name containing * wildcards into an anchored Regexp
55
+ # @param table [String, Regexp] Table name, wildcard pattern, or Regexp
56
+ # @return [String, Regexp] The table name or matching Regexp
57
+ def self.skip_table_matcher(table)
58
+ return table unless table.is_a?(String) && table.include?('*')
59
+
60
+ Regexp.new("\\A#{Regexp.escape(table).gsub('\\*', '.*')}\\z")
61
+ end
62
+
53
63
  # Sets up relationships between created models
54
64
  # @param database [Database] The database instance containing the models
55
65
  # @param relationships [Hash] Custom foreign key relationships to add
@@ -33,10 +33,12 @@ module DynamicActiveModel
33
33
  # @param table_name [String] Name of the database table
34
34
  # @param class_name [String, nil] Optional custom class name
35
35
  # @return [Class] The model class
36
+ # @raise [ClassNameConflict] If the class name is already used by a different table
36
37
  def create(table_name, class_name = nil)
37
38
  class_name ||= generate_class_name(table_name)
38
- create!(table_name, class_name) unless @base_module.const_defined?(class_name)
39
- @base_module.const_get(class_name)
39
+ return create!(table_name, class_name) unless @base_module.const_defined?(class_name, false)
40
+
41
+ existing_model!(table_name, class_name)
40
42
  end
41
43
 
42
44
  # Creates a new model class for a table, overwriting if it exists
@@ -49,7 +51,7 @@ module DynamicActiveModel
49
51
  include DynamicActiveModel::DangerousAttributesPatch
50
52
  end
51
53
  @base_module.const_set(class_name, kls)
52
- @base_module.const_get(class_name)
54
+ @base_module.const_get(class_name, false)
53
55
  end
54
56
 
55
57
  # Gets or creates the base class for all models
@@ -85,5 +87,22 @@ module DynamicActiveModel
85
87
 
86
88
  class_name
87
89
  end
90
+
91
+ private
92
+
93
+ # Returns the existing model for a class name if it belongs to the table
94
+ # @param table_name [String] Name of the database table
95
+ # @param class_name [String] Name of the existing class
96
+ # @return [Class] The existing model class
97
+ # @raise [ClassNameConflict] If the class belongs to a different table
98
+ def existing_model!(table_name, class_name)
99
+ model = @base_module.const_get(class_name, false)
100
+ return model if model.respond_to?(:table_name) && model.table_name == table_name.to_s
101
+
102
+ raise ClassNameConflict,
103
+ "table #{table_name} maps to class #{model.name}, which is already used by " \
104
+ "#{model.respond_to?(:table_name) ? "table #{model.table_name}" : 'another constant'}; " \
105
+ 'set a different class name with table_class_name'
106
+ end
88
107
  end
89
108
  end
@@ -47,6 +47,7 @@ module DynamicActiveModel
47
47
  connection_options: nil,
48
48
  skip_tables: [],
49
49
  relationships: {},
50
+ table_class_names: {},
50
51
  extensions_path: nil,
51
52
  extensions_suffix: '.ext.rb'
52
53
  }
@@ -120,6 +121,21 @@ module DynamicActiveModel
120
121
  redefine_class_method(:dynamic_active_model_config, config)
121
122
  end
122
123
 
124
+ # Sets a custom class name for a table
125
+ # @param table_name [String] Name of the table
126
+ # @param class_name [String] Class name to use for the table's model
127
+ def table_class_name(table_name, class_name)
128
+ config = dynamic_active_model_config
129
+ config[:table_class_names][table_name.to_s] = class_name
130
+ redefine_class_method(:dynamic_active_model_config, config)
131
+ end
132
+
133
+ # Gets the custom class names by table name
134
+ # @return [Hash] The current table class names
135
+ def table_class_names
136
+ dynamic_active_model_config[:table_class_names]
137
+ end
138
+
123
139
  # Sets or gets the path for model extensions
124
140
  # @param path [String, nil] Path to extension files
125
141
  # @return [String, nil] The current extensions path
@@ -156,7 +172,8 @@ module DynamicActiveModel
156
172
  self,
157
173
  connection_options,
158
174
  skip_tables,
159
- relationships
175
+ relationships,
176
+ table_class_names
160
177
  )
161
178
  )
162
179
  database.update_all_models(extensions_path, extensions_suffix) if extensions_path
@@ -62,33 +62,25 @@ module DynamicActiveModel
62
62
  # Gets all has_many relationships for the model
63
63
  # @return [Array<ActiveRecord::Reflection::HasManyReflection>]
64
64
  def all_has_many_relationships
65
- @model.reflect_on_all_associations.select do |assoc|
66
- assoc.is_a?(ActiveRecord::Reflection::HasManyReflection)
67
- end
65
+ @model.reflect_on_all_associations.grep(ActiveRecord::Reflection::HasManyReflection)
68
66
  end
69
67
 
70
68
  # Gets all belongs_to relationships for the model
71
69
  # @return [Array<ActiveRecord::Reflection::BelongsToReflection>]
72
70
  def all_belongs_to_relationships
73
- @model.reflect_on_all_associations.select do |assoc|
74
- assoc.is_a?(ActiveRecord::Reflection::BelongsToReflection)
75
- end
71
+ @model.reflect_on_all_associations.grep(ActiveRecord::Reflection::BelongsToReflection)
76
72
  end
77
73
 
78
74
  # Gets all has_one relationships for the model
79
75
  # @return [Array<ActiveRecord::Reflection::HasOneReflection>]
80
76
  def all_has_one_relationships
81
- @model.reflect_on_all_associations.select do |assoc|
82
- assoc.is_a?(ActiveRecord::Reflection::HasOneReflection)
83
- end
77
+ @model.reflect_on_all_associations.grep(ActiveRecord::Reflection::HasOneReflection)
84
78
  end
85
79
 
86
80
  # Gets all has_and_belongs_to_many relationships for the model
87
81
  # @return [Array<ActiveRecord::Reflection::HasAndBelongsToManyReflection>]
88
82
  def all_has_and_belongs_to_many_relationships
89
- @model.reflect_on_all_associations.select do |assoc|
90
- assoc.is_a?(ActiveRecord::Reflection::HasAndBelongsToManyReflection)
91
- end
83
+ @model.reflect_on_all_associations.grep(ActiveRecord::Reflection::HasAndBelongsToManyReflection)
92
84
  end
93
85
 
94
86
  # Appends an association definition to the source string
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicActiveModel
4
+ # Gem version, bumped by release-please
5
+ VERSION = '0.9.0'
6
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'dynamic-active-model/version'
4
+
3
5
  # DynamicActiveModel is a Ruby gem that provides automatic database discovery,
4
6
  # model creation, and relationship mapping for Rails applications.
5
7
  #
@@ -44,6 +46,12 @@ module DynamicActiveModel
44
46
  # Manages the setup process and configuration
45
47
  autoload :Setup, 'dynamic-active-model/setup'
46
48
 
49
+ # Base class for errors raised by DynamicActiveModel
50
+ class Error < StandardError; end
51
+
47
52
  # Raised when a requested model cannot be found
48
- class ModelNotFound < StandardError; end
53
+ class ModelNotFound < Error; end
54
+
55
+ # Raised when two tables map to the same model class name
56
+ class ClassNameConflict < Error; end
49
57
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynamic-active-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Doug Youch
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-02-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord
@@ -58,6 +58,7 @@ files:
58
58
  - lib/dynamic-active-model/foreign_key.rb
59
59
  - lib/dynamic-active-model/setup.rb
60
60
  - lib/dynamic-active-model/template_class_file.rb
61
+ - lib/dynamic-active-model/version.rb
61
62
  homepage: https://github.com/dougyouch/dynamic-active-model
62
63
  licenses:
63
64
  - MIT
@@ -77,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
77
78
  - !ruby/object:Gem::Version
78
79
  version: '0'
79
80
  requirements: []
80
- rubygems_version: 3.6.2
81
+ rubygems_version: 4.0.20
81
82
  specification_version: 4
82
83
  summary: Automatically discover database schemas and create ActiveRecord models with
83
84
  relationships