Skip to content

Commit 939d532

Browse files
committed
Add automatic acts_as declaration updates to upgrade generator
- Upgrade generator now automatically rewrites existing models' acts_as declarations - Updates Chat, Message, and ToolCall models to include proper model associations - Create shared GeneratorHelpers module to DRY up generator code - Add automatic namespace module creation with table_name_prefix for namespaced models - Fix template paths in upgrade generator to use source_paths correctly - Update docs to highlight automatic acts_as updates as key feature - Delete implementation detail tests that check source code strings
1 parent 1a5ad13 commit 939d532

File tree

6 files changed

+153
-198
lines changed

6 files changed

+153
-198
lines changed

docs/_advanced/upgrading-to-1.7.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ rails db:migrate
3434
That's it! The generator:
3535
- Creates the models table if needed
3636
- Automatically adds `config.use_new_acts_as = true` to your initializer
37+
- Automatically updates your existing models' `acts_as` declarations to the new version
3738
- Migrates your existing data to use foreign keys
39+
- Loads the models in the db
3840
- Preserves all your data (old string columns renamed to `model_id_string`)
3941

4042
### Custom Model Names
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# frozen_string_literal: true
2+
3+
module RubyLLM
4+
# Shared helpers for RubyLLM generators
5+
module GeneratorHelpers
6+
def parse_model_mappings
7+
@model_names = {
8+
chat: 'Chat',
9+
message: 'Message',
10+
tool_call: 'ToolCall',
11+
model: 'Model'
12+
}
13+
14+
model_mappings.each do |mapping|
15+
if mapping.include?(':')
16+
key, value = mapping.split(':', 2)
17+
@model_names[key.to_sym] = value.classify
18+
end
19+
end
20+
21+
@model_names
22+
end
23+
24+
%i[chat message tool_call model].each do |type|
25+
define_method("#{type}_model_name") do
26+
@model_names ||= parse_model_mappings
27+
@model_names[type]
28+
end
29+
30+
define_method("#{type}_table_name") do
31+
table_name_for(send("#{type}_model_name"))
32+
end
33+
end
34+
35+
def acts_as_chat_declaration
36+
params = []
37+
38+
add_association_params(params, :messages, message_table_name, message_model_name, plural: true)
39+
add_association_params(params, :model, model_table_name, model_model_name)
40+
41+
"acts_as_chat#{" #{params.join(', ')}" if params.any?}"
42+
end
43+
44+
def acts_as_message_declaration
45+
params = []
46+
47+
add_association_params(params, :chat, chat_table_name, chat_model_name)
48+
add_association_params(params, :tool_calls, tool_call_table_name, tool_call_model_name, plural: true)
49+
add_association_params(params, :model, model_table_name, model_model_name)
50+
51+
"acts_as_message#{" #{params.join(', ')}" if params.any?}"
52+
end
53+
54+
def acts_as_model_declaration
55+
params = []
56+
57+
add_association_params(params, :chats, chat_table_name, chat_model_name, plural: true)
58+
59+
"acts_as_model#{" #{params.join(', ')}" if params.any?}"
60+
end
61+
62+
def acts_as_tool_call_declaration
63+
params = []
64+
65+
add_association_params(params, :message, message_table_name, message_model_name)
66+
67+
"acts_as_tool_call#{" #{params.join(', ')}" if params.any?}"
68+
end
69+
70+
def create_namespace_modules
71+
namespaces = []
72+
73+
[chat_model_name, message_model_name, tool_call_model_name, model_model_name].each do |model_name|
74+
if model_name.include?('::')
75+
namespace = model_name.split('::').first
76+
namespaces << namespace unless namespaces.include?(namespace)
77+
end
78+
end
79+
80+
namespaces.each do |namespace|
81+
module_path = "app/models/#{namespace.underscore}.rb"
82+
next if File.exist?(Rails.root.join(module_path))
83+
84+
create_file module_path do
85+
<<~RUBY
86+
module #{namespace}
87+
def self.table_name_prefix
88+
"#{namespace.underscore}_"
89+
end
90+
end
91+
RUBY
92+
end
93+
end
94+
end
95+
96+
def migration_version
97+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
98+
end
99+
100+
def postgresql?
101+
::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
102+
rescue StandardError
103+
false
104+
end
105+
106+
def table_exists?(table_name)
107+
::ActiveRecord::Base.connection.table_exists?(table_name)
108+
rescue StandardError
109+
false
110+
end
111+
112+
private
113+
114+
def add_association_params(params, default_assoc, table_name, model_name, plural: false)
115+
assoc = plural ? table_name.to_sym : table_name.singularize.to_sym
116+
117+
return if assoc == default_assoc
118+
119+
params << "#{default_assoc}: :#{assoc}"
120+
params << "#{default_assoc.to_s.singularize}_class: '#{model_name}'" if model_name != assoc.to_s.classify
121+
end
122+
123+
def table_name_for(model_name)
124+
# Convert namespaced model names to proper table names
125+
# e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
126+
model_name.underscore.pluralize.tr('/', '_')
127+
end
128+
end
129+
end

lib/generators/ruby_llm/install/install_generator.rb

Lines changed: 4 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
require 'rails/generators'
44
require 'rails/generators/active_record'
5+
require_relative '../generator_helpers'
56

67
module RubyLLM
78
# Generator for RubyLLM Rails models and migrations
89
class InstallGenerator < Rails::Generators::Base
910
include Rails::Generators::Migration
11+
include RubyLLM::GeneratorHelpers
1012

1113
namespace 'ruby_llm:install'
1214

@@ -24,80 +26,6 @@ def self.next_migration_number(dirname)
2426
::ActiveRecord::Generators::Base.next_migration_number(dirname)
2527
end
2628

27-
def migration_version
28-
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
29-
end
30-
31-
def postgresql?
32-
::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
33-
rescue StandardError
34-
false
35-
end
36-
37-
def parse_model_mappings
38-
@model_names = {
39-
chat: 'Chat',
40-
message: 'Message',
41-
tool_call: 'ToolCall',
42-
model: 'Model'
43-
}
44-
45-
model_mappings.each do |mapping|
46-
if mapping.include?(':')
47-
key, value = mapping.split(':', 2)
48-
@model_names[key.to_sym] = value.classify
49-
end
50-
end
51-
52-
@model_names
53-
end
54-
55-
%i[chat message tool_call model].each do |type|
56-
define_method("#{type}_model_name") do
57-
@model_names ||= parse_model_mappings
58-
@model_names[type]
59-
end
60-
61-
define_method("#{type}_table_name") do
62-
table_name_for(send("#{type}_model_name"))
63-
end
64-
end
65-
66-
def acts_as_chat_declaration
67-
params = []
68-
69-
add_association_params(params, :messages, message_table_name, message_model_name, plural: true)
70-
add_association_params(params, :model, model_table_name, model_model_name)
71-
72-
"acts_as_chat#{" #{params.join(', ')}" if params.any?}"
73-
end
74-
75-
def acts_as_message_declaration
76-
params = []
77-
78-
add_association_params(params, :chat, chat_table_name, chat_model_name)
79-
add_association_params(params, :tool_calls, tool_call_table_name, tool_call_model_name, plural: true)
80-
add_association_params(params, :model, model_table_name, model_model_name)
81-
82-
"acts_as_message#{" #{params.join(', ')}" if params.any?}"
83-
end
84-
85-
def acts_as_model_declaration
86-
params = []
87-
88-
add_association_params(params, :chats, chat_table_name, chat_model_name, plural: true)
89-
90-
"acts_as_model#{" #{params.join(', ')}" if params.any?}"
91-
end
92-
93-
def acts_as_tool_call_declaration
94-
params = []
95-
96-
add_association_params(params, :message, message_table_name, message_model_name)
97-
98-
"acts_as_tool_call#{" #{params.join(', ')}" if params.any?}"
99-
end
100-
10129
def create_migration_files
10230
# Create migrations with timestamps to ensure proper order
10331
# First create chats table
@@ -121,6 +49,8 @@ def create_migration_files
12149
end
12250

12351
def create_model_files
52+
create_namespace_modules
53+
12454
template 'chat_model.rb.tt', "app/models/#{chat_model_name.underscore}.rb"
12555
template 'message_model.rb.tt', "app/models/#{message_model_name.underscore}.rb"
12656
template 'tool_call_model.rb.tt', "app/models/#{tool_call_model_name.underscore}.rb"
@@ -170,22 +100,5 @@ def show_install_info
170100
say ' • 🐦 Follow for updates: https://x.com/paolino'
171101
say "\n"
172102
end
173-
174-
private
175-
176-
def add_association_params(params, default_assoc, table_name, model_name, plural: false)
177-
assoc = plural ? table_name.to_sym : table_name.singularize.to_sym
178-
179-
return if assoc == default_assoc
180-
181-
params << "#{default_assoc}: :#{assoc}"
182-
params << "#{default_assoc.to_s.singularize}_class: '#{model_name}'" if model_name != assoc.to_s.classify
183-
end
184-
185-
def table_name_for(model_name)
186-
# Convert namespaced model names to proper table names
187-
# e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
188-
model_name.underscore.pluralize.tr('/', '_')
189-
end
190103
end
191104
end

lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,7 @@ class Create<%= model_model_name.gsub('::', '').pluralize %> < ActiveRecord::Mig
3434
# Load models from JSON
3535
say_with_time "Loading models from models.json" do
3636
RubyLLM.models.load_from_json!
37-
model_class = '<%= model_model_name %>'.constantize
38-
model_class.save_to_database
39-
40-
"Loaded #{model_class.count} models"
37+
<%= model_model_name %>.save_to_database
4138
end
4239
end
4340
end

0 commit comments

Comments
 (0)