rails_engine_toolkit 0.6.3

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 (64) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +54 -0
  3. data/.github/workflows/release.yml +22 -0
  4. data/Gemfile +11 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +83 -0
  7. data/Rakefile +7 -0
  8. data/docs/ARCHITECTURE.md +18 -0
  9. data/docs/COMPATIBILITY.md +18 -0
  10. data/docs/CONTRIBUTING.md +26 -0
  11. data/docs/END_TO_END.md +26 -0
  12. data/docs/PUBLISHING.md +24 -0
  13. data/docs/RELEASE.md +32 -0
  14. data/docs/RELEASE_CHECKLIST.md +49 -0
  15. data/docs/TESTING.md +21 -0
  16. data/exe/engine-toolkit +6 -0
  17. data/lib/rails_engine_toolkit/actions/delete_engine_migration.rb +46 -0
  18. data/lib/rails_engine_toolkit/actions/init.rb +54 -0
  19. data/lib/rails_engine_toolkit/actions/install_engine_migrations.rb +83 -0
  20. data/lib/rails_engine_toolkit/actions/new_engine.rb +127 -0
  21. data/lib/rails_engine_toolkit/actions/new_engine_migration.rb +30 -0
  22. data/lib/rails_engine_toolkit/actions/new_engine_model.rb +30 -0
  23. data/lib/rails_engine_toolkit/actions/remove_engine.rb +78 -0
  24. data/lib/rails_engine_toolkit/actions/uninstall_engine_migrations.rb +79 -0
  25. data/lib/rails_engine_toolkit/actions/update_engine_readme.rb +60 -0
  26. data/lib/rails_engine_toolkit/cli.rb +110 -0
  27. data/lib/rails_engine_toolkit/config.rb +98 -0
  28. data/lib/rails_engine_toolkit/errors.rb +7 -0
  29. data/lib/rails_engine_toolkit/file_editor.rb +40 -0
  30. data/lib/rails_engine_toolkit/generators/install/install_generator.rb +67 -0
  31. data/lib/rails_engine_toolkit/project.rb +71 -0
  32. data/lib/rails_engine_toolkit/railtie.rb +9 -0
  33. data/lib/rails_engine_toolkit/route_inspector.rb +44 -0
  34. data/lib/rails_engine_toolkit/routes_rewriter.rb +63 -0
  35. data/lib/rails_engine_toolkit/templates/engine_readme.erb +37 -0
  36. data/lib/rails_engine_toolkit/templates/engine_toolkit_yml.erb +35 -0
  37. data/lib/rails_engine_toolkit/templates/gemspec.erb +25 -0
  38. data/lib/rails_engine_toolkit/templates/license.erb +3 -0
  39. data/lib/rails_engine_toolkit/templates.rb +12 -0
  40. data/lib/rails_engine_toolkit/utils.rb +56 -0
  41. data/lib/rails_engine_toolkit/version.rb +5 -0
  42. data/lib/rails_engine_toolkit.rb +33 -0
  43. data/rails_engine_toolkit.gemspec +31 -0
  44. data/spec/rails_engine_toolkit/cli_spec.rb +11 -0
  45. data/spec/rails_engine_toolkit/config_spec.rb +52 -0
  46. data/spec/rails_engine_toolkit/file_editor_spec.rb +26 -0
  47. data/spec/rails_engine_toolkit/install_engine_migrations_spec.rb +36 -0
  48. data/spec/rails_engine_toolkit/install_generator_spec.rb +26 -0
  49. data/spec/rails_engine_toolkit/new_engine_integration_spec.rb +59 -0
  50. data/spec/rails_engine_toolkit/new_engine_spec.rb +54 -0
  51. data/spec/rails_engine_toolkit/project_spec.rb +19 -0
  52. data/spec/rails_engine_toolkit/remove_engine_integration_spec.rb +40 -0
  53. data/spec/rails_engine_toolkit/remove_engine_spec.rb +72 -0
  54. data/spec/rails_engine_toolkit/route_inspector_spec.rb +20 -0
  55. data/spec/rails_engine_toolkit/routes_rewriter_spec.rb +36 -0
  56. data/spec/rails_engine_toolkit/uninstall_engine_migrations_spec.rb +35 -0
  57. data/spec/rails_engine_toolkit/update_engine_readme_spec.rb +32 -0
  58. data/spec/spec_helper.rb +30 -0
  59. data/test/fixtures/host_app/Gemfile +5 -0
  60. data/test/fixtures/host_app/config/application.rb +11 -0
  61. data/test/fixtures/host_app/config/boot.rb +3 -0
  62. data/test/fixtures/host_app/config/environment.rb +3 -0
  63. data/test/fixtures/host_app/config/routes.rb +2 -0
  64. metadata +140 -0
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module RailsEngineToolkit
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ namespace 'engine_toolkit:install'
9
+
10
+ def create_config_file
11
+ require 'rails_engine_toolkit'
12
+
13
+ root_path = Pathname(destination_root)
14
+ slug_default = RailsEngineToolkit::Utils.repo_slug_from_path(root_path)
15
+ name_default = RailsEngineToolkit::Utils.humanize_slug(slug_default)
16
+
17
+ say_status :info, 'Creating engine toolkit configuration', :blue
18
+
19
+ project_slug = ask_with_default('Project slug', slug_default)
20
+ project_name = ask_with_default('Project name', name_default)
21
+ project_url = ask_with_default('Project URL', RailsEngineToolkit::Utils.git_remote_url.to_s)
22
+ author_name = ask_with_default('Author name', RailsEngineToolkit::Utils.git_config('user.name').to_s)
23
+ author_email = ask_with_default('Author email', RailsEngineToolkit::Utils.git_config('user.email').to_s)
24
+ database = ask_with_default('Default database adapter', 'postgresql')
25
+
26
+ api_only = yes?('Use API-only engines by default? [Y/n] ', :green)
27
+ skip_asset_pipeline = yes?('Skip asset pipeline by default? [Y/n] ', :green)
28
+ mount_routes = yes?('Mount engine routes automatically? [Y/n] ', :green)
29
+ create_ddd_structure = yes?('Create DDD folders by default? [Y/n] ', :green)
30
+
31
+ config_content = RailsEngineToolkit::Templates.render('engine_toolkit_yml', {
32
+ project_slug: project_slug,
33
+ project_name: project_name,
34
+ project_url: project_url,
35
+ author_name: author_name,
36
+ author_email: author_email,
37
+ database: database,
38
+ api_only: api_only,
39
+ skip_asset_pipeline: skip_asset_pipeline,
40
+ mount_routes: mount_routes,
41
+ create_ddd_structure: create_ddd_structure
42
+ })
43
+
44
+ create_file 'config/engine_toolkit.yml', config_content
45
+
46
+ say ''
47
+ say 'Default configuration created:', :green
48
+ say " project.slug: #{project_slug}"
49
+ say " project.name: #{project_name}"
50
+ say " defaults.database: #{database}"
51
+ say " defaults.api_only: #{api_only}"
52
+ say " defaults.mount_routes: #{mount_routes}"
53
+ say " defaults.create_ddd_structure: #{create_ddd_structure}"
54
+ say ''
55
+ say 'Edit this file to customize the toolkit for your project:', :yellow
56
+ say " #{File.join(destination_root, 'config/engine_toolkit.yml')}", :yellow
57
+ end
58
+
59
+ private
60
+
61
+ def ask_with_default(label, default)
62
+ value = ask("#{label} [#{default}]")
63
+ value.present? ? value : default
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsEngineToolkit
4
+ class Project
5
+ attr_reader :root
6
+
7
+ def initialize(root = Pathname.pwd)
8
+ @root = Pathname(root).expand_path
9
+ end
10
+
11
+ def validate_root!
12
+ raise ValidationError, 'You must run this command from the Rails project root.' unless gemfile.file?
13
+
14
+ true
15
+ end
16
+
17
+ def config
18
+ @config ||= Config.load(root)
19
+ end
20
+
21
+ def gemfile = root.join('Gemfile')
22
+ def routes_file = root.join('config/routes.rb')
23
+ def root_migrations_dir = root.join('db/migrate')
24
+ def config_file = root.join(Config::DEFAULT_PATH)
25
+
26
+ def engine_path(engine_name)
27
+ root.join("engines/#{engine_name}")
28
+ end
29
+
30
+ def engine_exists?(engine_name)
31
+ engine_path(engine_name).directory?
32
+ end
33
+
34
+ def broken_engine_references
35
+ return [] unless gemfile.file?
36
+
37
+ gemfile.read.lines.filter_map do |line|
38
+ match = line.match(%r{path:\s*["']engines/([^"']+)["']})
39
+ next unless match
40
+
41
+ engine_name = match[1]
42
+ engine_name unless engine_exists?(engine_name)
43
+ end.uniq
44
+ end
45
+
46
+ def remove_broken_engine_references!(engine_names)
47
+ return if engine_names.empty?
48
+
49
+ content = gemfile.read
50
+ engine_names.each do |name|
51
+ content = content.lines.grep_v(%r{path:\s*["']engines/#{Regexp.escape(name)}["']}).join
52
+ end
53
+ gemfile.write(content)
54
+ end
55
+
56
+ def route_inspector
57
+ return nil unless routes_file.file?
58
+
59
+ RouteInspector.new(routes_file.read)
60
+ end
61
+
62
+ def matching_root_migrations_for_engine(engine_name)
63
+ return [] unless root_migrations_dir.directory?
64
+
65
+ engine_class = Utils.classify(engine_name)
66
+ root_migrations_dir.glob('*.rb').select do |file|
67
+ file.read.include?(engine_class) || file.read.include?("#{engine_name}_")
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsEngineToolkit
4
+ class Railtie < Rails::Railtie
5
+ generators do
6
+ require_relative 'generators/install/install_generator'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsEngineToolkit
4
+ class RouteInspector
5
+ Mount = Struct.new(:engine_class, :path, :line)
6
+
7
+ def initialize(content)
8
+ @content = content
9
+ end
10
+
11
+ def mounts
12
+ return [] unless syntax_valid?
13
+
14
+ @content.lines.each_with_index.flat_map do |line, index|
15
+ extract_mounts_from_line(line, index + 1)
16
+ end
17
+ end
18
+
19
+ def includes_mount?(engine_class, path)
20
+ mounts.any? { |mount| mount.engine_class == engine_class && mount.path == path }
21
+ end
22
+
23
+ def syntax_valid?
24
+ !Ripper.sexp(@content).nil?
25
+ end
26
+
27
+ private
28
+
29
+ def extract_mounts_from_line(line, line_number)
30
+ patterns = [
31
+ /^\s*mount\s+([A-Z][A-Za-z0-9_:]+)::Engine,\s+at:\s+["']([^"']+)["']\s*$/,
32
+ /^\s*mount\(([^,]+)::Engine,\s*at:\s+["']([^"']+)["']\s*\)\s*$/,
33
+ /^\s*mount\s+([A-Z][A-Za-z0-9_:]+)::Engine\s*=>\s*["']([^"']+)["']\s*$/
34
+ ]
35
+
36
+ patterns.filter_map do |pattern|
37
+ match = line.match(pattern)
38
+ next unless match
39
+
40
+ Mount.new(engine_class: match[1], path: match[2], line: line_number)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsEngineToolkit
4
+ class RoutesRewriter
5
+ def initialize(path)
6
+ @path = Pathname(path)
7
+ end
8
+
9
+ def add_mount(engine_class:, mount_path:)
10
+ raise ValidationError, "Routes file not found: #{@path}" unless @path.file?
11
+
12
+ content = @path.read
13
+ inspector = RouteInspector.new(content)
14
+ return false if inspector.includes_mount?(engine_class, mount_path)
15
+
16
+ mount_line = %( mount #{engine_class}::Engine, at: "#{mount_path}"\n)
17
+
18
+ updated = content.sub(/^(\s*Rails\.application\.routes\.draw do\s*\n)/) do |match|
19
+ "#{match}#{mount_line}"
20
+ end
21
+ raise CommandError, "Could not find Rails.application.routes.draw block in #{@path}" if updated == content
22
+
23
+ @path.write(updated)
24
+ true
25
+ end
26
+
27
+ def remove_mount(engine_class:, mount_path:)
28
+ raise ValidationError, "Routes file not found: #{@path}" unless @path.file?
29
+
30
+ original = @path.read
31
+ lines = original.lines.reject do |line|
32
+ mount_line?(line, engine_class: engine_class, mount_path: mount_path)
33
+ end
34
+ updated = lines.join
35
+ changed = updated != original
36
+ @path.write(updated) if changed
37
+ changed
38
+ end
39
+
40
+ private
41
+
42
+ def mount_line?(line, engine_class:, mount_path:)
43
+ regexes = [
44
+ mount_comma_regex(engine_class, mount_path),
45
+ mount_parenthesized_regex(engine_class, mount_path),
46
+ mount_hash_rocket_regex(engine_class, mount_path)
47
+ ]
48
+ regexes.any? { |regex| line.match?(regex) }
49
+ end
50
+
51
+ def mount_comma_regex(engine_class, mount_path)
52
+ /^\s*mount\s+#{Regexp.escape(engine_class)}::Engine,\s+at:\s+["']#{Regexp.escape(mount_path)}["']\s*$/
53
+ end
54
+
55
+ def mount_parenthesized_regex(engine_class, mount_path)
56
+ /^\s*mount\(\s*#{Regexp.escape(engine_class)}::Engine\s*,\s*at:\s+["']#{Regexp.escape(mount_path)}["']\s*\)\s*$/
57
+ end
58
+
59
+ def mount_hash_rocket_regex(engine_class, mount_path)
60
+ /^\s*mount\s+#{Regexp.escape(engine_class)}::Engine\s*=>\s*["']#{Regexp.escape(mount_path)}["']\s*$/
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,37 @@
1
+ # <%= engine_class %>
2
+
3
+ ## Purpose
4
+ <%= description %>
5
+
6
+ ## Responsibilities
7
+ - Describe the main responsibilities of this bounded context.
8
+
9
+ ## Owned tables
10
+ - None defined yet.
11
+
12
+ ## Public endpoints
13
+ - None defined yet.
14
+
15
+ ## Integration points
16
+ - None defined yet.
17
+
18
+ ## Dependencies
19
+ - Rails
20
+ - PostgreSQL
21
+
22
+ ## Commands
23
+
24
+ ### Generate a model
25
+ ```bash
26
+ bundle exec engine-toolkit new_engine_model <%= engine_name %> example_entity name:string
27
+ ```
28
+
29
+ ### Generate a migration
30
+ ```bash
31
+ bundle exec engine-toolkit new_engine_migration <%= engine_name %> CreateExampleEntities
32
+ ```
33
+
34
+ ### Update owned tables
35
+ ```bash
36
+ bundle exec engine-toolkit update_engine_readme <%= engine_name %>
37
+ ```
@@ -0,0 +1,35 @@
1
+ project:
2
+ name: "<%= project_name %>"
3
+ slug: "<%= project_slug %>"
4
+ url: "<%= project_url %>"
5
+
6
+ author:
7
+ name: "<%= author_name %>"
8
+ email: "<%= author_email %>"
9
+
10
+ defaults:
11
+ database: "<%= database %>"
12
+ api_only: <%= api_only %>
13
+ skip_asset_pipeline: <%= skip_asset_pipeline %>
14
+ skip_action_mailbox: true
15
+ skip_action_text: true
16
+ skip_active_storage: true
17
+ skip_hotwire: true
18
+ skip_jbuilder: true
19
+ skip_system_test: true
20
+ mount_routes: <%= mount_routes %>
21
+ create_ddd_structure: <%= create_ddd_structure %>
22
+
23
+ metadata:
24
+ license: "MIT"
25
+ ruby_version: ">= 3.2"
26
+ rails_version: ">= 8.1.2"
27
+
28
+ ddd:
29
+ folders:
30
+ - app/use_cases
31
+ - app/services
32
+ - app/policies
33
+ - app/serializers
34
+ - app/repositories
35
+ - app/contracts
@@ -0,0 +1,25 @@
1
+ require_relative "lib/<%= engine_name %>/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "<%= engine_name %>"
5
+ spec.version = <%= engine_class %>::VERSION
6
+ spec.authors = ["<%= author_name %>"]
7
+ spec.email = ["<%= author_email %>"]
8
+ spec.summary = "<%= summary %>"
9
+ spec.description = "<%= description %>"
10
+
11
+ spec.homepage = "<%= project_url %>"
12
+ spec.license = "<%= license %>"
13
+
14
+ spec.required_ruby_version = "<%= ruby_version %>"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "<%= project_url %>"
18
+ spec.metadata["changelog_uri"] = "<%= project_url %>"
19
+
20
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
+ Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
22
+ end
23
+
24
+ spec.add_dependency "rails", "<%= rails_version %>"
25
+ end
@@ -0,0 +1,3 @@
1
+ <%= license %> License
2
+
3
+ Copyright (c) <%= year %> <%= author_name %>
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsEngineToolkit
4
+ module Templates
5
+ module_function
6
+
7
+ def render(name, locals = {})
8
+ template = Pathname(__dir__).join("templates/#{name}.erb").read
9
+ ERB.new(template, trim_mode: '-').result_with_hash(locals)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsEngineToolkit
4
+ module Utils
5
+ module_function
6
+
7
+ def snake_case?(value)
8
+ value.is_a?(String) && value.match?(/\A[a-z0-9_]+\z/)
9
+ end
10
+
11
+ def classify(snake_name)
12
+ snake_name.split('_').map(&:capitalize).join
13
+ end
14
+
15
+ def humanize_slug(slug)
16
+ slug.split('_').map(&:capitalize).join(' ')
17
+ end
18
+
19
+ def repo_slug_from_path(pathname)
20
+ File.basename(Pathname(pathname).expand_path.to_s)
21
+ end
22
+
23
+ def ask(prompt, default: nil, input: $stdin, output: $stdout)
24
+ shown = default.nil? || default.to_s.empty? ? "#{prompt}: " : "#{prompt} [#{default}]: "
25
+ output.print(shown)
26
+ answer = input.gets&.chomp
27
+ return default if answer.nil? || answer.empty?
28
+
29
+ answer
30
+ end
31
+
32
+ def ask_yes_no(prompt, default: false, input: $stdin, output: $stdout)
33
+ suffix = default ? ' [Y/n]: ' : ' [y/N]: '
34
+ output.print("#{prompt}#{suffix}")
35
+ answer = input.gets&.chomp.to_s.strip.downcase
36
+ return default if answer.empty?
37
+
38
+ %w[y yes].include?(answer)
39
+ end
40
+
41
+ def safe_system(*cmd, chdir:)
42
+ success = system(*cmd, chdir: chdir.to_s)
43
+ raise CommandError, "Command failed: #{cmd.join(' ')}" unless success
44
+ end
45
+
46
+ def git_config(key)
47
+ value = `git config #{key} 2>/dev/null`.to_s.strip
48
+ value.empty? ? nil : value
49
+ end
50
+
51
+ def git_remote_url
52
+ value = `git remote get-url origin 2>/dev/null`.to_s.strip
53
+ value.empty? ? nil : value
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsEngineToolkit
4
+ VERSION = '0.6.3'
5
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'erb'
5
+ require 'pathname'
6
+ require 'fileutils'
7
+ require 'thor'
8
+ require 'ripper'
9
+
10
+ require_relative 'rails_engine_toolkit/version'
11
+ require_relative 'rails_engine_toolkit/errors'
12
+ require_relative 'rails_engine_toolkit/utils'
13
+ require_relative 'rails_engine_toolkit/config'
14
+ require_relative 'rails_engine_toolkit/file_editor'
15
+ require_relative 'rails_engine_toolkit/route_inspector'
16
+ require_relative 'rails_engine_toolkit/routes_rewriter'
17
+ require_relative 'rails_engine_toolkit/project'
18
+ require_relative 'rails_engine_toolkit/templates'
19
+ require_relative 'rails_engine_toolkit/actions/init'
20
+ require_relative 'rails_engine_toolkit/actions/new_engine'
21
+ require_relative 'rails_engine_toolkit/actions/update_engine_readme'
22
+ require_relative 'rails_engine_toolkit/actions/new_engine_migration'
23
+ require_relative 'rails_engine_toolkit/actions/delete_engine_migration'
24
+ require_relative 'rails_engine_toolkit/actions/new_engine_model'
25
+ require_relative 'rails_engine_toolkit/actions/install_engine_migrations'
26
+ require_relative 'rails_engine_toolkit/actions/uninstall_engine_migrations'
27
+ require_relative 'rails_engine_toolkit/actions/remove_engine'
28
+ require_relative 'rails_engine_toolkit/cli'
29
+
30
+ module RailsEngineToolkit
31
+ end
32
+
33
+ require_relative 'rails_engine_toolkit/railtie' if defined?(Rails::Railtie)
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/rails_engine_toolkit/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'rails_engine_toolkit'
7
+ spec.version = RailsEngineToolkit::VERSION
8
+ spec.authors = ['Your Name']
9
+ spec.email = ['you@example.com']
10
+
11
+ spec.summary = 'Reusable CLI and generators for Rails engines'
12
+ spec.description =
13
+ 'Creates and manages internal Rails engines with configurable conventions, safe file mutations, parser-assisted route inspection, engine-specific migration installation, and Rails install generators.'
14
+ spec.homepage = 'https://github.com/your-org/rails_engine_toolkit'
15
+ spec.license = 'MIT'
16
+ spec.required_ruby_version = '>= 3.2'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = spec.homepage
20
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/releases"
21
+ spec.metadata['rubygems_mfa_required'] = 'true'
22
+
23
+ spec.files = Dir.glob('{exe,lib,spec,test,docs,.github}/**/*') +
24
+ %w[README.md LICENSE.txt rails_engine_toolkit.gemspec Gemfile Rakefile]
25
+ spec.bindir = 'exe'
26
+ spec.executables = ['engine-toolkit']
27
+ spec.require_paths = ['lib']
28
+
29
+ spec.add_dependency 'railties', '>= 8.1.0'
30
+ spec.add_dependency 'thor', '>= 1.3'
31
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RailsEngineToolkit::CLI do
6
+ it 'shows help for unknown commands' do
7
+ expect do
8
+ described_class.start(['unknown'])
9
+ end.to output(/Commands:/).to_stdout
10
+ end
11
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RailsEngineToolkit::Config do
6
+ it 'loads valid configuration' do
7
+ in_tmpdir do |dir|
8
+ write(dir.join('config/engine_toolkit.yml'), <<~YAML)
9
+ project:
10
+ name: "Transport System"
11
+ slug: "transport_system"
12
+ url: "https://example.test/repo"
13
+ author:
14
+ name: "Juan"
15
+ email: "juan@example.com"
16
+ defaults:
17
+ database: "postgresql"
18
+ api_only: true
19
+ mount_routes: true
20
+ create_ddd_structure: true
21
+ metadata:
22
+ license: "MIT"
23
+ ruby_version: ">= 3.2"
24
+ rails_version: ">= 8.1.2"
25
+ YAML
26
+
27
+ config = described_class.load(dir)
28
+ expect(config.project_name).to eq('Transport System')
29
+ expect(config.project_slug).to eq('transport_system')
30
+ end
31
+ end
32
+
33
+ it 'rejects invalid database adapter' do
34
+ in_tmpdir do |dir|
35
+ write(dir.join('config/engine_toolkit.yml'), <<~YAML)
36
+ project:
37
+ name: "Transport System"
38
+ slug: "transport_system"
39
+ url: ""
40
+ author:
41
+ name: "Juan"
42
+ email: "juan@example.com"
43
+ defaults:
44
+ database: "oracle"
45
+ metadata:
46
+ license: "MIT"
47
+ YAML
48
+
49
+ expect { described_class.load(dir) }.to raise_error(RailsEngineToolkit::ValidationError, /defaults.database/)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RailsEngineToolkit::FileEditor do
6
+ it 'removes matching lines safely' do
7
+ in_tmpdir do |dir|
8
+ path = dir.join('routes.rb')
9
+ write(path, "a\nb\nc\n")
10
+ changed = described_class.remove_lines_matching(path, /^b$/)
11
+ expect(changed).to be(true)
12
+ expect(path.read).to eq("a\nc\n")
13
+ end
14
+ end
15
+
16
+ it 'inserts content after first match' do
17
+ in_tmpdir do |dir|
18
+ path = dir.join('routes.rb')
19
+ write(path, "Rails.application.routes.draw do\nend\n")
20
+ changed = described_class.insert_after_first_match(path, /Rails\.application\.routes\.draw do\s*\n/,
21
+ " mount Auth::Engine, at: \"/auth\"\n")
22
+ expect(changed).to be(true)
23
+ expect(path.read).to include('mount Auth::Engine, at: "/auth"')
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RailsEngineToolkit::Actions::InstallEngineMigrations do
6
+ it 'copies only the selected engine migrations into root db/migrate' do
7
+ in_tmpdir do |dir|
8
+ write(dir.join('Gemfile'), "source \"https://rubygems.org\"\n")
9
+ write(dir.join('engines/auth/db/migrate/20260101000000_create_auth_credentials.rb'),
10
+ 'class CreateAuthCredentials; end')
11
+ write(dir.join('engines/billing/db/migrate/20260101000001_create_billing_invoices.rb'),
12
+ 'class CreateBillingInvoices; end')
13
+
14
+ out = StringIO.new
15
+ described_class.new(['auth'], stdin: StringIO.new, stdout: out, stderr: StringIO.new, root: dir).call
16
+
17
+ root_migrations = dir.join('db/migrate').glob('*.rb').map { |f| f.basename.to_s }
18
+ expect(root_migrations.any? { |name| name.end_with?('create_auth_credentials.rb') }).to be(true)
19
+ expect(root_migrations.any? { |name| name.end_with?('create_billing_invoices.rb') }).to be(false)
20
+ end
21
+ end
22
+
23
+ it 'does not duplicate already installed migrations' do
24
+ in_tmpdir do |dir|
25
+ write(dir.join('Gemfile'), "source \"https://rubygems.org\"\n")
26
+ write(dir.join('engines/auth/db/migrate/20260101000000_create_auth_credentials.rb'),
27
+ 'class CreateAuthCredentials; end')
28
+ write(dir.join('db/migrate/20260102000000_create_auth_credentials.rb'), 'class CreateAuthCredentials; end')
29
+
30
+ out = StringIO.new
31
+ described_class.new(['auth'], stdin: StringIO.new, stdout: out, stderr: StringIO.new, root: dir).call
32
+
33
+ expect(out.string).to include('No new migrations')
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'rails_engine_toolkit/generators/install/install_generator'
5
+
6
+ RSpec.describe RailsEngineToolkit::Generators::InstallGenerator do
7
+ it 'creates config and prints summary in a host app' do
8
+ in_tmpdir do |dir|
9
+ write(dir.join('Gemfile'), "source \"https://rubygems.org\"\n")
10
+
11
+ generator = described_class.new
12
+ allow(generator).to receive(:destination_root).and_return(dir.to_s)
13
+ allow(generator).to receive(:ask).and_return('', '', '', '', '', 'postgresql')
14
+ allow(generator).to receive(:yes?).and_return(true, true, true, true)
15
+ output = StringIO.new
16
+ allow(generator).to receive(:say) { |message = '', *_args| output.puts(message) }
17
+ allow(generator).to receive(:say_status) { |_status, message, *_args| output.puts(message) }
18
+
19
+ generator.create_config_file
20
+
21
+ expect(dir.join('config/engine_toolkit.yml')).to exist
22
+ expect(output.string).to include('Default configuration created:')
23
+ expect(output.string).to include('config/engine_toolkit.yml')
24
+ end
25
+ end
26
+ end