dynamic-active-model 0.6.1 → 0.6.4

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: a37ad8867ebc7255e83cbb62c1a70e3394e77db96e230afb669352cd4d68643e
4
- data.tar.gz: 7fd3f802d8cae2c9bb59856d3cbd0a71c94977b76dc53ee69471b1eb0718fb3c
3
+ metadata.gz: 14a04d1dd0784f4300e46dd358b5ad392cac0f1244b6a7520465df06ae2bb84a
4
+ data.tar.gz: ad0cf1bda6db17ee7354d64c24dc42cd9330f8404b1b98c10a15f87decde094d
5
5
  SHA512:
6
- metadata.gz: 88068b7c0075fb5a23ceb713459f14ecdf8bddb3300f112bf0dd85a7c3a917f5d7607b1127709747af9ffd7737a038d4392498b43f19c759cebca535c653cdb9
7
- data.tar.gz: da5ea3b6205ed36515493143e98d1188fe54f708e3135ec87845b44b8f7cd1b22fb24deb392bfd84f6f97f0a7261f5582647b09d81ef3c7525d11b9c9cc221a6
6
+ metadata.gz: 8000cf349d155abaf9feec697a440e3a9d1db4112cb2257005900b712774a6d05b5c41f5f68f8ca2a05443385e202b62ea5ffa021c92a47e6038ad7b65213107
7
+ data.tar.gz: 7adf72224a086374d1b54034d6f3ada3fb05537acc3d031dfdaf701658dd5cc05121f232f4be80075070db54750f48ad714d18e0b2fd9f0ff2d8aab7bae77bd2
@@ -1,26 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DynamicActiveModel
4
- # DynamicActiveModel::Associations iterates over the models of a
5
- # database and adds has_many and belongs_to based on foreign keys
4
+ # The Associations class is responsible for automatically detecting and setting up
5
+ # ActiveRecord relationships between models based on database schema analysis.
6
+ # It supports the following relationship types:
7
+ # - belongs_to
8
+ # - has_many
9
+ # - has_one (automatically detected from unique constraints)
10
+ # - has_and_belongs_to_many (automatically detected from join tables)
11
+ #
12
+ # @example Basic Usage
13
+ # db = DynamicActiveModel::Database.new(DB, database_config)
14
+ # db.create_models!
15
+ # associations = DynamicActiveModel::Associations.new(db)
16
+ # associations.build!
17
+ #
18
+ # @example Custom Foreign Key
19
+ # associations.add_foreign_key('users', 'manager_id', 'manager')
6
20
  class Associations
7
- attr_reader :database,
8
- :table_indexes
9
-
21
+ # @return [Database] The database instance containing the models
22
+ attr_reader :database
23
+
24
+ # @return [Hash] Mapping of table names to their indexes
25
+ attr_reader :table_indexes
26
+
27
+ # @return [Array] List of detected join tables
28
+ attr_reader :join_tables
29
+
30
+ # Initializes a new Associations instance
31
+ # @param database [Database] The database instance containing the models
10
32
  def initialize(database)
11
33
  @database = database
12
34
  @table_indexes = {}
13
- @foreign_keys = database.models.each_with_object({}) do |model, hsh|
14
- hsh[model.table_name] = ForeignKey.new(model)
35
+ @join_tables = []
36
+ @foreign_keys = {}
37
+ database.models.each do |model|
38
+ @foreign_keys[model.table_name] = ForeignKey.new(model)
15
39
  @table_indexes[model.table_name] = model.connection.indexes(model.table_name)
40
+ @join_tables << model if join_table?(model)
16
41
  end
17
42
  end
18
43
 
44
+ # Adds a custom foreign key relationship
45
+ # @param table_name [String] Name of the table with the foreign key
46
+ # @param foreign_key [String] Name of the foreign key column
47
+ # @param relationship_name [String, nil] Custom name for the relationship
19
48
  def add_foreign_key(table_name, foreign_key, relationship_name = nil)
20
49
  @foreign_keys[table_name].add(foreign_key, relationship_name)
21
50
  end
22
51
 
23
- # iterates over the models and adds relationships
52
+ # Builds all relationships between models based on foreign keys and constraints
53
+ # This method:
54
+ # 1. Maps foreign keys to their corresponding models
55
+ # 2. Adds belongs_to relationships
56
+ # 3. Adds has_many or has_one relationships based on unique constraints
57
+ # 4. Sets up has_and_belongs_to_many relationships for join tables
58
+ # @return [void]
24
59
  def build!
25
60
  foreign_key_to_models = create_foreign_key_to_model_map
26
61
 
@@ -35,10 +70,31 @@ module DynamicActiveModel
35
70
  end
36
71
  end
37
72
  end
73
+
74
+ @join_tables.each do |join_table_model|
75
+ models = join_table_model.column_names.map { |column_name| foreign_key_to_models[column_name.downcase]&.first&.first }.compact
76
+ if models.size == 2
77
+ add_has_and_belongs_to_many(join_table_model, models)
78
+ end
79
+ end
38
80
  end
39
81
 
40
82
  private
41
83
 
84
+ # Adds has_and_belongs_to_many relationships between two models
85
+ # @param join_table_model [Class] The join table model
86
+ # @param models [Array<Class>] The two models to be related
87
+ def add_has_and_belongs_to_many(join_table_model, models)
88
+ model1, model2 = *models
89
+ model1.has_and_belongs_to_many model2.table_name.pluralize.to_sym, join_table: join_table_model.table_name, class_name: model2.name
90
+ model2.has_and_belongs_to_many model1.table_name.pluralize.to_sym, join_table: join_table_model.table_name, class_name: model1.name
91
+ end
92
+
93
+ # Adds appropriate relationships between two models
94
+ # @param relationship_name [String] Name of the relationship
95
+ # @param model [Class] The model with the foreign key
96
+ # @param belongs_to_model [Class] The model being referenced
97
+ # @param foreign_key [String] The foreign key column name
42
98
  def add_relationships(relationship_name, model, belongs_to_model, foreign_key)
43
99
  add_belongs_to(relationship_name, model, belongs_to_model, foreign_key)
44
100
  if unique_index?(model, foreign_key)
@@ -48,6 +104,11 @@ module DynamicActiveModel
48
104
  end
49
105
  end
50
106
 
107
+ # Adds a belongs_to relationship to a model
108
+ # @param relationship_name [String] Name of the relationship
109
+ # @param model [Class] The model with the foreign key
110
+ # @param belongs_to_model [Class] The model being referenced
111
+ # @param foreign_key [String] The foreign key column name
51
112
  def add_belongs_to(relationship_name, model, belongs_to_model, foreign_key)
52
113
  model.belongs_to(
53
114
  relationship_name.singularize.to_sym,
@@ -57,6 +118,11 @@ module DynamicActiveModel
57
118
  )
58
119
  end
59
120
 
121
+ # Adds a has_many relationship to a model
122
+ # @param relationship_name [String] Name of the relationship
123
+ # @param model [Class] The model with the foreign key
124
+ # @param has_many_model [Class] The model being referenced
125
+ # @param foreign_key [String] The foreign key column name
60
126
  def add_has_many(relationship_name, model, has_many_model, foreign_key)
61
127
  model.has_many(
62
128
  generate_has_many_association_name(relationship_name, model, has_many_model),
@@ -66,6 +132,11 @@ module DynamicActiveModel
66
132
  )
67
133
  end
68
134
 
135
+ # Adds a has_one relationship to a model
136
+ # @param relationship_name [String] Name of the relationship
137
+ # @param model [Class] The model with the foreign key
138
+ # @param has_one_model [Class] The model being referenced
139
+ # @param foreign_key [String] The foreign key column name
69
140
  def add_has_one(relationship_name, model, has_one_model, foreign_key)
70
141
  model.has_one(
71
142
  generate_has_one_association_name(relationship_name, model, has_one_model),
@@ -75,6 +146,8 @@ module DynamicActiveModel
75
146
  )
76
147
  end
77
148
 
149
+ # Creates a mapping of foreign key column names to their corresponding models
150
+ # @return [Hash] Mapping of foreign key names to model and relationship name pairs
78
151
  def create_foreign_key_to_model_map
79
152
  @foreign_keys.values.each_with_object({}) do |foreign_key, hsh|
80
153
  foreign_key.keys.each do |key, relationship_name|
@@ -84,16 +157,26 @@ module DynamicActiveModel
84
157
  end
85
158
  end
86
159
 
160
+ # Generates an appropriate name for a has_many association
161
+ # @param relationship_name [String] Original relationship name
162
+ # @param model [Class] The model with the foreign key
163
+ # @param has_many_model [Class] The model being referenced
164
+ # @return [Symbol] The generated association name
87
165
  def generate_has_many_association_name(relationship_name, model, has_many_model)
88
166
  name =
89
167
  if relationship_name == model.table_name.underscore
90
168
  has_many_model.table_name
91
169
  else
92
- relationship_name
170
+ "#{relationship_name}_#{has_many_model.table_name}"
93
171
  end
94
172
  name.underscore.pluralize.to_sym
95
173
  end
96
174
 
175
+ # Generates an appropriate name for a has_one association
176
+ # @param relationship_name [String] Original relationship name
177
+ # @param model [Class] The model with the foreign key
178
+ # @param has_one_model [Class] The model being referenced
179
+ # @return [Symbol] The generated association name
97
180
  def generate_has_one_association_name(relationship_name, model, has_one_model)
98
181
  name =
99
182
  if relationship_name == model.table_name.underscore
@@ -104,6 +187,10 @@ module DynamicActiveModel
104
187
  name.underscore.singularize.to_sym
105
188
  end
106
189
 
190
+ # Checks if a foreign key column has a unique index
191
+ # @param model [Class] The model to check
192
+ # @param foreign_key [String] The foreign key column name
193
+ # @return [Boolean] Whether the foreign key has a unique index
107
194
  def unique_index?(model, foreign_key)
108
195
  indexes = table_indexes[model.table_name]
109
196
  indexes.any? do |index|
@@ -112,5 +199,14 @@ module DynamicActiveModel
112
199
  index.columns.first == foreign_key
113
200
  end
114
201
  end
202
+
203
+ # Detects if a model represents a join table for has_and_belongs_to_many
204
+ # @param model [Class] The model to check
205
+ # @return [Boolean] Whether the model is a join table
206
+ def join_table?(model)
207
+ model.primary_key.nil? &&
208
+ model.columns.size == 2 &&
209
+ model.columns.all? { |column| column.name =~ /#{ForeignKey.id_suffix}$/ }
210
+ end
115
211
  end
116
212
  end
@@ -1,9 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DynamicActiveModel
4
- # DynamicActiveModel::DangerousAttributesPatch is used to remove dangerous attribute names
5
- # from attribute_names method in ActiveRecord
4
+ # The DangerousAttributesPatch module is a safety feature that prevents conflicts
5
+ # between database column names and Ruby reserved words or ActiveRecord methods.
6
+ # It automatically detects and ignores columns that could cause conflicts,
7
+ # particularly focusing on boolean columns that might conflict with Ruby's
8
+ # question mark methods.
9
+ #
10
+ # @example Basic Usage
11
+ # class User < ActiveRecord::Base
12
+ # include DynamicActiveModel::DangerousAttributesPatch
13
+ # end
14
+ #
15
+ # @example With Boolean Column
16
+ # # If a table has a boolean column named 'class',
17
+ # # it will be automatically ignored to prevent conflicts
18
+ # # with Ruby's Object#class method
6
19
  module DangerousAttributesPatch
20
+ # Extends the including class with dangerous attribute protection
21
+ # This method:
22
+ # 1. Checks if the class has any attributes
23
+ # 2. Identifies columns that could cause conflicts
24
+ # 3. Adds those columns to the ignored_columns list
25
+ #
26
+ # @param base [Class] The ActiveRecord model class
7
27
  def self.included(base)
8
28
  return unless base.attribute_names
9
29
 
@@ -1,19 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DynamicActiveModel
4
- # DynamicActiveModel::Database iterates over the tables of a
5
- # database and create ActiveRecord models
4
+ # The Database class is responsible for connecting to a database and creating
5
+ # ActiveRecord models from its tables. It provides functionality for:
6
+ # - Table filtering (blacklist/whitelist)
7
+ # - Model creation and management
8
+ # - Model updates and extensions
9
+ #
10
+ # @example Basic Usage
11
+ # db = DynamicActiveModel::Database.new(DB, database_config)
12
+ # db.create_models!
13
+ #
14
+ # @example Table Filtering
15
+ # db.skip_table 'temporary_data'
16
+ # db.include_table 'users'
17
+ # db.create_models!
18
+ #
19
+ # @example Model Updates
20
+ # db.update_model(:users) do
21
+ # def full_name
22
+ # "#{first_name} #{last_name}"
23
+ # end
24
+ # end
6
25
  class Database
7
- attr_reader :table_class_names,
8
- :factory,
9
- :models
10
-
26
+ # @return [Hash] Mapping of table names to custom class names
27
+ attr_reader :table_class_names
28
+
29
+ # @return [Factory] Factory instance used for model creation
30
+ attr_reader :factory
31
+
32
+ # @return [Array] List of created model classes
33
+ attr_reader :models
34
+
35
+ # Helper class for updating model definitions
11
36
  ModelUpdater = Struct.new(:model) do
37
+ # Updates a model's definition with the provided block
38
+ # @param block [Proc] Code to evaluate in the model's context
12
39
  def update_model(&block)
13
40
  model.class_eval(&block)
14
41
  end
15
42
  end
16
43
 
44
+ # Initializes a new Database instance
45
+ # @param base_module [Module] The namespace for created models
46
+ # @param connection_options [Hash] Database connection options
47
+ # @param base_class_name [String, nil] Optional base class name for models
17
48
  def initialize(base_module, connection_options, base_class_name = nil)
18
49
  @factory = Factory.new(base_module, connection_options, base_class_name)
19
50
  @table_class_names = {}
@@ -24,6 +55,8 @@ module DynamicActiveModel
24
55
  @models = []
25
56
  end
26
57
 
58
+ # Adds a table to the blacklist
59
+ # @param table [String, Regexp] Table name or pattern to skip
27
60
  def skip_table(table)
28
61
  if table.is_a?(Regexp)
29
62
  @skip_table_matchers << table
@@ -32,10 +65,14 @@ module DynamicActiveModel
32
65
  end
33
66
  end
34
67
 
68
+ # Adds multiple tables to the blacklist
69
+ # @param tables [Array<String, Regexp>] Table names or patterns to skip
35
70
  def skip_tables(tables)
36
71
  tables.each { |table| skip_table(table) }
37
72
  end
38
73
 
74
+ # Adds a table to the whitelist
75
+ # @param table [String, Regexp] Table name or pattern to include
39
76
  def include_table(table)
40
77
  if table.is_a?(Regexp)
41
78
  @include_table_matchers << table
@@ -44,14 +81,21 @@ module DynamicActiveModel
44
81
  end
45
82
  end
46
83
 
84
+ # Adds multiple tables to the whitelist
85
+ # @param tables [Array<String, Regexp>] Table names or patterns to include
47
86
  def include_tables(tables)
48
87
  tables.each { |table| include_table(table) }
49
88
  end
50
89
 
90
+ # Sets a custom class name for a table
91
+ # @param table_name [String] Name of the table
92
+ # @param class_name [String] Custom class name to use
51
93
  def table_class_name(table_name, class_name)
52
94
  @table_class_names[table_name.to_s] = class_name
53
95
  end
54
96
 
97
+ # Creates ActiveRecord models for all included tables
98
+ # @return [Array] List of created model classes
55
99
  def create_models!
56
100
  @factory.base_class.connection.tables.each do |table_name|
57
101
  next if skip_table?(table_name)
@@ -61,14 +105,18 @@ module DynamicActiveModel
61
105
  end
62
106
  end
63
107
 
108
+ # @return [Array] List of all skipped tables and patterns
64
109
  def skipped_tables
65
110
  @skip_tables + @skip_table_matchers
66
111
  end
67
112
 
113
+ # @return [Array] List of all included tables and patterns
68
114
  def included_tables
69
115
  @include_tables + @include_table_matchers
70
116
  end
71
117
 
118
+ # Disables Single Table Inheritance (STI) for all models
119
+ # @return [void]
72
120
  def disable_standard_table_inheritance!
73
121
  models.each do |model|
74
122
  model.inheritance_column = :_type_disabled if model.attribute_names.include?('type')
@@ -76,11 +124,18 @@ module DynamicActiveModel
76
124
  end
77
125
  alias disable_sti! disable_standard_table_inheritance!
78
126
 
127
+ # Finds a model by table name
128
+ # @param table_name [String] Name of the table
129
+ # @return [Class, nil] The model class or nil if not found
79
130
  def get_model(table_name)
80
131
  table_name = table_name.to_s
81
132
  models.detect { |model| model.table_name == table_name }
82
133
  end
83
134
 
135
+ # Finds a model by table name, raising an error if not found
136
+ # @param table_name [String] Name of the table
137
+ # @return [Class] The model class
138
+ # @raise [ModelNotFound] If no model is found for the table
84
139
  def get_model!(table_name)
85
140
  model = get_model(table_name)
86
141
  return model if model
@@ -88,6 +143,11 @@ module DynamicActiveModel
88
143
  raise ::DynamicActiveModel::ModelNotFound, "no model found for table #{table_name}"
89
144
  end
90
145
 
146
+ # Updates a model's definition
147
+ # @param table_name [String] Name of the table
148
+ # @param file [String, nil] Path to a file containing model updates
149
+ # @param block [Proc] Code to evaluate in the model's context
150
+ # @return [Class] The updated model class
91
151
  def update_model(table_name, file = nil, &block)
92
152
  model = get_model!(table_name)
93
153
  ModelUpdater.new(model).instance_eval(File.read(file)) if file
@@ -95,6 +155,10 @@ module DynamicActiveModel
95
155
  model
96
156
  end
97
157
 
158
+ # Updates all models using extension files from a directory
159
+ # @param base_dir [String] Directory containing extension files
160
+ # @param ext [String] Extension for model update files
161
+ # @return [void]
98
162
  def update_all_models(base_dir, ext = '.ext.rb')
99
163
  Dir.glob("#{base_dir}/*#{ext}") do |file|
100
164
  next unless File.file?(file)
@@ -106,11 +170,17 @@ module DynamicActiveModel
106
170
 
107
171
  private
108
172
 
173
+ # Checks if a table should be skipped
174
+ # @param table_name [String] Name of the table
175
+ # @return [Boolean] Whether the table should be skipped
109
176
  def skip_table?(table_name)
110
177
  @skip_tables.include?(table_name.to_s) ||
111
178
  @skip_table_matchers.any? { |r| r.match(table_name) }
112
179
  end
113
180
 
181
+ # Checks if a table should be included
182
+ # @param table_name [String] Name of the table
183
+ # @return [Boolean] Whether the table should be included
114
184
  def include_table?(table_name)
115
185
  (@include_tables.empty? && @include_table_matchers.empty?) ||
116
186
  @include_tables.include?(table_name) ||
@@ -1,14 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DynamicActiveModel
4
- # DynamicActiveModel::Explorer creates models and relationships
4
+ # The Explorer module provides a high-level interface for automatically discovering
5
+ # and setting up ActiveRecord models and their relationships from a database schema.
6
+ # It combines the functionality of Database and Associations classes into a simple
7
+ # one-call interface.
8
+ #
9
+ # @example Basic Usage
10
+ # module DB; end
11
+ # DynamicActiveModel::Explorer.explore(DB, database_config)
12
+ #
13
+ # @example With Table Filtering
14
+ # skip_tables = ['temporary_data', 'audit_logs']
15
+ # DynamicActiveModel::Explorer.explore(DB, database_config, skip_tables)
16
+ #
17
+ # @example With Custom Relationships
18
+ # relationships = {
19
+ # 'users' => {
20
+ # 'manager_id' => 'manager',
21
+ # 'department_id' => 'department'
22
+ # }
23
+ # }
24
+ # DynamicActiveModel::Explorer.explore(DB, database_config, [], relationships)
5
25
  module Explorer
26
+ # Creates models and sets up relationships in a single call
27
+ # @param base_module [Module] The namespace for created models
28
+ # @param connection_options [Hash] Database connection options
29
+ # @param skip_tables [Array<String, Regexp>] Tables to exclude from model creation
30
+ # @param relationships [Hash] Custom foreign key relationships to add
31
+ # @return [Database] The configured database instance
6
32
  def self.explore(base_module, connection_options, skip_tables = [], relationships = {})
7
33
  database = create_models!(base_module, connection_options, skip_tables)
8
34
  build_relationships!(database, relationships)
9
35
  database
10
36
  end
11
37
 
38
+ # Creates ActiveRecord models from database tables
39
+ # @param base_module [Module] The namespace for created models
40
+ # @param connection_options [Hash] Database connection options
41
+ # @param skip_tables [Array<String, Regexp>] Tables to exclude from model creation
42
+ # @return [Database] The configured database instance
12
43
  def self.create_models!(base_module, connection_options, skip_tables)
13
44
  database = Database.new(base_module, connection_options)
14
45
  skip_tables.each do |table_name|
@@ -19,6 +50,10 @@ module DynamicActiveModel
19
50
  database
20
51
  end
21
52
 
53
+ # Sets up relationships between created models
54
+ # @param database [Database] The database instance containing the models
55
+ # @param relationships [Hash] Custom foreign key relationships to add
56
+ # @return [void]
22
57
  def self.build_relationships!(database, relationships)
23
58
  relations = Associations.new(database)
24
59
  relationships.each do |table_name, foreign_keys|
@@ -1,22 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DynamicActiveModel
4
- # DynamicActiveModel::Factory creates ActiveRecord class for tables
4
+ # The Factory class is responsible for creating ActiveRecord model classes
5
+ # from database tables. It handles:
6
+ # - Base class creation and connection setup
7
+ # - Model class generation with proper naming
8
+ # - Table name assignment
9
+ # - Safety patches for dangerous attributes
10
+ #
11
+ # @example Basic Usage
12
+ # factory = DynamicActiveModel::Factory.new(DB, database_config)
13
+ # model = factory.create('users')
14
+ #
15
+ # @example With Custom Class Name
16
+ # factory = DynamicActiveModel::Factory.new(DB, database_config)
17
+ # model = factory.create('users', 'CustomUser')
5
18
  class Factory
19
+ # @return [Class] The base class for all generated models
6
20
  attr_writer :base_class
7
21
 
22
+ # Initializes a new Factory instance
23
+ # @param base_module [Module] The namespace for created models
24
+ # @param connection_options [Hash] Database connection options
25
+ # @param base_class_name [Symbol, nil] Optional name for the base class
8
26
  def initialize(base_module, connection_options, base_class_name = nil)
9
27
  @base_module = base_module
10
28
  @connection_options = connection_options
11
29
  @base_class_name = base_class_name || :DynamicAbstractBase
12
30
  end
13
31
 
32
+ # Creates a new model class for a table if it doesn't exist
33
+ # @param table_name [String] Name of the database table
34
+ # @param class_name [String, nil] Optional custom class name
35
+ # @return [Class] The model class
14
36
  def create(table_name, class_name = nil)
15
37
  class_name ||= generate_class_name(table_name)
16
38
  create!(table_name, class_name) unless @base_module.const_defined?(class_name)
17
39
  @base_module.const_get(class_name)
18
40
  end
19
41
 
42
+ # Creates a new model class for a table, overwriting if it exists
43
+ # @param table_name [String] Name of the database table
44
+ # @param class_name [String] Name for the model class
45
+ # @return [Class] The model class
20
46
  def create!(table_name, class_name)
21
47
  kls = Class.new(base_class) do
22
48
  self.table_name = table_name
@@ -26,7 +52,12 @@ module DynamicActiveModel
26
52
  @base_module.const_get(class_name)
27
53
  end
28
54
 
29
- # rubocop:disable Metrics/MethodLength
55
+ # Gets or creates the base class for all models
56
+ # This method:
57
+ # 1. Creates an abstract ActiveRecord::Base subclass if needed
58
+ # 2. Establishes the database connection
59
+ # 3. Returns the configured base class
60
+ # @return [Class] The base class for all models
30
61
  def base_class
31
62
  @base_class ||=
32
63
  begin
@@ -44,8 +75,10 @@ module DynamicActiveModel
44
75
  end
45
76
  end
46
77
  end
47
- # rubocop:enable Metrics/MethodLength
48
78
 
79
+ # Generates a valid Ruby class name from a table name
80
+ # @param table_name [String] Name of the database table
81
+ # @return [String] A valid Ruby class name
49
82
  def generate_class_name(table_name)
50
83
  class_name = table_name.classify
51
84
  return "N#{class_name}" if class_name =~ /\A\d/
@@ -1,33 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DynamicActiveModel
4
- # DynamicActiveModel::ForeignKey tracks foreign keys related to the model
4
+ # The ForeignKey class manages foreign key relationships for a model.
5
+ # It provides functionality for:
6
+ # - Tracking foreign key columns
7
+ # - Generating standard foreign key names
8
+ # - Managing custom relationship names
9
+ # - Configuring the foreign key suffix
10
+ #
11
+ # @example Basic Usage
12
+ # model = DB::User
13
+ # fk = DynamicActiveModel::ForeignKey.new(model)
14
+ # fk.add('manager_id', 'manager')
15
+ #
16
+ # @example Custom Suffix
17
+ # DynamicActiveModel::ForeignKey.id_suffix = '_ref'
5
18
  class ForeignKey
6
- attr_reader :model,
7
- :keys
19
+ # @return [Class] The model this foreign key belongs to
20
+ attr_reader :model
21
+
22
+ # @return [Hash] Mapping of foreign key columns to relationship names
23
+ attr_reader :keys
8
24
 
25
+ # Default suffix used for foreign key columns
9
26
  DEFAULT_ID_SUFFIX = '_id'
10
27
 
28
+ # Gets the current foreign key suffix
29
+ # @return [String] The suffix used for foreign key columns
11
30
  def self.id_suffix
12
31
  @id_suffix || DEFAULT_ID_SUFFIX
13
32
  end
14
33
 
15
- # rubocop:disable Style/TrivialAccessors
34
+ # Sets a custom foreign key suffix
35
+ # @param val [String] The new suffix to use
16
36
  def self.id_suffix=(val)
17
37
  @id_suffix = val
18
38
  end
19
- # rubocop:enable Style/TrivialAccessors
20
39
 
40
+ # Initializes a new ForeignKey instance
41
+ # @param model [Class] The model to track foreign keys for
21
42
  def initialize(model)
22
43
  @model = model
23
44
  @keys = {}
24
45
  add(generate_foreign_key(model.table_name))
25
46
  end
26
47
 
48
+ # Adds a foreign key to track
49
+ # @param key [String] The foreign key column name
50
+ # @param relationship_name [String, nil] Optional custom name for the relationship
27
51
  def add(key, relationship_name = nil)
28
52
  @keys[key] = relationship_name || model.table_name.underscore
29
53
  end
30
54
 
55
+ # Generates a standard foreign key name from a table name
56
+ # @param table_name [String] The name of the referenced table
57
+ # @return [String] The generated foreign key column name
31
58
  def generate_foreign_key(table_name)
32
59
  table_name.underscore.singularize + self.class.id_suffix
33
60
  end
@@ -3,19 +3,45 @@
3
3
  require 'inheritance-helper'
4
4
 
5
5
  module DynamicActiveModel
6
- # DynamicActiveModel::Explorer creates models and relationships
6
+ # The Setup module provides configuration and initialization methods for
7
+ # DynamicActiveModel. It allows you to:
8
+ # - Configure database connections
9
+ # - Specify tables to skip
10
+ # - Define custom relationships
11
+ # - Set up model extensions
12
+ #
13
+ # @example Basic Usage
14
+ # module DB
15
+ # include DynamicActiveModel::Setup
16
+ # connection_options database_config
17
+ # skip_tables ['temporary_data']
18
+ # create_models!
19
+ # end
20
+ #
21
+ # @example With Custom Relationships
22
+ # module DB
23
+ # include DynamicActiveModel::Setup
24
+ # foreign_key 'users', 'manager_id', 'manager'
25
+ # create_models!
26
+ # end
7
27
  module Setup
28
+ # Extends the including module with configuration methods
29
+ # @param base [Module] The module including this module
8
30
  def self.included(base)
9
31
  base.extend InheritanceHelper::Methods
10
32
  base.extend ClassMethods
11
33
  end
12
34
 
13
- # ClassMethods various class methods for configuring a module
35
+ # ClassMethods provides various class methods for configuring a module
14
36
  module ClassMethods
37
+ # Gets the database instance
38
+ # @return [Database, nil] The configured database instance
15
39
  def database
16
40
  nil
17
41
  end
18
42
 
43
+ # Gets the current configuration
44
+ # @return [Hash] The configuration hash with default values
19
45
  def dynamic_active_model_config
20
46
  {
21
47
  connection_options: nil,
@@ -26,6 +52,9 @@ module DynamicActiveModel
26
52
  }
27
53
  end
28
54
 
55
+ # Sets or gets the database connection options
56
+ # @param options [Hash, String, nil] Database configuration or named configuration
57
+ # @return [Hash] The current connection options
29
58
  def connection_options(options = nil)
30
59
  if options.is_a?(String)
31
60
  name = options
@@ -47,6 +76,9 @@ module DynamicActiveModel
47
76
  dynamic_active_model_config[:connection_options]
48
77
  end
49
78
 
79
+ # Sets or gets the list of tables to skip
80
+ # @param tables [Array<String>, nil] Tables to skip
81
+ # @return [Array<String>] The current list of skipped tables
50
82
  def skip_tables(tables = nil)
51
83
  if tables
52
84
  config = dynamic_active_model_config
@@ -56,12 +88,17 @@ module DynamicActiveModel
56
88
  dynamic_active_model_config[:skip_tables]
57
89
  end
58
90
 
91
+ # Adds a single table to the skip list
92
+ # @param table [String] Table to skip
59
93
  def skip_table(table)
60
94
  config = dynamic_active_model_config
61
95
  config[:skip_tables] << table
62
96
  redefine_class_method(:dynamic_active_model_config, config)
63
97
  end
64
98
 
99
+ # Sets or gets the custom relationships
100
+ # @param all_relationships [Hash, nil] All custom relationships
101
+ # @return [Hash] The current relationships
65
102
  def relationships(all_relationships = nil)
66
103
  if all_relationships
67
104
  config = dynamic_active_model_config
@@ -71,6 +108,10 @@ module DynamicActiveModel
71
108
  dynamic_active_model_config[:relationships]
72
109
  end
73
110
 
111
+ # Adds a custom foreign key relationship
112
+ # @param table_name [String] Name of the table
113
+ # @param foreign_key [String] Name of the foreign key column
114
+ # @param relationship_name [String] Name for the relationship
74
115
  def foreign_key(table_name, foreign_key, relationship_name)
75
116
  config = dynamic_active_model_config
76
117
  current_relationships = config[:relationships]
@@ -79,6 +120,9 @@ module DynamicActiveModel
79
120
  redefine_class_method(:dynamic_active_model_config, config)
80
121
  end
81
122
 
123
+ # Sets or gets the path for model extensions
124
+ # @param path [String, nil] Path to extension files
125
+ # @return [String, nil] The current extensions path
82
126
  def extensions_path(path = nil)
83
127
  if path
84
128
  config = dynamic_active_model_config
@@ -88,6 +132,9 @@ module DynamicActiveModel
88
132
  dynamic_active_model_config[:extensions_path]
89
133
  end
90
134
 
135
+ # Sets or gets the suffix for extension files
136
+ # @param suffix [String, nil] File extension suffix
137
+ # @return [String] The current extensions suffix
91
138
  def extensions_suffix(suffix = nil)
92
139
  if suffix
93
140
  config = dynamic_active_model_config
@@ -97,6 +144,11 @@ module DynamicActiveModel
97
144
  dynamic_active_model_config[:extensions_suffix]
98
145
  end
99
146
 
147
+ # Creates all models and applies extensions
148
+ # This method:
149
+ # 1. Creates models using Explorer
150
+ # 2. Applies any model extensions if configured
151
+ # @return [Database] The configured database instance
100
152
  def create_models!
101
153
  redefine_class_method(
102
154
  :database,
@@ -1,17 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DynamicActiveModel
4
- # DynamicActiveModel::TemplateClassFile creates ActiveRecord file for model
4
+ # The TemplateClassFile class generates Ruby source files for ActiveRecord models.
5
+ # It creates properly formatted class definitions that include:
6
+ # - Table name configuration
7
+ # - Has many relationships
8
+ # - Belongs to relationships
9
+ # - Has one relationships
10
+ # - Has and belongs to many relationships
11
+ # - Custom association options
12
+ #
13
+ # @example Basic Usage
14
+ # model = DB::User
15
+ # template = DynamicActiveModel::TemplateClassFile.new(model)
16
+ # template.create_template!('app/models')
17
+ #
18
+ # @example Generate Source String
19
+ # model = DB::User
20
+ # template = DynamicActiveModel::TemplateClassFile.new(model)
21
+ # source = template.to_s
5
22
  class TemplateClassFile
23
+ # Initializes a new TemplateClassFile instance
24
+ # @param model [Class] The ActiveRecord model to generate a template for
6
25
  def initialize(model)
7
26
  @model = model
8
27
  end
9
28
 
29
+ # Creates a Ruby source file for the model
30
+ # @param dir [String] Directory to create the file in
31
+ # @return [void]
10
32
  def create_template!(dir)
11
33
  file = dir + '/' + @model.name.underscore + '.rb'
12
34
  File.open(file, 'wb') { |f| f.write(to_s) }
13
35
  end
14
36
 
37
+ # Generates the Ruby source code for the model
38
+ # @return [String] The complete model class definition
15
39
  def to_s
16
40
  str = "class #{@model.name} < ActiveRecord::Base\n".dup
17
41
  str << " self.table_name = #{@model.table_name.to_sym.inspect}\n" unless @model.name.underscore.pluralize == @model.table_name
@@ -21,27 +45,76 @@ module DynamicActiveModel
21
45
  all_belongs_to_relationships.each do |assoc|
22
46
  append_association!(str, assoc)
23
47
  end
48
+ all_has_one_relationships.each do |assoc|
49
+ append_association!(str, assoc)
50
+ end
51
+ all_has_and_belongs_to_many_relationships.each do |assoc|
52
+ append_association!(str, assoc)
53
+ end
24
54
  str << "end\n"
25
55
  str
26
56
  end
27
57
 
28
58
  private
29
59
 
60
+ # Gets all has_many relationships for the model
61
+ # @return [Array<ActiveRecord::Reflection::HasManyReflection>]
30
62
  def all_has_many_relationships
31
63
  @model.reflect_on_all_associations.select do |assoc|
32
64
  assoc.is_a?(ActiveRecord::Reflection::HasManyReflection)
33
65
  end
34
66
  end
35
67
 
68
+ # Gets all belongs_to relationships for the model
69
+ # @return [Array<ActiveRecord::Reflection::BelongsToReflection>]
36
70
  def all_belongs_to_relationships
37
71
  @model.reflect_on_all_associations.select do |assoc|
38
72
  assoc.is_a?(ActiveRecord::Reflection::BelongsToReflection)
39
73
  end
40
74
  end
41
75
 
76
+ # Gets all has_one relationships for the model
77
+ # @return [Array<ActiveRecord::Reflection::HasOneReflection>]
78
+ def all_has_one_relationships
79
+ @model.reflect_on_all_associations.select do |assoc|
80
+ assoc.is_a?(ActiveRecord::Reflection::HasOneReflection)
81
+ end
82
+ end
83
+
84
+ # Gets all has_and_belongs_to_many relationships for the model
85
+ # @return [Array<ActiveRecord::Reflection::HasAndBelongsToManyReflection>]
86
+ def all_has_and_belongs_to_many_relationships
87
+ @model.reflect_on_all_associations.select do |assoc|
88
+ assoc.is_a?(ActiveRecord::Reflection::HasAndBelongsToManyReflection)
89
+ end
90
+ end
91
+
92
+ # Appends an association definition to the source string
93
+ # @param str [String] The source string being built
94
+ # @param assoc [ActiveRecord::Reflection::AssociationReflection] The association to add
42
95
  def append_association!(str, assoc)
43
- assoc_type = assoc.is_a?(ActiveRecord::Reflection::HasManyReflection) ? 'has_many' : 'belongs_to'
44
- association_options = assoc_type == 'has_many' ? has_many_association_options(assoc) : belongs_to_association_options(assoc)
96
+ assoc_type = case assoc
97
+ when ActiveRecord::Reflection::HasManyReflection
98
+ 'has_many'
99
+ when ActiveRecord::Reflection::BelongsToReflection
100
+ 'belongs_to'
101
+ when ActiveRecord::Reflection::HasOneReflection
102
+ 'has_one'
103
+ when ActiveRecord::Reflection::HasAndBelongsToManyReflection
104
+ 'has_and_belongs_to_many'
105
+ end
106
+
107
+ association_options = case assoc_type
108
+ when 'has_many'
109
+ has_many_association_options(assoc)
110
+ when 'belongs_to'
111
+ belongs_to_association_options(assoc)
112
+ when 'has_one'
113
+ has_one_association_options(assoc)
114
+ when 'has_and_belongs_to_many'
115
+ has_and_belongs_to_many_association_options(assoc)
116
+ end
117
+
45
118
  str << " #{assoc_type} #{assoc.name.inspect}"
46
119
  unless association_options.empty?
47
120
  association_options.each do |name, value|
@@ -51,6 +124,9 @@ module DynamicActiveModel
51
124
  str << "\n"
52
125
  end
53
126
 
127
+ # Gets the options for a has_many association
128
+ # @param assoc [ActiveRecord::Reflection::HasManyReflection] The association
129
+ # @return [Hash] The association options
54
130
  def has_many_association_options(assoc)
55
131
  options = {}
56
132
  options[:class_name] = assoc.options[:class_name] unless assoc.options[:class_name].underscore.pluralize == assoc.name.to_s
@@ -59,6 +135,9 @@ module DynamicActiveModel
59
135
  options
60
136
  end
61
137
 
138
+ # Gets the options for a belongs_to association
139
+ # @param assoc [ActiveRecord::Reflection::BelongsToReflection] The association
140
+ # @return [Hash] The association options
62
141
  def belongs_to_association_options(assoc)
63
142
  options = {}
64
143
  options[:class_name] = assoc.options[:class_name] unless assoc.options[:class_name] == assoc.name.to_s.classify
@@ -67,10 +146,43 @@ module DynamicActiveModel
67
146
  options
68
147
  end
69
148
 
149
+ # Gets the options for a has_one association
150
+ # @param assoc [ActiveRecord::Reflection::HasOneReflection] The association
151
+ # @return [Hash] The association options
152
+ def has_one_association_options(assoc)
153
+ options = {}
154
+ options[:class_name] = assoc.options[:class_name] unless assoc.options[:class_name].underscore.singularize == assoc.name.to_s
155
+ options[:foreign_key] = assoc.options[:foreign_key] unless assoc.options[:foreign_key] == default_foreign_key_name
156
+ options[:primary_key] = assoc.options[:primary_key] unless assoc.options[:primary_key] == 'id'
157
+ options
158
+ end
159
+
160
+ # Gets the options for a has_and_belongs_to_many association
161
+ # @param assoc [ActiveRecord::Reflection::HasAndBelongsToManyReflection] The association
162
+ # @return [Hash] The association options
163
+ def has_and_belongs_to_many_association_options(assoc)
164
+ options = {}
165
+ options[:join_table] = assoc.options[:join_table] if assoc.options[:join_table]
166
+ options[:class_name] = assoc.options[:class_name] unless assoc.options[:class_name].underscore.singularize == assoc.name.to_s
167
+ options
168
+ end
169
+
170
+ # Gets the default foreign key name for the model
171
+ # @return [String] The default foreign key name
70
172
  def default_foreign_key_name
71
173
  @model.table_name.underscore.singularize + '_id'
72
174
  end
73
175
 
176
+ # Generates the default foreign key for the associated model
177
+ # @param assoc [ActiveRecord::Reflection::HasAndBelongsToManyReflection] The association
178
+ # @return [String] The generated foreign key name
179
+ def generate_association_foreign_key(assoc)
180
+ assoc.options[:class_name].underscore.singularize + '_id'
181
+ end
182
+
183
+ # Gets a constant by its fully qualified name
184
+ # @param class_name [String] The fully qualified class name
185
+ # @return [Class] The resolved class
74
186
  def const_get(class_name)
75
187
  class_name.split('::').inject(Object) { |mod, name| mod.const_get(name) }
76
188
  end
@@ -1,15 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # DynamicActiveModel module for create ActiveRecord models
3
+ # DynamicActiveModel is a Ruby gem that provides automatic database discovery,
4
+ # model creation, and relationship mapping for Rails applications.
5
+ #
6
+ # It allows developers to dynamically create ActiveRecord models from existing
7
+ # database schemas without manually writing model classes. The gem automatically
8
+ # detects and sets up relationships including has_many, belongs_to, has_one,
9
+ # and has_and_belongs_to_many based on database constraints.
10
+ #
11
+ # @example Basic Usage
12
+ # module DB; end
13
+ # DynamicActiveModel::Explorer.explore(DB, database_config)
14
+ #
15
+ # @example Model Extension
16
+ # db = DynamicActiveModel::Database.new(DB, database_config)
17
+ # db.update_model(:users) do
18
+ # def full_name
19
+ # "#{first_name} #{last_name}"
20
+ # end
21
+ # end
4
22
  module DynamicActiveModel
23
+ # Database class handles database connection and model creation
5
24
  autoload :Database, 'dynamic-active-model/database'
25
+
26
+ # Safety feature that prevents conflicts with Ruby reserved words
6
27
  autoload :DangerousAttributesPatch, 'dynamic-active-model/dangerous_attributes_patch'
28
+
29
+ # High-level interface for model discovery and relationship mapping
7
30
  autoload :Explorer, 'dynamic-active-model/explorer'
31
+
32
+ # Manages the creation of model classes
8
33
  autoload :Factory, 'dynamic-active-model/factory'
34
+
35
+ # Handles foreign key relationships and constraints
9
36
  autoload :ForeignKey, 'dynamic-active-model/foreign_key'
37
+
38
+ # Manages automatic discovery and setup of model relationships
10
39
  autoload :Associations, 'dynamic-active-model/associations'
40
+
41
+ # Handles generation of model class files
11
42
  autoload :TemplateClassFile, 'dynamic-active-model/template_class_file'
43
+
44
+ # Manages the setup process and configuration
12
45
  autoload :Setup, 'dynamic-active-model/setup'
13
46
 
47
+ # Raised when a requested model cannot be found
14
48
  class ModelNotFound < StandardError; end
15
49
  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.6.1
4
+ version: 0.6.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Doug Youch
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-05-02 00:00:00.000000000 Z
10
+ date: 2025-05-10 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord