claude-code-templates 1.10.0 → 1.10.1
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.
- package/package.json +2 -2
- package/src/hook-scanner.js +21 -1
- package/src/templates.js +24 -0
- package/src/utils.js +46 -0
- package/templates/ruby/.claude/commands/model.md +360 -0
- package/templates/ruby/.claude/commands/test.md +480 -0
- package/templates/ruby/.claude/settings.json +146 -0
- package/templates/ruby/.mcp.json +83 -0
- package/templates/ruby/CLAUDE.md +284 -0
- package/templates/ruby/examples/rails-app/.claude/commands/authentication.md +490 -0
- package/templates/ruby/examples/rails-app/CLAUDE.md +376 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-templates",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.1",
|
|
4
4
|
"description": "CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"dev:link": "npm link",
|
|
32
32
|
"dev:unlink": "npm unlink -g claude-code-templates",
|
|
33
33
|
"pretest:commands": "npm run dev:link",
|
|
34
|
-
"prepublishOnly": "echo 'Skipping tests for
|
|
34
|
+
"prepublishOnly": "echo 'Skipping tests for Ruby on Rails 8 release'",
|
|
35
35
|
"analytics:start": "node src/analytics.js",
|
|
36
36
|
"analytics:test": "npm run test:analytics"
|
|
37
37
|
},
|
package/src/hook-scanner.js
CHANGED
|
@@ -123,6 +123,10 @@ function getHookDescription(hook, matcher, type) {
|
|
|
123
123
|
return 'Block print() statements in Python files';
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
if (command.includes('puts\\|p ') && command.includes('rb$')) {
|
|
127
|
+
return 'Block puts/p statements in Ruby files';
|
|
128
|
+
}
|
|
129
|
+
|
|
126
130
|
if (command.includes('fmt.Print') && command.includes('go$')) {
|
|
127
131
|
return 'Block fmt.Print statements in Go files';
|
|
128
132
|
}
|
|
@@ -131,7 +135,7 @@ function getHookDescription(hook, matcher, type) {
|
|
|
131
135
|
return 'Block println! macros in Rust files';
|
|
132
136
|
}
|
|
133
137
|
|
|
134
|
-
if (command.includes('npm audit') || command.includes('pip-audit') || command.includes('cargo audit')) {
|
|
138
|
+
if (command.includes('npm audit') || command.includes('pip-audit') || command.includes('bundle audit') || command.includes('cargo audit')) {
|
|
135
139
|
return 'Security audit for dependencies';
|
|
136
140
|
}
|
|
137
141
|
|
|
@@ -143,6 +147,18 @@ function getHookDescription(hook, matcher, type) {
|
|
|
143
147
|
return 'Auto-format Python files with Black';
|
|
144
148
|
}
|
|
145
149
|
|
|
150
|
+
if (command.includes('rubocop -A') && command.includes('rb$')) {
|
|
151
|
+
return 'Auto-format Ruby files with RuboCop';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (command.includes('rubocop') && command.includes('rb$') && !command.includes('-A')) {
|
|
155
|
+
return 'Run Ruby linting with RuboCop';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (command.includes('brakeman')) {
|
|
159
|
+
return 'Run Ruby security scan with Brakeman';
|
|
160
|
+
}
|
|
161
|
+
|
|
146
162
|
if (command.includes('isort') && command.includes('py$')) {
|
|
147
163
|
return 'Auto-sort Python imports with isort';
|
|
148
164
|
}
|
|
@@ -195,6 +211,10 @@ function getHookDescription(hook, matcher, type) {
|
|
|
195
211
|
return 'Auto-run Python tests for modified files';
|
|
196
212
|
}
|
|
197
213
|
|
|
214
|
+
if (command.includes('rspec')) {
|
|
215
|
+
return 'Auto-run Ruby tests with RSpec';
|
|
216
|
+
}
|
|
217
|
+
|
|
198
218
|
if (command.includes('go test')) {
|
|
199
219
|
return 'Auto-run Go tests for modified files';
|
|
200
220
|
}
|
package/src/templates.js
CHANGED
|
@@ -76,6 +76,30 @@ const TEMPLATES_CONFIG = {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
},
|
|
79
|
+
'ruby': {
|
|
80
|
+
name: 'Ruby',
|
|
81
|
+
description: 'Optimized for Ruby development with modern tools',
|
|
82
|
+
files: [
|
|
83
|
+
{ source: 'ruby/CLAUDE.md', destination: 'CLAUDE.md' },
|
|
84
|
+
{ source: 'ruby/.claude', destination: '.claude' },
|
|
85
|
+
{ source: 'ruby/.mcp.json', destination: '.mcp.json' }
|
|
86
|
+
],
|
|
87
|
+
frameworks: {
|
|
88
|
+
'rails': {
|
|
89
|
+
name: 'Ruby on Rails 8',
|
|
90
|
+
additionalFiles: [
|
|
91
|
+
{ source: 'ruby/examples/rails-app/.claude/commands', destination: '.claude/commands' },
|
|
92
|
+
{ source: 'ruby/examples/rails-app/CLAUDE.md', destination: 'CLAUDE.md' }
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
'sinatra': {
|
|
96
|
+
name: 'Sinatra',
|
|
97
|
+
additionalFiles: [
|
|
98
|
+
{ source: 'ruby/examples/sinatra-app/.claude/commands', destination: '.claude/commands' }
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
79
103
|
'rust': {
|
|
80
104
|
name: 'Rust',
|
|
81
105
|
description: 'Optimized for Rust development',
|
package/src/utils.js
CHANGED
|
@@ -60,6 +60,50 @@ async function detectProject(targetDir) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// Check for Ruby files
|
|
64
|
+
const rubyFiles = await findFilesByExtension(targetDir, ['.rb']);
|
|
65
|
+
const gemfilePath = path.join(targetDir, 'Gemfile');
|
|
66
|
+
const gemfileLockPath = path.join(targetDir, 'Gemfile.lock');
|
|
67
|
+
|
|
68
|
+
if (rubyFiles.length > 0 || await fs.pathExists(gemfilePath)) {
|
|
69
|
+
detectedLanguages.push('ruby');
|
|
70
|
+
|
|
71
|
+
// Check for Ruby frameworks
|
|
72
|
+
if (await fs.pathExists(gemfilePath)) {
|
|
73
|
+
try {
|
|
74
|
+
const gemfile = await fs.readFile(gemfilePath, 'utf-8');
|
|
75
|
+
if (gemfile.includes('rails')) {
|
|
76
|
+
detectedFrameworks.push('rails');
|
|
77
|
+
}
|
|
78
|
+
if (gemfile.includes('sinatra')) {
|
|
79
|
+
detectedFrameworks.push('sinatra');
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.warn('Could not parse Gemfile');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check for Rails application structure
|
|
87
|
+
const railsAppPath = path.join(targetDir, 'config', 'application.rb');
|
|
88
|
+
const railsRoutesPath = path.join(targetDir, 'config', 'routes.rb');
|
|
89
|
+
if (await fs.pathExists(railsAppPath) || await fs.pathExists(railsRoutesPath)) {
|
|
90
|
+
detectedFrameworks.push('rails');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check for Rakefile (common in Rails and Ruby projects)
|
|
94
|
+
const rakefilePath = path.join(targetDir, 'Rakefile');
|
|
95
|
+
if (await fs.pathExists(rakefilePath)) {
|
|
96
|
+
try {
|
|
97
|
+
const rakefile = await fs.readFile(rakefilePath, 'utf-8');
|
|
98
|
+
if (rakefile.includes('Rails.application.load_tasks')) {
|
|
99
|
+
detectedFrameworks.push('rails');
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
// Ignore parsing errors
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
63
107
|
// Check for Rust files
|
|
64
108
|
const rustFiles = await findFilesByExtension(targetDir, ['.rs']);
|
|
65
109
|
const cargoPath = path.join(targetDir, 'Cargo.toml');
|
|
@@ -145,6 +189,7 @@ async function getProjectSummary(targetDir) {
|
|
|
145
189
|
hasGit: await fs.pathExists(path.join(targetDir, '.git')),
|
|
146
190
|
hasNodeModules: await fs.pathExists(path.join(targetDir, 'node_modules')),
|
|
147
191
|
hasVenv: await fs.pathExists(path.join(targetDir, 'venv')) || await fs.pathExists(path.join(targetDir, '.venv')),
|
|
192
|
+
hasBundle: await fs.pathExists(path.join(targetDir, 'vendor', 'bundle')),
|
|
148
193
|
configFiles: []
|
|
149
194
|
};
|
|
150
195
|
|
|
@@ -152,6 +197,7 @@ async function getProjectSummary(targetDir) {
|
|
|
152
197
|
const configFiles = [
|
|
153
198
|
'package.json', 'tsconfig.json', 'webpack.config.js', 'vite.config.js',
|
|
154
199
|
'requirements.txt', 'setup.py', 'pyproject.toml', 'Pipfile',
|
|
200
|
+
'Gemfile', 'Gemfile.lock', 'Rakefile', 'config.ru',
|
|
155
201
|
'Cargo.toml', 'go.mod', '.gitignore', 'README.md'
|
|
156
202
|
];
|
|
157
203
|
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# Ruby Model Generator
|
|
2
|
+
|
|
3
|
+
Create Ruby classes and models with proper structure and best practices.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This command helps you quickly create Ruby model classes with proper initialization, attributes, and methods following Ruby conventions.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
/model
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## What this command does
|
|
16
|
+
|
|
17
|
+
1. **Creates model classes** with proper initialization methods
|
|
18
|
+
2. **Adds attribute accessors** and instance variables
|
|
19
|
+
3. **Includes validation methods** and error handling
|
|
20
|
+
4. **Follows Ruby conventions** and best practices
|
|
21
|
+
5. **Generates related test files** when applicable
|
|
22
|
+
|
|
23
|
+
## Example Output
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
# app/models/user.rb
|
|
27
|
+
class User
|
|
28
|
+
attr_accessor :name, :email, :age
|
|
29
|
+
attr_reader :id, :created_at
|
|
30
|
+
|
|
31
|
+
def initialize(attributes = {})
|
|
32
|
+
@id = attributes[:id] || generate_id
|
|
33
|
+
@name = attributes[:name]
|
|
34
|
+
@email = attributes[:email]
|
|
35
|
+
@age = attributes[:age]
|
|
36
|
+
@created_at = Time.now
|
|
37
|
+
@errors = []
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def valid?
|
|
41
|
+
@errors.clear
|
|
42
|
+
validate_presence_of_name
|
|
43
|
+
validate_email_format
|
|
44
|
+
validate_age_range
|
|
45
|
+
@errors.empty?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def errors
|
|
49
|
+
@errors.dup
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def save
|
|
53
|
+
return false unless valid?
|
|
54
|
+
|
|
55
|
+
# Persist logic here
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_h
|
|
60
|
+
{
|
|
61
|
+
id: @id,
|
|
62
|
+
name: @name,
|
|
63
|
+
email: @email,
|
|
64
|
+
age: @age,
|
|
65
|
+
created_at: @created_at
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_json(*args)
|
|
70
|
+
JSON.generate(to_h, *args)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def validate_presence_of_name
|
|
76
|
+
if @name.nil? || @name.strip.empty?
|
|
77
|
+
@errors << "Name cannot be blank"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def validate_email_format
|
|
82
|
+
if @email && !@email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
|
|
83
|
+
@errors << "Email format is invalid"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def validate_age_range
|
|
88
|
+
if @age && (@age < 0 || @age > 150)
|
|
89
|
+
@errors << "Age must be between 0 and 150"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def generate_id
|
|
94
|
+
SecureRandom.uuid
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Example with inheritance
|
|
99
|
+
class AdminUser < User
|
|
100
|
+
attr_accessor :permissions
|
|
101
|
+
|
|
102
|
+
def initialize(attributes = {})
|
|
103
|
+
super
|
|
104
|
+
@permissions = attributes[:permissions] || []
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def admin?
|
|
108
|
+
true
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def has_permission?(permission)
|
|
112
|
+
@permissions.include?(permission)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def validate_permissions
|
|
118
|
+
unless @permissions.is_a?(Array)
|
|
119
|
+
@errors << "Permissions must be an array"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Advanced Features
|
|
126
|
+
|
|
127
|
+
### Module Inclusion
|
|
128
|
+
```ruby
|
|
129
|
+
module Timestamps
|
|
130
|
+
def self.included(base)
|
|
131
|
+
base.extend(ClassMethods)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
module ClassMethods
|
|
135
|
+
def with_timestamps
|
|
136
|
+
attr_reader :created_at, :updated_at
|
|
137
|
+
|
|
138
|
+
define_method :initialize do |*args|
|
|
139
|
+
super(*args)
|
|
140
|
+
@created_at ||= Time.now
|
|
141
|
+
@updated_at = @created_at
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
define_method :touch do
|
|
145
|
+
@updated_at = Time.now
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
class Post
|
|
152
|
+
include Timestamps
|
|
153
|
+
with_timestamps
|
|
154
|
+
|
|
155
|
+
attr_accessor :title, :content
|
|
156
|
+
|
|
157
|
+
def initialize(attributes = {})
|
|
158
|
+
@title = attributes[:title]
|
|
159
|
+
@content = attributes[:content]
|
|
160
|
+
super
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Class Methods and Scopes
|
|
166
|
+
```ruby
|
|
167
|
+
class User
|
|
168
|
+
@@users = []
|
|
169
|
+
|
|
170
|
+
def self.all
|
|
171
|
+
@@users.dup
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def self.find_by_email(email)
|
|
175
|
+
@@users.find { |user| user.email == email }
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def self.where(conditions = {})
|
|
179
|
+
@@users.select do |user|
|
|
180
|
+
conditions.all? { |key, value| user.send(key) == value }
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def self.create(attributes = {})
|
|
185
|
+
user = new(attributes)
|
|
186
|
+
if user.save
|
|
187
|
+
@@users << user
|
|
188
|
+
user
|
|
189
|
+
else
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def save
|
|
195
|
+
return false unless valid?
|
|
196
|
+
|
|
197
|
+
unless @@users.include?(self)
|
|
198
|
+
@@users << self
|
|
199
|
+
end
|
|
200
|
+
true
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def destroy
|
|
204
|
+
@@users.delete(self)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Testing Template
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
# spec/models/user_spec.rb
|
|
213
|
+
require 'spec_helper'
|
|
214
|
+
|
|
215
|
+
RSpec.describe User do
|
|
216
|
+
let(:valid_attributes) do
|
|
217
|
+
{
|
|
218
|
+
name: 'John Doe',
|
|
219
|
+
email: 'john@example.com',
|
|
220
|
+
age: 30
|
|
221
|
+
}
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
describe '#initialize' do
|
|
225
|
+
it 'sets attributes correctly' do
|
|
226
|
+
user = User.new(valid_attributes)
|
|
227
|
+
|
|
228
|
+
expect(user.name).to eq('John Doe')
|
|
229
|
+
expect(user.email).to eq('john@example.com')
|
|
230
|
+
expect(user.age).to eq(30)
|
|
231
|
+
expect(user.id).not_to be_nil
|
|
232
|
+
expect(user.created_at).to be_a(Time)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
describe '#valid?' do
|
|
237
|
+
it 'returns true for valid attributes' do
|
|
238
|
+
user = User.new(valid_attributes)
|
|
239
|
+
expect(user).to be_valid
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
it 'returns false when name is blank' do
|
|
243
|
+
user = User.new(valid_attributes.merge(name: ''))
|
|
244
|
+
expect(user).not_to be_valid
|
|
245
|
+
expect(user.errors).to include('Name cannot be blank')
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
it 'returns false for invalid email format' do
|
|
249
|
+
user = User.new(valid_attributes.merge(email: 'invalid-email'))
|
|
250
|
+
expect(user).not_to be_valid
|
|
251
|
+
expect(user.errors).to include('Email format is invalid')
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it 'returns false for invalid age' do
|
|
255
|
+
user = User.new(valid_attributes.merge(age: -5))
|
|
256
|
+
expect(user).not_to be_valid
|
|
257
|
+
expect(user.errors).to include('Age must be between 0 and 150')
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
describe '#save' do
|
|
262
|
+
it 'saves valid user' do
|
|
263
|
+
user = User.new(valid_attributes)
|
|
264
|
+
expect(user.save).to be true
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
it 'does not save invalid user' do
|
|
268
|
+
user = User.new(name: '')
|
|
269
|
+
expect(user.save).to be false
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
describe '#to_h' do
|
|
274
|
+
it 'returns hash representation' do
|
|
275
|
+
user = User.new(valid_attributes)
|
|
276
|
+
hash = user.to_h
|
|
277
|
+
|
|
278
|
+
expect(hash).to include(
|
|
279
|
+
name: 'John Doe',
|
|
280
|
+
email: 'john@example.com',
|
|
281
|
+
age: 30
|
|
282
|
+
)
|
|
283
|
+
expect(hash[:id]).not_to be_nil
|
|
284
|
+
expect(hash[:created_at]).to be_a(Time)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Best Practices Included
|
|
291
|
+
|
|
292
|
+
- **Proper initialization** with hash parameters
|
|
293
|
+
- **Attribute accessors** for public attributes
|
|
294
|
+
- **Validation methods** with error collection
|
|
295
|
+
- **JSON serialization** support
|
|
296
|
+
- **Class and instance methods** separation
|
|
297
|
+
- **Error handling** and reporting
|
|
298
|
+
- **Ruby naming conventions** (snake_case)
|
|
299
|
+
- **Encapsulation** with private methods
|
|
300
|
+
|
|
301
|
+
## Common Patterns
|
|
302
|
+
|
|
303
|
+
### Value Objects
|
|
304
|
+
```ruby
|
|
305
|
+
class Money
|
|
306
|
+
include Comparable
|
|
307
|
+
|
|
308
|
+
attr_reader :amount, :currency
|
|
309
|
+
|
|
310
|
+
def initialize(amount, currency = 'USD')
|
|
311
|
+
@amount = amount.to_f
|
|
312
|
+
@currency = currency.to_s.upcase
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def +(other)
|
|
316
|
+
raise ArgumentError, "Currency mismatch" unless currency == other.currency
|
|
317
|
+
Money.new(amount + other.amount, currency)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def <=>(other)
|
|
321
|
+
raise ArgumentError, "Currency mismatch" unless currency == other.currency
|
|
322
|
+
amount <=> other.amount
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def to_s
|
|
326
|
+
"#{currency} #{format('%.2f', amount)}"
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Service Objects
|
|
332
|
+
```ruby
|
|
333
|
+
class UserRegistrationService
|
|
334
|
+
attr_reader :user, :errors
|
|
335
|
+
|
|
336
|
+
def initialize(user_params)
|
|
337
|
+
@user_params = user_params
|
|
338
|
+
@errors = []
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def call
|
|
342
|
+
@user = User.new(@user_params)
|
|
343
|
+
|
|
344
|
+
if @user.valid?
|
|
345
|
+
@user.save
|
|
346
|
+
send_welcome_email
|
|
347
|
+
true
|
|
348
|
+
else
|
|
349
|
+
@errors = @user.errors
|
|
350
|
+
false
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
private
|
|
355
|
+
|
|
356
|
+
def send_welcome_email
|
|
357
|
+
# Email sending logic
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
```
|