dynamic-active-model 0.6.4 → 0.7.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: 14a04d1dd0784f4300e46dd358b5ad392cac0f1244b6a7520465df06ae2bb84a
4
- data.tar.gz: ad0cf1bda6db17ee7354d64c24dc42cd9330f8404b1b98c10a15f87decde094d
3
+ metadata.gz: c613724f6f06d888232f59f4e35ec8040134d5929d4098ee404bb1c70389b1bd
4
+ data.tar.gz: 1c98814db2e5123bcc43357e02b1ed100ea5f03088bdf1365d03c84cb1e71359
5
5
  SHA512:
6
- metadata.gz: 8000cf349d155abaf9feec697a440e3a9d1db4112cb2257005900b712774a6d05b5c41f5f68f8ca2a05443385e202b62ea5ffa021c92a47e6038ad7b65213107
7
- data.tar.gz: 7adf72224a086374d1b54034d6f3ada3fb05537acc3d031dfdaf701658dd5cc05121f232f4be80075070db54750f48ad714d18e0b2fd9f0ff2d8aab7bae77bd2
6
+ metadata.gz: 537abf6be0b70bf1ac9a4ae74cec03084fe866afb66cc23d4cbf612df2e45a3dd9543c2cc231ab4b3c2c86b6be9037037eebc35517e29dca7988903382418c0c
7
+ data.tar.gz: fb4e92f9e2dcdc33aac80dae1058eafb996ceaaf4054f5a68f91a1733bd4702fa93446d8d259389276a7a5422ee67afad14dcc6ffdef6a80e917cc0290e25583
@@ -5,7 +5,7 @@ require 'dynamic-active-model'
5
5
  require 'optparse'
6
6
  require 'yaml'
7
7
 
8
- DEFAULT_CONFIG_FILE = "#{ENV['HOME']}/.dynamic-db-explorer.yml".freeze
8
+ DEFAULT_CONFIG_FILE = "#{Dir.home}/.dynamic-db-explorer.yml".freeze
9
9
 
10
10
  options = {
11
11
  base_module: Object,
@@ -56,6 +56,8 @@ OptionParser.new do |opts|
56
56
  end
57
57
 
58
58
  opts.on('--module MODULE_NAME', 'module name') do |v|
59
+ raise ArgumentError, "Invalid module name: #{v}" unless v.match?(/\A[A-Z][A-Za-z0-9_]*\z/)
60
+
59
61
  Object.const_set(v, Module.new)
60
62
  options[:base_module] = Object.const_get(v)
61
63
  end
@@ -86,10 +88,12 @@ DynamicActiveModel::ForeignKey.id_suffix = options[:id_suffix] if options[:id_su
86
88
  if options[:config_file]
87
89
  raise('must specify a section') unless options[:config_section]
88
90
 
89
- config = YAML.load_file(options[:config_file])[options[:config_section]]
91
+ config = YAML.safe_load_file(options[:config_file], permitted_classes: [Symbol])[options[:config_section]]
90
92
  options[:skip_tables] = config.delete('skip_tables') || []
91
93
  options[:relationships] = config.delete('relationships') || {}
92
94
  if (module_name = config.delete('module'))
95
+ raise ArgumentError, "Invalid module name: #{module_name}" unless module_name.match?(/\A[A-Z][A-Za-z0-9_]*\z/)
96
+
93
97
  Object.const_set(module_name, Module.new)
94
98
  options[:base_module] = Object.const_get(module_name)
95
99
  end
@@ -20,10 +20,10 @@ module DynamicActiveModel
20
20
  class Associations
21
21
  # @return [Database] The database instance containing the models
22
22
  attr_reader :database
23
-
23
+
24
24
  # @return [Hash] Mapping of table names to their indexes
25
25
  attr_reader :table_indexes
26
-
26
+
27
27
  # @return [Array] List of detected join tables
28
28
  attr_reader :join_tables
29
29
 
@@ -72,10 +72,10 @@ module DynamicActiveModel
72
72
  end
73
73
 
74
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
75
+ models = join_table_model.column_names.map do |column_name|
76
+ foreign_key_to_models[column_name.downcase]&.first&.first
77
+ end.compact
78
+ add_has_and_belongs_to_many(join_table_model, models) if models.size == 2
79
79
  end
80
80
  end
81
81
 
@@ -86,8 +86,10 @@ module DynamicActiveModel
86
86
  # @param models [Array<Class>] The two models to be related
87
87
  def add_has_and_belongs_to_many(join_table_model, models)
88
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
89
+ model1.has_and_belongs_to_many model2.table_name.pluralize.to_sym, join_table: join_table_model.table_name,
90
+ class_name: model2.name
91
+ model2.has_and_belongs_to_many model1.table_name.pluralize.to_sym, join_table: join_table_model.table_name,
92
+ class_name: model1.name
91
93
  end
92
94
 
93
95
  # Adds appropriate relationships between two models
@@ -150,7 +152,7 @@ module DynamicActiveModel
150
152
  # @return [Hash] Mapping of foreign key names to model and relationship name pairs
151
153
  def create_foreign_key_to_model_map
152
154
  @foreign_keys.values.each_with_object({}) do |foreign_key, hsh|
153
- foreign_key.keys.each do |key, relationship_name|
155
+ foreign_key.keys.each do |key, relationship_name| # rubocop:disable Style/HashEachMethods
154
156
  hsh[key.downcase] ||= []
155
157
  hsh[key.downcase] << [foreign_key.model, relationship_name]
156
158
  end
@@ -25,10 +25,10 @@ module DynamicActiveModel
25
25
  class Database
26
26
  # @return [Hash] Mapping of table names to custom class names
27
27
  attr_reader :table_class_names
28
-
28
+
29
29
  # @return [Factory] Factory instance used for model creation
30
30
  attr_reader :factory
31
-
31
+
32
32
  # @return [Array] List of created model classes
33
33
  attr_reader :models
34
34
 
@@ -18,7 +18,7 @@ module DynamicActiveModel
18
18
  class ForeignKey
19
19
  # @return [Class] The model this foreign key belongs to
20
20
  attr_reader :model
21
-
21
+
22
22
  # @return [Hash] Mapping of foreign key columns to relationship names
23
23
  attr_reader :keys
24
24
 
@@ -33,8 +33,8 @@ module DynamicActiveModel
33
33
 
34
34
  # Sets a custom foreign key suffix
35
35
  # @param val [String] The new suffix to use
36
- def self.id_suffix=(val)
37
- @id_suffix = val
36
+ class << self
37
+ attr_writer :id_suffix
38
38
  end
39
39
 
40
40
  # Initializes a new ForeignKey instance
@@ -59,12 +59,12 @@ module DynamicActiveModel
59
59
  if options.is_a?(String)
60
60
  name = options
61
61
  options = ActiveRecord::Base
62
- .configurations
63
- .configs_for(
64
- env_name: Rails.env,
65
- name: name
66
- )
67
- .configuration_hash
62
+ .configurations
63
+ .configs_for(
64
+ env_name: Rails.env,
65
+ name: name
66
+ )
67
+ .configuration_hash
68
68
  end
69
69
 
70
70
  if options
@@ -30,15 +30,17 @@ module DynamicActiveModel
30
30
  # @param dir [String] Directory to create the file in
31
31
  # @return [void]
32
32
  def create_template!(dir)
33
- file = dir + '/' + @model.name.underscore + '.rb'
34
- File.open(file, 'wb') { |f| f.write(to_s) }
33
+ file = "#{dir}/#{@model.name.underscore}.rb"
34
+ File.binwrite(file, to_s)
35
35
  end
36
36
 
37
37
  # Generates the Ruby source code for the model
38
38
  # @return [String] The complete model class definition
39
39
  def to_s
40
- str = "class #{@model.name} < ActiveRecord::Base\n".dup
41
- str << " self.table_name = #{@model.table_name.to_sym.inspect}\n" unless @model.name.underscore.pluralize == @model.table_name
40
+ str = "class #{@model.name} < ActiveRecord::Base\n"
41
+ unless @model.name.underscore.pluralize == @model.table_name
42
+ str << " self.table_name = #{@model.table_name.to_sym.inspect}\n"
43
+ end
42
44
  all_has_many_relationships.each do |assoc|
43
45
  append_association!(str, assoc)
44
46
  end
@@ -105,15 +107,15 @@ module DynamicActiveModel
105
107
  end
106
108
 
107
109
  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
110
+ when 'has_many'
111
+ has_many_association_options(assoc)
112
+ when 'belongs_to'
113
+ belongs_to_association_options(assoc)
114
+ when 'has_one'
115
+ has_one_association_options(assoc)
116
+ when 'has_and_belongs_to_many'
117
+ has_and_belongs_to_many_association_options(assoc)
118
+ end
117
119
 
118
120
  str << " #{assoc_type} #{assoc.name.inspect}"
119
121
  unless association_options.empty?
@@ -129,7 +131,10 @@ module DynamicActiveModel
129
131
  # @return [Hash] The association options
130
132
  def has_many_association_options(assoc)
131
133
  options = {}
132
- options[:class_name] = assoc.options[:class_name] unless assoc.options[:class_name].underscore.pluralize == assoc.name.to_s
134
+ unless assoc.options[:class_name].underscore.pluralize == assoc.name.to_s
135
+ options[:class_name] =
136
+ assoc.options[:class_name]
137
+ end
133
138
  options[:foreign_key] = assoc.options[:foreign_key] unless assoc.options[:foreign_key] == default_foreign_key_name
134
139
  options[:primary_key] = assoc.options[:primary_key] unless assoc.options[:primary_key] == 'id'
135
140
  options
@@ -141,7 +146,10 @@ module DynamicActiveModel
141
146
  def belongs_to_association_options(assoc)
142
147
  options = {}
143
148
  options[:class_name] = assoc.options[:class_name] unless assoc.options[:class_name] == assoc.name.to_s.classify
144
- options[:foreign_key] = assoc.options[:foreign_key] unless assoc.options[:foreign_key] == (assoc.options[:class_name].underscore + '_id')
149
+ unless assoc.options[:foreign_key] == "#{assoc.options[:class_name].underscore}_id"
150
+ options[:foreign_key] =
151
+ assoc.options[:foreign_key]
152
+ end
145
153
  options[:primary_key] = assoc.options[:primary_key] unless assoc.options[:primary_key] == 'id'
146
154
  options
147
155
  end
@@ -151,7 +159,10 @@ module DynamicActiveModel
151
159
  # @return [Hash] The association options
152
160
  def has_one_association_options(assoc)
153
161
  options = {}
154
- options[:class_name] = assoc.options[:class_name] unless assoc.options[:class_name].underscore.singularize == assoc.name.to_s
162
+ unless assoc.options[:class_name].underscore.singularize == assoc.name.to_s
163
+ options[:class_name] =
164
+ assoc.options[:class_name]
165
+ end
155
166
  options[:foreign_key] = assoc.options[:foreign_key] unless assoc.options[:foreign_key] == default_foreign_key_name
156
167
  options[:primary_key] = assoc.options[:primary_key] unless assoc.options[:primary_key] == 'id'
157
168
  options
@@ -163,21 +174,24 @@ module DynamicActiveModel
163
174
  def has_and_belongs_to_many_association_options(assoc)
164
175
  options = {}
165
176
  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
177
+ unless assoc.options[:class_name].underscore.singularize == assoc.name.to_s
178
+ options[:class_name] =
179
+ assoc.options[:class_name]
180
+ end
167
181
  options
168
182
  end
169
183
 
170
184
  # Gets the default foreign key name for the model
171
185
  # @return [String] The default foreign key name
172
186
  def default_foreign_key_name
173
- @model.table_name.underscore.singularize + '_id'
187
+ "#{@model.table_name.underscore.singularize}_id"
174
188
  end
175
189
 
176
190
  # Generates the default foreign key for the associated model
177
191
  # @param assoc [ActiveRecord::Reflection::HasAndBelongsToManyReflection] The association
178
192
  # @return [String] The generated foreign key name
179
193
  def generate_association_foreign_key(assoc)
180
- assoc.options[:class_name].underscore.singularize + '_id'
194
+ "#{assoc.options[:class_name].underscore.singularize}_id"
181
195
  end
182
196
 
183
197
  # Gets a constant by its fully qualified name
@@ -22,25 +22,25 @@
22
22
  module DynamicActiveModel
23
23
  # Database class handles database connection and model creation
24
24
  autoload :Database, 'dynamic-active-model/database'
25
-
25
+
26
26
  # Safety feature that prevents conflicts with Ruby reserved words
27
27
  autoload :DangerousAttributesPatch, 'dynamic-active-model/dangerous_attributes_patch'
28
-
28
+
29
29
  # High-level interface for model discovery and relationship mapping
30
30
  autoload :Explorer, 'dynamic-active-model/explorer'
31
-
31
+
32
32
  # Manages the creation of model classes
33
33
  autoload :Factory, 'dynamic-active-model/factory'
34
-
34
+
35
35
  # Handles foreign key relationships and constraints
36
36
  autoload :ForeignKey, 'dynamic-active-model/foreign_key'
37
-
37
+
38
38
  # Manages automatic discovery and setup of model relationships
39
39
  autoload :Associations, 'dynamic-active-model/associations'
40
-
40
+
41
41
  # Handles generation of model class files
42
42
  autoload :TemplateClassFile, 'dynamic-active-model/template_class_file'
43
-
43
+
44
44
  # Manages the setup process and configuration
45
45
  autoload :Setup, 'dynamic-active-model/setup'
46
46
 
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.4
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Doug Youch
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-05-10 00:00:00.000000000 Z
10
+ date: 2026-01-31 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord
@@ -37,7 +37,11 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0.2'
40
- description: Dynamically create ActiveRecord models for tables
40
+ description: Dynamic Active Model automatically discovers database schemas and creates
41
+ ActiveRecord models without manual class definitions. It detects and configures
42
+ relationships (belongs_to, has_many, has_one, has_and_belongs_to_many) based on
43
+ foreign keys and constraints, handles dangerous attribute names, supports model
44
+ extensions, and includes a CLI tool for interactive database exploration.
41
45
  email: dougyouch@gmail.com
42
46
  executables:
43
47
  - dynamic-db-explorer
@@ -57,7 +61,8 @@ files:
57
61
  homepage: https://github.com/dougyouch/dynamic-active-model
58
62
  licenses:
59
63
  - MIT
60
- metadata: {}
64
+ metadata:
65
+ rubygems_mfa_required: 'true'
61
66
  rdoc_options: []
62
67
  require_paths:
63
68
  - lib
@@ -74,5 +79,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
79
  requirements: []
75
80
  rubygems_version: 3.6.2
76
81
  specification_version: 4
77
- summary: Dynamic ActiveRecord Models
82
+ summary: Automatically discover database schemas and create ActiveRecord models with
83
+ relationships
78
84
  test_files: []