yoker 0.1.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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +8 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +223 -0
  5. data/Rakefile +12 -0
  6. data/exe/yoker +177 -0
  7. data/exe/yoker (Copy) +87 -0
  8. data/lib/yoker/cli/base.rb +106 -0
  9. data/lib/yoker/cli/init.rb +193 -0
  10. data/lib/yoker/cli/update.rb +457 -0
  11. data/lib/yoker/configuration.rb +290 -0
  12. data/lib/yoker/detectors/database_detector.rb +35 -0
  13. data/lib/yoker/detectors/rails_detector.rb +48 -0
  14. data/lib/yoker/detectors/version_manager_detector.rb +91 -0
  15. data/lib/yoker/errors.rb +149 -0
  16. data/lib/yoker/generators/base_generator.rb +116 -0
  17. data/lib/yoker/generators/container/docker.rb +255 -0
  18. data/lib/yoker/generators/container/docker_compose.rb +255 -0
  19. data/lib/yoker/generators/container/none.rb +314 -0
  20. data/lib/yoker/generators/database/mysql.rb +147 -0
  21. data/lib/yoker/generators/database/postgresql.rb +64 -0
  22. data/lib/yoker/generators/database/sqlite.rb +123 -0
  23. data/lib/yoker/generators/version_manager/mise.rb +140 -0
  24. data/lib/yoker/generators/version_manager/rbenv.rb +165 -0
  25. data/lib/yoker/generators/version_manager/rvm.rb +246 -0
  26. data/lib/yoker/templates/bin/setup.rb.erb +232 -0
  27. data/lib/yoker/templates/config/database_mysql.yml.erb +47 -0
  28. data/lib/yoker/templates/config/database_postgresql.yml.erb +34 -0
  29. data/lib/yoker/templates/config/database_sqlite.yml.erb +40 -0
  30. data/lib/yoker/templates/docker/Dockerfile.erb +124 -0
  31. data/lib/yoker/templates/docker/docker-compose.yml.erb +117 -0
  32. data/lib/yoker/templates/docker/entrypoint.sh.erb +94 -0
  33. data/lib/yoker/templates/docker/init_mysql.sql.erb +44 -0
  34. data/lib/yoker/templates/docker/init_postgresql.sql.erb +203 -0
  35. data/lib/yoker/templates/version_managers/gemrc.erb +61 -0
  36. data/lib/yoker/templates/version_managers/mise.toml.erb +72 -0
  37. data/lib/yoker/templates/version_managers/rbenv_setup.sh.erb +93 -0
  38. data/lib/yoker/templates/version_managers/rvm_setup.sh.erb +99 -0
  39. data/lib/yoker/templates/version_managers/rvmrc.erb +70 -0
  40. data/lib/yoker/version.rb +5 -0
  41. data/lib/yoker.rb +32 -0
  42. data/sig/yoker.rbs +4 -0
  43. metadata +215 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1363e81437edece05648573f73cd9b68e8545b969a6a862cedffbbd753a52261
4
+ data.tar.gz: 057c0bdecb48574f069401362c1055e5c47236cde5241609a922241f38c14fd5
5
+ SHA512:
6
+ metadata.gz: b8d0862dc46af8411281d731dbe39eb963073a608b0830a465978bc2dbeb8cdad93b03ce0f3dce8b9c3fd40363736095d75d7113ff52c1cc986e29ecf15d908e
7
+ data.tar.gz: 700d09620026d2697116d6b47a5513498f633e353cb68024f10823bf6c690bcf02f576dbb66bb391d6cc92e521c35cd8b24186b86726c1e470589615395b90b2
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.1
3
+
4
+ Style/StringLiterals:
5
+ EnforcedStyle: double_quotes
6
+
7
+ Style/StringLiteralsInInterpolation:
8
+ EnforcedStyle: double_quotes
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Lance Taylor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,223 @@
1
+ # Yoker
2
+
3
+ > Automated development environment setup for Rails applications
4
+
5
+ Yoker eliminates the tedious manual configuration of Rails development environments by providing intelligent, automated setup with support for multiple databases, version managers, and containerization options.
6
+
7
+ ## ✨ Features
8
+
9
+ - **Smart Detection**: Automatically detects existing Rails apps, database configs, and version managers
10
+ - **Multiple Database Support**: PostgreSQL, MySQL, and SQLite3
11
+ - **Version Manager Integration**: mise, rbenv, and rvm support
12
+ - **Containerization Options**: Docker Compose, standalone Docker, or native setup
13
+ - **Interactive & Automated Modes**: Guided setup or command-line automation
14
+ - **Modern Tooling**: Built for Rails 7+ with cutting-edge development tools
15
+
16
+ ## 🚀 Quick Start
17
+
18
+ ```bash
19
+ # Install the gem
20
+ gem install yoker
21
+
22
+ # Navigate to your Rails app
23
+ cd my-rails-app
24
+
25
+ # Interactive setup (recommended for first-time users)
26
+ yoker init --interactive
27
+
28
+ # Or automated setup with specific options
29
+ yoker init \
30
+ --database=postgresql \
31
+ --version-manager=mise \
32
+ --container=docker-compose \
33
+ --ruby-version=3.2.0
34
+
35
+ # Run the generated setup
36
+ ./bin/setup
37
+ ```
38
+
39
+ ## 📋 Requirements
40
+
41
+ - Ruby 3.0+
42
+ - Rails application (any version)
43
+ - Docker (if using containerization)
44
+ - mise, rbenv, or rvm (if using version management)
45
+
46
+ ## đŸ› ī¸ Configuration Options
47
+
48
+ ### Databases
49
+ - **PostgreSQL** (recommended) - Latest PostgreSQL 18 with Alpine Linux
50
+ - **MySQL** - MySQL 8.0 with optimized configuration
51
+ - **SQLite3** - For simple development setups
52
+
53
+ ### Version Managers
54
+ - **mise** (recommended) - Modern, fast tool version management
55
+ - **rbenv** - Simple Ruby version management
56
+ - **rvm** - Ruby Version Manager
57
+ - **none** - Use system Ruby
58
+
59
+ ### Containerization
60
+ - **docker-compose** (recommended) - Full orchestration with services
61
+ - **docker** - Standalone database containers
62
+ - **none** - Native installation
63
+
64
+ ## 📁 Generated Files
65
+
66
+ Yoker creates and configures:
67
+
68
+ ```
69
+ your-rails-app/
70
+ ├── bin/setup # Enhanced setup script
71
+ ├── config/database.yml # Database configuration
72
+ ├── docker-compose.yml # Service orchestration
73
+ ├── Dockerfile # Container definition
74
+ ├── mise.toml # Tool version management
75
+ └── docker/
76
+ └── init.sql # Database initialization
77
+ ```
78
+
79
+ ## đŸŽ¯ Example Configurations
80
+
81
+ ### PostgreSQL + mise + Docker Compose
82
+ Perfect for modern Rails development with full containerization:
83
+
84
+ ```bash
85
+ yoker init \
86
+ --database=postgresql \
87
+ --version-manager=mise \
88
+ --container=docker-compose
89
+ ```
90
+
91
+ Generates:
92
+ - PostgreSQL 18 service with persistent data
93
+ - mise configuration with Ruby and Node.js
94
+ - Docker Compose with health checks
95
+ - Enhanced bin/setup with smart detection
96
+
97
+ ### MySQL + rbenv + Standalone Docker
98
+ Great for teams preferring rbenv with containerized database:
99
+
100
+ ```bash
101
+ yoker init \
102
+ --database=mysql \
103
+ --version-manager=rbenv \
104
+ --container=docker
105
+ ```
106
+
107
+ ### SQLite + Native Setup
108
+ Minimal setup for simple projects:
109
+
110
+ ```bash
111
+ yoker init \
112
+ --database=sqlite3 \
113
+ --container=none
114
+ ```
115
+
116
+ ## 🔧 Advanced Features
117
+
118
+ ### Additional Services
119
+ When using Docker Compose, you can include additional services:
120
+
121
+ - **Redis** - For caching and background jobs
122
+ - **Sidekiq** - Background job processing
123
+ - **Mailcatcher** - Email testing in development
124
+
125
+ ```bash
126
+ yoker init --interactive
127
+ # Select additional services during interactive setup
128
+ ```
129
+
130
+ ### mise Integration
131
+ Yoker generates comprehensive mise configurations:
132
+
133
+ ```toml
134
+ [tools]
135
+ ruby = "3.2.0"
136
+ node = "20.0.0"
137
+ postgres = "16" # When using native setup
138
+
139
+ [tasks."dev:setup"]
140
+ run = "./bin/setup"
141
+
142
+ [tasks."dev:server"]
143
+ run = "bin/rails server"
144
+
145
+ [env]
146
+ DATABASE_URL = "postgresql://postgres:password@localhost:5432/myapp_development"
147
+ ```
148
+
149
+ ### Smart Setup Script
150
+ The generated `bin/setup` script includes:
151
+
152
+ - ✅ Dependency installation with verification
153
+ - ✅ Tool version management integration
154
+ - ✅ Database container orchestration
155
+ - ✅ Health checks and wait conditions
156
+ - ✅ Database preparation and migrations
157
+ - ✅ Clear success/error messaging
158
+
159
+ ## 📊 Status Checking
160
+
161
+ Check your current development environment:
162
+
163
+ ```bash
164
+ yoker status
165
+ ```
166
+
167
+ Output:
168
+ ```
169
+ ✅ Rails application detected
170
+ Rails version: 7.1.2
171
+ ✅ Database: postgresql
172
+ ✅ Version manager: mise
173
+ Ruby version: 3.2.0
174
+ ✅ Docker Compose configuration found
175
+ ✅ Setup script ready (bin/setup)
176
+ ```
177
+
178
+ ## 🤝 Contributing
179
+
180
+ We welcome contributions! This gem aims to standardize Rails development environment setup across the community.
181
+
182
+ ### Development Setup
183
+
184
+ ```bash
185
+ git clone https://github.com/ecnal/yoker.git
186
+ cd yoker
187
+ bundle install
188
+ bundle exec rspec
189
+ ```
190
+
191
+ ### Contributing Guidelines
192
+
193
+ 1. **Fork** the repository
194
+ 2. **Create** a feature branch (`git checkout -b feature/amazing-feature`)
195
+ 3. **Add** tests for your changes
196
+ 4. **Ensure** tests pass (`bundle exec rspec`)
197
+ 5. **Commit** your changes (`git commit -m 'Add amazing feature'`)
198
+ 6. **Push** to the branch (`git push origin feature/amazing-feature`)
199
+ 7. **Open** a Pull Request
200
+
201
+ ## 🐛 Bug Reports & Feature Requests
202
+
203
+ Please use [GitHub Issues](https://github.com/ecnal/yoker/issues) for:
204
+
205
+ - 🐛 Bug reports with reproduction steps
206
+ - 💡 Feature requests with use cases
207
+ - 📖 Documentation improvements
208
+ - ❓ Questions about usage
209
+
210
+ ## 📜 License
211
+
212
+ Yoker is released under the [MIT License](LICENSE).
213
+
214
+ ## 🙏 Acknowledgments
215
+
216
+ - Rails team for the excellent framework
217
+ - mise developers for modern tool management
218
+ - Docker team for containerization technology
219
+ - The Ruby community for continuous innovation
220
+
221
+ ---
222
+
223
+ **Made with â¤ī¸ for the Rails community**
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
data/exe/yoker ADDED
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Add the lib directory to the load path
5
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
6
+
7
+ require "yoker"
8
+ require "yoker/cli/base"
9
+ require "yoker/cli/init"
10
+ require "yoker/cli/update"
11
+
12
+ module Yoker
13
+ module CLI
14
+ class Main < Base
15
+ desc "init", "Initialize development environment setup for Rails application"
16
+ method_option :database,
17
+ type: :string,
18
+ enum: %w[postgresql mysql sqlite3],
19
+ desc: "Database adapter to use"
20
+
21
+ method_option :version_manager,
22
+ type: :string,
23
+ enum: %w[mise rbenv rvm none],
24
+ desc: "Ruby version manager to use"
25
+
26
+ method_option :container,
27
+ type: :string,
28
+ enum: %w[docker-compose docker none],
29
+ desc: "Containerization approach"
30
+
31
+ method_option :ruby_version,
32
+ type: :string,
33
+ desc: "Ruby version to use"
34
+
35
+ method_option :interactive,
36
+ type: :boolean,
37
+ default: false,
38
+ aliases: %w[-i],
39
+ desc: "Run in interactive mode"
40
+
41
+ method_option :force,
42
+ type: :boolean,
43
+ default: false,
44
+ aliases: %w[-f],
45
+ desc: "Overwrite existing files"
46
+
47
+ def init
48
+ Init.new([], options).generate
49
+ end
50
+
51
+ desc "update", "Update existing development environment setup"
52
+ method_option :database,
53
+ type: :string,
54
+ enum: %w[postgresql mysql sqlite3],
55
+ desc: "Change database adapter"
56
+
57
+ method_option :version_manager,
58
+ type: :string,
59
+ enum: %w[mise rbenv rvm none],
60
+ desc: "Change Ruby version manager"
61
+
62
+ method_option :container,
63
+ type: :string,
64
+ enum: %w[docker-compose docker none],
65
+ desc: "Change containerization approach"
66
+
67
+ method_option :ruby_version,
68
+ type: :string,
69
+ desc: "Update Ruby version"
70
+
71
+ method_option :add_service,
72
+ type: :array,
73
+ enum: %w[redis sidekiq mailcatcher],
74
+ desc: "Add additional services"
75
+
76
+ method_option :remove_service,
77
+ type: :array,
78
+ enum: %w[redis sidekiq mailcatcher],
79
+ desc: "Remove services"
80
+
81
+ method_option :interactive,
82
+ type: :boolean,
83
+ default: false,
84
+ aliases: %w[-i],
85
+ desc: "Run in interactive mode"
86
+
87
+ method_option :backup,
88
+ type: :boolean,
89
+ default: true,
90
+ desc: "Backup existing files before updating"
91
+
92
+ method_option :dry_run,
93
+ type: :boolean,
94
+ default: false,
95
+ desc: "Show what would be changed without making changes"
96
+
97
+ def update
98
+ Update.new([], options).execute
99
+ end
100
+
101
+ desc "version", "Show yoker version"
102
+ def version
103
+ puts "yoker #{Yoker::VERSION}"
104
+ end
105
+
106
+ desc "status", "Show current development environment status"
107
+ def status
108
+ detect_rails_app!
109
+
110
+ info "Development Environment Status"
111
+ puts ""
112
+
113
+ # Rails detection
114
+ if Detectors::RailsDetector.rails_app?
115
+ success "Rails application detected"
116
+ if version = Detectors::RailsDetector.rails_version
117
+ puts " Rails version: #{version}"
118
+ end
119
+ else
120
+ error "Not a Rails application"
121
+ return
122
+ end
123
+
124
+ # Database detection
125
+ if db = Detectors::DatabaseDetector.detect
126
+ success "Database: #{db}"
127
+ else
128
+ warning "No database adapter detected"
129
+ end
130
+
131
+ # Version manager detection
132
+ if vm = Detectors::VersionManagerDetector.detect
133
+ success "Version manager: #{vm}"
134
+ if ruby_version = Detectors::VersionManagerDetector.ruby_version_from_file
135
+ puts " Ruby version: #{ruby_version}"
136
+ end
137
+ else
138
+ warning "No version manager detected"
139
+ end
140
+
141
+ # Container detection
142
+ if File.exist?("docker-compose.yml")
143
+ success "Docker Compose configuration found"
144
+ elsif File.exist?("Dockerfile")
145
+ success "Dockerfile found"
146
+ else
147
+ info "No containerization setup detected"
148
+ end
149
+
150
+ # Setup script detection
151
+ if File.exist?("bin/setup")
152
+ if File.executable?("bin/setup")
153
+ success "Setup script ready (bin/setup)"
154
+ else
155
+ warning "Setup script exists but not executable (bin/setup)"
156
+ end
157
+ else
158
+ info "No setup script found"
159
+ end
160
+ end
161
+
162
+ # Default task when no command is provided
163
+ def self.exit_on_failure?
164
+ true
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ # Run the CLI
171
+ begin
172
+ Yoker::CLI::Main.start(ARGV)
173
+ rescue StandardError => e
174
+ puts "\n❌ Error: #{e.message}"
175
+ puts e.backtrace.join("\n") if ENV["DEBUG"]
176
+ exit 1
177
+ end
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+
6
+ require_relative "../lib/yoker"
7
+ require_relative "../lib/yoker/cli/init"
8
+
9
+ module Yoker
10
+ module CLI
11
+ class Main < Base
12
+ desc "init", "Initialize development environment setup for Rails application"
13
+ def init
14
+ Init.new.generate
15
+ end
16
+
17
+ desc "update", "Update existing development environment setup"
18
+ def update
19
+ Update.new.run
20
+ end
21
+
22
+ desc "version", "Show yoker version"
23
+ def version
24
+ puts "yoker #{Yoker::VERSION}"
25
+ end
26
+
27
+ desc "status", "Show current development environment status"
28
+ def status
29
+ detect_rails_app!
30
+
31
+ info "Development Environment Status"
32
+ puts ""
33
+
34
+ # Rails detection
35
+ if Detectors::RailsDetector.rails_app?
36
+ success "Rails application detected"
37
+ if version = Detectors::RailsDetector.rails_version
38
+ puts " Rails version: #{version}"
39
+ end
40
+ else
41
+ error "Not a Rails application"
42
+ return
43
+ end
44
+
45
+ # Database detection
46
+ if db = Detectors::DatabaseDetector.detect
47
+ success "Database: #{db}"
48
+ else
49
+ warning "No database adapter detected"
50
+ end
51
+
52
+ # Version manager detection
53
+ if vm = Detectors::VersionManagerDetector.detect
54
+ success "Version manager: #{vm}"
55
+ if ruby_version = Detectors::VersionManagerDetector.ruby_version_from_file
56
+ puts " Ruby version: #{ruby_version}"
57
+ end
58
+ else
59
+ warning "No version manager detected"
60
+ end
61
+
62
+ # Container detection
63
+ if File.exist?("docker-compose.yml")
64
+ success "Docker Compose configuration found"
65
+ elsif File.exist?("Dockerfile")
66
+ success "Dockerfile found"
67
+ else
68
+ info "No containerization setup detected"
69
+ end
70
+
71
+ # Setup script detection
72
+ if File.exist?("bin/setup")
73
+ if File.executable?("bin/setup")
74
+ success "Setup script ready (bin/setup)"
75
+ else
76
+ warning "Setup script exists but not executable (bin/setup)"
77
+ end
78
+ else
79
+ info "No setup script found"
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ # Only run if this file is executed directly
87
+ Yoker::CLI::Main.start(ARGV) if __FILE__ == $PROGRAM_NAME
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "tty-prompt"
5
+ require "tty-spinner"
6
+ require "pastel"
7
+ require "fileutils"
8
+
9
+ module Yoker
10
+ module CLI
11
+ class Base < Thor
12
+ include Thor::Actions
13
+
14
+ def self.source_root
15
+ Yoker.template_path
16
+ end
17
+
18
+ def template(source, destination, context = {})
19
+ # Read the template file
20
+ template_path = File.join(self.class.source_root, source)
21
+
22
+ unless File.exist?(template_path)
23
+ error "Template not found: #{template_path}"
24
+ return
25
+ end
26
+
27
+ # Create the binding with the context
28
+ template_content = File.read(template_path)
29
+
30
+ # Use ERB to render the template
31
+ require "erb"
32
+ require "ostruct"
33
+
34
+ # Convert hash to OpenStruct for easier access in templates
35
+ binding_context = context.is_a?(Hash) ? OpenStruct.new(context) : context
36
+ rendered = ERB.new(template_content, trim_mode: "-").result(binding_context.instance_eval { binding })
37
+
38
+ # Write the file
39
+ create_file(destination, rendered, force: true)
40
+ end
41
+
42
+ def create_file(path, content, options = {})
43
+ return if File.exist?(path) && !options[:force] && !prompt.yes?("File #{path} exists. Overwrite?")
44
+
45
+ # Create directory if it doesn't exist
46
+ dir = File.dirname(path)
47
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
48
+
49
+ # Write the file
50
+ File.write(path, content)
51
+ success "Created #{path}"
52
+ end
53
+
54
+ def append_to_file(path, content)
55
+ File.open(path, "a") { |f| f.write(content) }
56
+ info "Updated #{path}"
57
+ end
58
+
59
+ private
60
+
61
+ def prompt
62
+ @prompt ||= TTY::Prompt.new
63
+ end
64
+
65
+ def pastel
66
+ @pastel ||= Pastel.new
67
+ end
68
+
69
+ def success(message)
70
+ puts pastel.green("✅ #{message}")
71
+ end
72
+
73
+ def info(message)
74
+ puts pastel.blue("â„šī¸ #{message}")
75
+ end
76
+
77
+ def warning(message)
78
+ puts pastel.yellow("âš ī¸ #{message}")
79
+ end
80
+
81
+ def error(message)
82
+ puts pastel.red("❌ #{message}")
83
+ end
84
+
85
+ def spinner(message)
86
+ TTY::Spinner.new("[:spinner] #{message}", format: :dots)
87
+ end
88
+
89
+ def detect_rails_app!
90
+ return if Detectors::RailsDetector.rails_app?
91
+
92
+ error "This doesn't appear to be a Rails application directory"
93
+ error "Please run this command from the root of a Rails project"
94
+ exit 1
95
+ end
96
+
97
+ def current_directory_name
98
+ File.basename(Dir.pwd)
99
+ end
100
+
101
+ def sanitize_name(name)
102
+ name.gsub(/[^a-zA-Z0-9]/, "_").downcase
103
+ end
104
+ end
105
+ end
106
+ end