develoz-rails 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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +31 -0
- data/.github/workflows/release.yml +459 -0
- data/.rubocop.yml +52 -0
- data/.sisyphus/notepads/develoz-rails-template/learnings.md +855 -0
- data/.tool-versions +2 -0
- data/CHANGELOG.md +28 -0
- data/CONTRIBUTING.md +120 -0
- data/README.md +76 -0
- data/Rakefile +18 -0
- data/config/generators.yml +105 -0
- data/docs/guide/installation.md +574 -0
- data/exe/develoz +6 -0
- data/install +98 -0
- data/lib/develoz/canonical_fetcher.rb +56 -0
- data/lib/develoz/cli.rb +84 -0
- data/lib/develoz/generators/base.rb +134 -0
- data/lib/develoz/manifest.rb +44 -0
- data/lib/develoz/options.rb +55 -0
- data/lib/develoz/version.rb +5 -0
- data/lib/develoz/version_resolver.rb +103 -0
- data/lib/develoz.rb +15 -0
- data/lib/generators/develoz/active_resource/active_resource_generator.rb +25 -0
- data/lib/generators/develoz/active_resource/templates/application_resource.rb.tt +7 -0
- data/lib/generators/develoz/active_resource/templates/example_resource.rb.tt +5 -0
- data/lib/generators/develoz/admin/admin_generator.rb +44 -0
- data/lib/generators/develoz/admin/templates/admin_base_controller.rb.tt +7 -0
- data/lib/generators/develoz/admin/templates/admin_layout.html.erb.tt +38 -0
- data/lib/generators/develoz/admin/templates/dashboard_controller.rb.tt +7 -0
- data/lib/generators/develoz/admin/templates/dashboard_index.html.erb.tt +2 -0
- data/lib/generators/develoz/admin/templates/routes.rb.tt +3 -0
- data/lib/generators/develoz/agents_docs/agents_docs_generator.rb +50 -0
- data/lib/generators/develoz/agents_docs/templates/.github/pull_request_template.md.tt +13 -0
- data/lib/generators/develoz/agents_docs/templates/AGENTS.md.tt +36 -0
- data/lib/generators/develoz/agents_docs/templates/docs/development.md.tt +121 -0
- data/lib/generators/develoz/agents_docs/templates/docs/performance.md.tt +46 -0
- data/lib/generators/develoz/agents_docs/templates/docs/testing.md.tt +92 -0
- data/lib/generators/develoz/agents_docs/templates/spec/cassettes/Example_API/fetches_data_from_an_external_API.yml.tt +23 -0
- data/lib/generators/develoz/agents_docs/templates/spec/requests/example_api_spec.rb.tt +14 -0
- data/lib/generators/develoz/agents_docs/templates/spec/support/faraday.rb.tt +19 -0
- data/lib/generators/develoz/agents_docs/templates/spec/support/vcr.rb.tt +11 -0
- data/lib/generators/develoz/api/api_generator.rb +55 -0
- data/lib/generators/develoz/api/templates/app/blueprints/example_blueprint.rb.tt +6 -0
- data/lib/generators/develoz/api/templates/app/controllers/api/v1/base_controller.rb.tt +96 -0
- data/lib/generators/develoz/api/templates/config/initializers/blueprinter.rb.tt +16 -0
- data/lib/generators/develoz/api/templates/config/initializers/rswag_api.rb.tt +15 -0
- data/lib/generators/develoz/api/templates/config/initializers/rswag_ui.rb.tt +17 -0
- data/lib/generators/develoz/api/templates/spec/requests/api/v1/examples_spec.rb.tt +25 -0
- data/lib/generators/develoz/auth/auth_generator.rb +82 -0
- data/lib/generators/develoz/auth/templates/app/controllers/concerns/authentication.rb.tt +53 -0
- data/lib/generators/develoz/auth/templates/app/controllers/passwords_controller.rb.tt +35 -0
- data/lib/generators/develoz/auth/templates/app/controllers/sessions_controller.rb.tt +22 -0
- data/lib/generators/develoz/auth/templates/app/mailers/passwords_mailer.rb.tt +8 -0
- data/lib/generators/develoz/auth/templates/app/models/current.rb.tt +5 -0
- data/lib/generators/develoz/auth/templates/app/models/user.rb.tt +7 -0
- data/lib/generators/develoz/auth/templates/app/views/passwords/edit.html.erb.tt +9 -0
- data/lib/generators/develoz/auth/templates/app/views/passwords_mailer/reset.html.erb.tt +6 -0
- data/lib/generators/develoz/auth/templates/app/views/sessions/new.html.erb.tt +11 -0
- data/lib/generators/develoz/auth/templates/db/migrate/create_users.rb.tt +13 -0
- data/lib/generators/develoz/auth/templates/spec/requests/passwords_spec.rb.tt +60 -0
- data/lib/generators/develoz/auth/templates/spec/requests/sessions_spec.rb.tt +37 -0
- data/lib/generators/develoz/ci/ci_generator.rb +66 -0
- data/lib/generators/develoz/ci/templates/.github/workflows/ci.yml.tt +18 -0
- data/lib/generators/develoz/ci/templates/.haml-lint.yml.tt +12 -0
- data/lib/generators/develoz/ci/templates/.markdownlint.json.tt +10 -0
- data/lib/generators/develoz/ci/templates/.reek.yml.tt +28 -0
- data/lib/generators/develoz/ci/templates/.rubocop.yml.tt +22 -0
- data/lib/generators/develoz/ci/templates/.stylelintrc.json.tt +4 -0
- data/lib/generators/develoz/ci/templates/.yamllint.tt +14 -0
- data/lib/generators/develoz/ci/templates/Rakefile.tt +73 -0
- data/lib/generators/develoz/ci/templates/bin/ci.tt +6 -0
- data/lib/generators/develoz/ci/templates/biome.json.tt +41 -0
- data/lib/generators/develoz/ci/templates/config/ci.rb.tt +23 -0
- data/lib/generators/develoz/concerns/concerns_generator.rb +48 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/configurable.rb.tt +120 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/optimized_finders.rb.tt +93 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/searchable_concern.rb.tt +31 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/transitionable.rb.tt +144 -0
- data/lib/generators/develoz/concerns/templates/app/models/configuration.rb.tt +104 -0
- data/lib/generators/develoz/concerns/templates/db/migrate/add_status_transitions.rb.tt +30 -0
- data/lib/generators/develoz/concerns/templates/db/migrate/create_configurations.rb.tt +25 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/configurable_spec.rb.tt +172 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/optimized_finders_spec.rb.tt +200 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/searchable_concern_spec.rb.tt +84 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/transitionable_spec.rb.tt +234 -0
- data/lib/generators/develoz/concerns/templates/spec/models/configuration_spec.rb.tt +223 -0
- data/lib/generators/develoz/database/database_generator.rb +44 -0
- data/lib/generators/develoz/database/templates/config/database.yml.tt +53 -0
- data/lib/generators/develoz/database/templates/config/initializers/pg_search.rb.tt +5 -0
- data/lib/generators/develoz/database/templates/db/migrate/create_pg_extensions.rb.tt +13 -0
- data/lib/generators/develoz/db_backup/db_backup_generator.rb +47 -0
- data/lib/generators/develoz/db_backup/templates/backup_rake.tt +48 -0
- data/lib/generators/develoz/db_backup/templates/bin_db_backup.tt +25 -0
- data/lib/generators/develoz/db_backup/templates/compose_service.tt +14 -0
- data/lib/generators/develoz/doc_specs/doc_specs_generator.rb +33 -0
- data/lib/generators/develoz/doc_specs/templates/bin/generate-docs.tt +392 -0
- data/lib/generators/develoz/doc_specs/templates/lib/tasks/docs_check.rake.tt +8 -0
- data/lib/generators/develoz/doc_specs/templates/spec/support/doc_screenshot_helper.rb.tt +17 -0
- data/lib/generators/develoz/doc_specs/templates/spec/system/example_doc_spec.rb.tt +38 -0
- data/lib/generators/develoz/docker/docker_generator.rb +54 -0
- data/lib/generators/develoz/docker/templates/bin_dev.tt +5 -0
- data/lib/generators/develoz/docker/templates/bin_docker_entrypoint.tt +8 -0
- data/lib/generators/develoz/docker/templates/bin_run.tt +152 -0
- data/lib/generators/develoz/docker/templates/bin_setup.tt +15 -0
- data/lib/generators/develoz/docker/templates/docker-compose.yml.tt +59 -0
- data/lib/generators/develoz/docker/templates/dockerfile_dev.tt +24 -0
- data/lib/generators/develoz/docs_render/docs_render_generator.rb +46 -0
- data/lib/generators/develoz/docs_render/templates/app/assets/stylesheets/documentation.scss.tt +145 -0
- data/lib/generators/develoz/docs_render/templates/app/controllers/docs_controller.rb.tt +7 -0
- data/lib/generators/develoz/docs_render/templates/app/javascript/docs.js.tt +15 -0
- data/lib/generators/develoz/docs_render/templates/app/models/document.rb.tt +189 -0
- data/lib/generators/develoz/docs_render/templates/app/views/docs/show.html.erb.tt +7 -0
- data/lib/generators/develoz/docs_render/templates/config/initializers/redcarpet_rouge.rb.tt +16 -0
- data/lib/generators/develoz/frontend_core/frontend_core_generator.rb +40 -0
- data/lib/generators/develoz/frontend_core/templates/.annotaterb.yml.tt +16 -0
- data/lib/generators/develoz/frontend_core/templates/config/importmap.rb.tt +12 -0
- data/lib/generators/develoz/frontend_core/templates/config/initializers/pagy.rb.tt +10 -0
- data/lib/generators/develoz/install/install_generator.rb +51 -0
- data/lib/generators/develoz/kamal/kamal_generator.rb +59 -0
- data/lib/generators/develoz/kamal/templates/accessories_postgres.yml.tt +14 -0
- data/lib/generators/develoz/kamal/templates/deploy.yml.tt +43 -0
- data/lib/generators/develoz/kamal/templates/dockerfile_prod.tt +54 -0
- data/lib/generators/develoz/kamal/templates/kamal_secrets.tt +22 -0
- data/lib/generators/develoz/maintenance/maintenance_generator.rb +29 -0
- data/lib/generators/develoz/maintenance/templates/app/tasks/maintenance/example_task.rb.tt +20 -0
- data/lib/generators/develoz/maintenance/templates/lib/tasks/maintenance_counters.rake.tt +27 -0
- data/lib/generators/develoz/push/push_generator.rb +61 -0
- data/lib/generators/develoz/push/templates/app/javascript/pwa/subscription.js.tt +57 -0
- data/lib/generators/develoz/push/templates/app/models/push_subscription.rb.tt +14 -0
- data/lib/generators/develoz/push/templates/app/services/push_notification_service.rb.tt +36 -0
- data/lib/generators/develoz/push/templates/app/views/pwa/sw_push_handlers.js.tt +49 -0
- data/lib/generators/develoz/push/templates/constants_env.tt +5 -0
- data/lib/generators/develoz/push/templates/db/migrate/create_push_subscriptions.rb.tt +15 -0
- data/lib/generators/develoz/pwa/pwa_generator.rb +46 -0
- data/lib/generators/develoz/pwa/templates/app/controllers/pwa_controller.rb.tt +17 -0
- data/lib/generators/develoz/pwa/templates/app/javascript/pwa/registration.js.tt +25 -0
- data/lib/generators/develoz/pwa/templates/app/views/pwa/manifest.json.erb.tt +24 -0
- data/lib/generators/develoz/pwa/templates/app/views/pwa/offline.html.erb.tt +51 -0
- data/lib/generators/develoz/pwa/templates/app/views/pwa/service-worker.js.tt +109 -0
- data/lib/generators/develoz/pwa/templates/routes.rb.tt +3 -0
- data/lib/generators/develoz/solid/solid_generator.rb +52 -0
- data/lib/generators/develoz/solid/templates/app/jobs/application_job.rb.tt +7 -0
- data/lib/generators/develoz/solid/templates/config/cable.yml.tt +10 -0
- data/lib/generators/develoz/solid/templates/config/cache.yml.tt +16 -0
- data/lib/generators/develoz/solid/templates/config/initializers/mission_control.rb.tt +8 -0
- data/lib/generators/develoz/solid/templates/config/initializers/solid.rb.tt +7 -0
- data/lib/generators/develoz/solid/templates/config/queue.yml.tt +18 -0
- data/lib/generators/develoz/solid/templates/config/recurring.yml.tt +20 -0
- data/lib/generators/develoz/strict_loading/strict_loading_generator.rb +17 -0
- data/lib/generators/develoz/strict_loading/templates/config/initializers/strict_loading.rb.tt +13 -0
- data/lib/generators/develoz/testing/templates/rspec.tt +2 -0
- data/lib/generators/develoz/testing/templates/rspec_parallel.tt +2 -0
- data/lib/generators/develoz/testing/templates/spec/rails_helper.rb.tt +32 -0
- data/lib/generators/develoz/testing/templates/spec/spec_helper.rb.tt +31 -0
- data/lib/generators/develoz/testing/testing_generator.rb +39 -0
- data/lib/generators/develoz/tooling/templates/constants.rb.tt +17 -0
- data/lib/generators/develoz/tooling/templates/env.example.tt +4 -0
- data/lib/generators/develoz/tooling/templates/vscode/extensions.json.tt +8 -0
- data/lib/generators/develoz/tooling/templates/vscode/settings.json.tt +26 -0
- data/lib/generators/develoz/tooling/templates/vscode/tasks.json.tt +26 -0
- data/lib/generators/develoz/tooling/tooling_generator.rb +33 -0
- data/lib/generators/develoz/ui/templates/bin/setup_develoz_ui.tt +20 -0
- data/lib/generators/develoz/ui/ui_generator.rb +57 -0
- data/lib/generators/develoz/versioning/templates/app/helpers/application_helper.rb.tt +7 -0
- data/lib/generators/develoz/versioning/templates/app/views/shared/_app_version.html.erb.tt +3 -0
- data/lib/generators/develoz/versioning/templates/spec/helpers/application_helper_spec.rb.tt +11 -0
- data/lib/generators/develoz/versioning/versioning_generator.rb +48 -0
- data/lib/tasks/canonical.rake +27 -0
- data/templates/.keep +0 -0
- data/templates/CANONICAL_SOURCES.md +93 -0
- data/templates/pull_request_template.md.tt +13 -0
- metadata +383 -0
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
# Develoz Rails Generator Pattern - Learnings
|
|
2
|
+
|
|
3
|
+
## Working Generator Pattern (Task 10 - ToolingGenerator)
|
|
4
|
+
|
|
5
|
+
### Key Success Factors
|
|
6
|
+
|
|
7
|
+
1. **Source Root Override**: Use `def self.source_root` to override the base class method
|
|
8
|
+
- Pattern: `File.expand_path("templates", __dir__)`
|
|
9
|
+
- This avoids the Thor `source_paths` error that caused infinite loops in prior attempts
|
|
10
|
+
- Templates must be co-located in `lib/generators/develoz/<generator_name>/templates/`
|
|
11
|
+
|
|
12
|
+
2. **Per-File Templates**: Use `template` method for each file, NOT Thor's `directory` method
|
|
13
|
+
- `template "vscode/settings.json.tt", ".vscode/settings.json"`
|
|
14
|
+
- `template "env.example.tt", ".env.example"`
|
|
15
|
+
- This gives fine-grained control and avoids directory-level issues
|
|
16
|
+
|
|
17
|
+
3. **Destination Root Binding in Specs**: CRITICAL for test isolation
|
|
18
|
+
- Always pass `destination_root: tmp_dir` when instantiating the generator
|
|
19
|
+
- Assert `expect(gen.destination_root).to eq(tmp_dir)` to verify binding
|
|
20
|
+
- Without this, the generator writes to the real repo and corrupts it
|
|
21
|
+
|
|
22
|
+
4. **Calling Generator Methods**: Do NOT use `invoke_all` in tests
|
|
23
|
+
- Reason: Thor treats all public methods as tasks, including helper methods like `inject_once`
|
|
24
|
+
- Instead: Call specific methods directly: `gen.create_vscode; gen.create_env_files; ...`
|
|
25
|
+
- This avoids calling base class helper methods as tasks
|
|
26
|
+
|
|
27
|
+
5. **Template Content**: Use ERB syntax for dynamic values
|
|
28
|
+
- `<%= app_name %>` works because base class defines `app_name` method
|
|
29
|
+
- Templates are processed through Rails' template engine
|
|
30
|
+
|
|
31
|
+
6. **Idempotency**: Use base class helpers with built-in guards
|
|
32
|
+
- `add_gem` checks if gem already exists before adding
|
|
33
|
+
- `ensure_gitignore` checks if pattern already exists
|
|
34
|
+
- `create_file` with conditional check for `.env`
|
|
35
|
+
- These prevent duplicate entries on re-runs
|
|
36
|
+
|
|
37
|
+
### File Structure
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
lib/generators/develoz/tooling/
|
|
41
|
+
├── tooling_generator.rb
|
|
42
|
+
└── templates/
|
|
43
|
+
├── vscode/
|
|
44
|
+
│ ├── settings.json.tt
|
|
45
|
+
│ ├── extensions.json.tt
|
|
46
|
+
│ └── tasks.json.tt
|
|
47
|
+
├── env.example.tt
|
|
48
|
+
└── constants.rb.tt
|
|
49
|
+
|
|
50
|
+
spec/develoz/generators/
|
|
51
|
+
└── tooling_generator_spec.rb
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Generator Methods Pattern
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
class ToolingGenerator < Develoz::Generators::Base
|
|
58
|
+
def self.source_root
|
|
59
|
+
File.expand_path("templates", __dir__)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def create_vscode
|
|
63
|
+
template "vscode/settings.json.tt", ".vscode/settings.json"
|
|
64
|
+
# ... more templates
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def create_env_files
|
|
68
|
+
template "env.example.tt", ".env.example"
|
|
69
|
+
create_file ".env", "" unless File.exist?(File.join(destination_root, ".env"))
|
|
70
|
+
ensure_gitignore(".env")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def add_dotenv
|
|
74
|
+
add_gem "dotenv-rails", group: %i[development test]
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Spec Pattern
|
|
80
|
+
|
|
81
|
+
- Use `with_tmp_dir` helper to create isolated temp directories
|
|
82
|
+
- Seed minimal Gemfile and .gitignore for base class helpers to work
|
|
83
|
+
- Call generator methods directly, not `invoke_all`
|
|
84
|
+
- Keep expectations to 3 per test (RuboCop rule)
|
|
85
|
+
- Avoid instance variables; use local variables and method parameters
|
|
86
|
+
|
|
87
|
+
### Coverage Requirements
|
|
88
|
+
|
|
89
|
+
- Generator code: 100% line + branch coverage
|
|
90
|
+
- Full suite: 100% line + branch coverage (includes all lib files)
|
|
91
|
+
- Tests must reach all branches (e.g., both `.env` exists/not-exists paths)
|
|
92
|
+
|
|
93
|
+
## Reuse for T11-T31
|
|
94
|
+
|
|
95
|
+
This pattern is stable and should be reused for all subsequent generators:
|
|
96
|
+
1. Create generator in `lib/generators/develoz/<name>/<name>_generator.rb`
|
|
97
|
+
2. Create templates in `lib/generators/develoz/<name>/templates/`
|
|
98
|
+
3. Override `source_root` with `File.expand_path("templates", __dir__)`
|
|
99
|
+
4. Use per-file `template` calls
|
|
100
|
+
5. Create spec in `spec/develoz/generators/<name>_generator_spec.rb`
|
|
101
|
+
6. Bind `destination_root` in all spec instantiations
|
|
102
|
+
7. Call methods directly, not `invoke_all`
|
|
103
|
+
8. Ensure 100% coverage with focused tests (max 3 expectations each)
|
|
104
|
+
|
|
105
|
+
## SimpleCov + Parallel Test Collation (Task 11 - TestingGenerator)
|
|
106
|
+
|
|
107
|
+
The generated app's test setup uses SimpleCov with parallel_tests collation:
|
|
108
|
+
|
|
109
|
+
1. **SimpleCov Configuration** (spec/spec_helper.rb):
|
|
110
|
+
- `enable_coverage :branch` for branch coverage tracking
|
|
111
|
+
- `minimum_coverage line: 100, branch: 100` enforces 100% coverage
|
|
112
|
+
- `coverage_dir "public/coverage"` outputs to public/coverage (not repo root)
|
|
113
|
+
- `SimpleCov.command_name "RSpec_#{ENV['TEST_ENV_NUMBER']}"` tags each parallel process
|
|
114
|
+
- `SimpleCov.use_merging true` merges coverage across parallel processes
|
|
115
|
+
- `add_filter %w[/spec/ /config/ /db/]` excludes test/config/db files from coverage
|
|
116
|
+
|
|
117
|
+
2. **Parallel Test Collation**:
|
|
118
|
+
- When running `parallel_tests`, each process gets a unique `TEST_ENV_NUMBER`
|
|
119
|
+
- SimpleCov uses this to create separate coverage reports per process
|
|
120
|
+
- The `use_merging true` flag combines all reports into a single result
|
|
121
|
+
- This allows 100% coverage enforcement even with parallel test execution
|
|
122
|
+
|
|
123
|
+
3. **Capybara + Selenium Configuration** (spec/rails_helper.rb):
|
|
124
|
+
- Registers `:headless_chrome` driver with Selenium WebDriver
|
|
125
|
+
- Chrome options: `--headless=new`, `--no-sandbox`, `--disable-gpu`
|
|
126
|
+
- Sets `Capybara.javascript_driver = :headless_chrome` for JS tests
|
|
127
|
+
- FactoryBot configured to load factories from `spec/factories`
|
|
128
|
+
- `ActiveRecord::Migration.maintain_test_schema!` keeps test DB in sync
|
|
129
|
+
|
|
130
|
+
4. **RSpec Configuration**:
|
|
131
|
+
- `.rspec` and `.rspec_parallel` both use `--require spec_helper --format documentation`
|
|
132
|
+
- `spec/spec_helper.rb` loaded first (SimpleCov setup)
|
|
133
|
+
- `spec/rails_helper.rb` requires spec_helper, then adds Rails-specific config
|
|
134
|
+
- Support files auto-loaded from `spec/support/**/*.rb`
|
|
135
|
+
|
|
136
|
+
This approach is used by T20 (database setup) and T32 (CI/parallel execution).
|
|
137
|
+
|
|
138
|
+
## Solid Queue/Cache/Cable Stack (Task 12 - SolidGenerator)
|
|
139
|
+
|
|
140
|
+
The solid generator adds Solid Queue, Solid Cache, Solid Cable, and Mission Control Jobs to a Rails app.
|
|
141
|
+
|
|
142
|
+
### Canonical Sources
|
|
143
|
+
|
|
144
|
+
The generator sources configuration files from the ams2stats repo:
|
|
145
|
+
- **Verbatim files**: `config/queue.yml`, `config/cache.yml`, `config/recurring.yml`, `app/jobs/application_job.rb` are copied directly from ams2stats (SQLite-based).
|
|
146
|
+
- **Adapted files**: `config/cable.yml` is adapted from ams2stats (changes Redis URL handling for production). `config/initializers/mission_control.rb` is verbatim.
|
|
147
|
+
- **Generated files**: `config/initializers/solid.rb` is authored (sets queue_adapter, cache_store, and cable config).
|
|
148
|
+
|
|
149
|
+
### Key Differences from ams2stats
|
|
150
|
+
|
|
151
|
+
ams2stats uses SQLite and runs on Windows without `fork()`. The develoz-rails template targets Postgres with multi-database support (T14 will define named connections `queue`, `cache`, `cable`). The YAML files are structured identically but will reference those named connections in production.
|
|
152
|
+
|
|
153
|
+
### Generator Methods
|
|
154
|
+
|
|
155
|
+
1. `add_solid_gems` - adds solid_queue, solid_cache, solid_cable, mission_control-jobs
|
|
156
|
+
2. `create_queue_config` - generates config/queue.yml with dispatcher/worker config
|
|
157
|
+
3. `create_cache_config` - generates config/cache.yml with store options
|
|
158
|
+
4. `create_cable_config` - generates config/cable.yml with async/test/redis adapters
|
|
159
|
+
5. `create_recurring_config` - generates config/recurring.yml with cleanup task
|
|
160
|
+
6. `create_application_job` - generates app/jobs/application_job.rb base class
|
|
161
|
+
7. `create_mission_control_initializer` - generates config/initializers/mission_control.rb with HTTP basic auth
|
|
162
|
+
8. `create_solid_initializer` - generates config/initializers/solid.rb with Rails config
|
|
163
|
+
9. `insert_mission_control_route` - idempotently mounts MissionControl::Jobs::Engine at /jobs
|
|
164
|
+
|
|
165
|
+
### Idempotency
|
|
166
|
+
|
|
167
|
+
All methods use base class helpers (add_gem, insert_route, template) which guard against duplicates:
|
|
168
|
+
- `add_gem` checks if gem already exists in Gemfile
|
|
169
|
+
- `insert_route` checks if route already exists in routes.rb
|
|
170
|
+
- `template` overwrites files (idempotent by nature)
|
|
171
|
+
|
|
172
|
+
### Spec Coverage
|
|
173
|
+
|
|
174
|
+
The spec file has 40+ tests covering:
|
|
175
|
+
- Gem additions (4 tests)
|
|
176
|
+
- Config file generation (queue, cache, cable, recurring)
|
|
177
|
+
- Initializer generation (mission_control, solid)
|
|
178
|
+
- Route insertion
|
|
179
|
+
- Idempotency (run twice, verify single entries)
|
|
180
|
+
- Content validation (structure, env vars, frozen_string_literal)
|
|
181
|
+
|
|
182
|
+
## CI Generator (Task 13)
|
|
183
|
+
|
|
184
|
+
### Pattern
|
|
185
|
+
|
|
186
|
+
The ci generator follows the same pattern as tooling/testing/solid generators:
|
|
187
|
+
1. Generator class at `lib/generators/develoz/ci/ci_generator.rb`
|
|
188
|
+
2. Templates co-located in `lib/generators/develoz/ci/templates/`
|
|
189
|
+
3. Spec at `spec/develoz/generators/ci_generator_spec.rb`
|
|
190
|
+
|
|
191
|
+
### Key Files Generated
|
|
192
|
+
|
|
193
|
+
- **bin/ci** - CI entry point (`require "active_support/continuous_integration"`, loads `config/ci.rb`)
|
|
194
|
+
- **config/ci.rb** - Step definitions using `CI.run do ... end` with setup, style (Ruby/JS/YAML/Markdown), security, and test steps
|
|
195
|
+
- **Linter configs**: `.rubocop.yml`, `.reek.yml`, `biome.json`, `.stylelintrc.json`, `.haml-lint.yml`, `.markdownlint.json`, `.yamllint`
|
|
196
|
+
- **Workflow**: `.github/workflows/ci.yml` (simple workflow running `bin/ci`)
|
|
197
|
+
|
|
198
|
+
### Gem Additions
|
|
199
|
+
|
|
200
|
+
All added to `[:development, :test]` group:
|
|
201
|
+
- `rubocop-rails-omakase` (RuboCop with Rails defaults)
|
|
202
|
+
- `reek` (code smell detector)
|
|
203
|
+
- `flay` (code duplication analysis)
|
|
204
|
+
- `brakeman` (security static analysis)
|
|
205
|
+
- `bundler-audit` (gem vulnerability check)
|
|
206
|
+
- `haml_lint` (Haml template linting, require: false)
|
|
207
|
+
|
|
208
|
+
### Canonical Sources
|
|
209
|
+
|
|
210
|
+
Linter configs are adapted from `LooperInsights/looper_core` (RuboCop, Reek, Biome, Stylelint, haml-lint, markdownlint, yamllint). `bin/ci` and `config/ci.rb` are adapted from `mauriciozaffari/ams2stats`. The CI workflow is authored.
|
|
211
|
+
|
|
212
|
+
### Spec Structure
|
|
213
|
+
|
|
214
|
+
55+ tests covering:
|
|
215
|
+
- 6 gem addition tests (one per gem)
|
|
216
|
+
- 7 gem idempotency tests (combined and individual)
|
|
217
|
+
- File existence for all 10 generated files
|
|
218
|
+
- Content validation for each file (structure, key settings)
|
|
219
|
+
- Idempotency for all file types
|
|
220
|
+
- 100% line + branch coverage verified
|
|
221
|
+
|
|
222
|
+
## Database Generator (Task 14)
|
|
223
|
+
|
|
224
|
+
The database generator adds PostgreSQL and pg_search gems, generates database.yml with Postgres 18+ config, creates a pg_search initializer, and ensures .tool-versions includes postgres 18.
|
|
225
|
+
|
|
226
|
+
### Key Decisions
|
|
227
|
+
|
|
228
|
+
1. **database.yml structure**: Uses named connections (primary, queue, cache, cable) for all environments. Production uses DATABASE_URL pattern for the primary connection. Test env uses TEST_ENV_NUMBER suffix for parallel_tests.
|
|
229
|
+
|
|
230
|
+
2. **pg_search initializer**: Minimal - just a comment noting that PgSearch is included via the searchable concern (T15). No global configuration needed at this stage.
|
|
231
|
+
|
|
232
|
+
3. **.tool-versions handling**: Uses `inject_once` when the file exists (idempotent append), `create_file` when it doesn't. Both branches tested for 100% coverage.
|
|
233
|
+
|
|
234
|
+
### Generator Methods
|
|
235
|
+
|
|
236
|
+
1. `add_database_gems` - adds pg and pg_search gems
|
|
237
|
+
2. `create_database_config` - generates config/database.yml with Postgres config
|
|
238
|
+
3. `create_pg_search_initializer` - generates config/initializers/pg_search.rb
|
|
239
|
+
4. `ensure_postgres_tool_version` - ensures .tool-versions has postgres 18
|
|
240
|
+
|
|
241
|
+
### Idempotency
|
|
242
|
+
|
|
243
|
+
- `add_gem` guards against duplicate gem declarations
|
|
244
|
+
- `template` overwrites files (idempotent by nature)
|
|
245
|
+
- `inject_once` guards against duplicate content in .tool-versions
|
|
246
|
+
- `create_file` only called when .tool-versions doesn't exist
|
|
247
|
+
|
|
248
|
+
### Testing ENV.fetch Patterns
|
|
249
|
+
|
|
250
|
+
When testing templates that use `ENV.fetch`, the ERB is processed at generation time. To verify the pattern works, set the env var before running the generator and assert the evaluated value appears in the output. Use `begin/ensure` to restore the env var after the test.
|
|
251
|
+
|
|
252
|
+
## Concerns Generator (Task 15)
|
|
253
|
+
|
|
254
|
+
The concerns generator copies 4 concern files from `LooperInsights/looper_core` verbatim, plus 2 migrations and 4 app-level spec templates.
|
|
255
|
+
|
|
256
|
+
### Canonical Sources
|
|
257
|
+
|
|
258
|
+
- **Verbatim concerns**: `searchable_concern.rb` (PgSearch), `optimized_finders.rb` (in-memory finders), `transitionable.rb` (state machine + JSONB history), `configurable.rb` (polymorphic config + global fallback). None reference an app name, so no ERB substitution is needed — the `.tt` files are byte-identical to the fixtures.
|
|
259
|
+
- **Migrations**: `add_status_transitions.rb.tt` is adapted from looper_core's `add_status_columns_to_merchandizing_pages` migration (generic `:records` table name). `create_configurations.rb.tt` is adapted from looper_core's `create_configurations` migration (UUID primary keys with pgcrypto, polymorphic columns, concurrent indexes).
|
|
260
|
+
- **App-level specs**: Authored templates modeled on looper_core's own specs. Each spec defines its own test models using `stub_const` and `ActiveRecord::Schema.define` with `before(:all)`/`after(:all)` to create/drop temporary tables. This makes them self-contained and runnable in any generated Rails app.
|
|
261
|
+
|
|
262
|
+
### Transitionable Schema
|
|
263
|
+
|
|
264
|
+
The transitionable concern uses a `status` string column (enum) and a `status_transitions` JSONB column (default: `[]`) on the model itself — NOT a separate transitions table. Each entry is `{ "to_state" => "...", "created_at" => "<ISO8601>", "metadata" => {...} }`.
|
|
265
|
+
|
|
266
|
+
### Configurable Schema
|
|
267
|
+
|
|
268
|
+
The configurable concern uses a polymorphic `configurations` table with UUID primary keys, `configurable_type`/`configurable_id` columns, `key`/`value` (jsonb), and `active` boolean. Global configurations have `configurable_type` and `configurable_id` set to nil.
|
|
269
|
+
|
|
270
|
+
### RuboCop Notes
|
|
271
|
+
|
|
272
|
+
- `RSpec/DescribeClass` allows string describes that match `/^(?:(?:::)?[A-Z]\w*)+$/` — so `"Fidelity"` passes but `"Concerns Fidelity"` (with a space) does not. Use `"ConcernsFidelity"` instead.
|
|
273
|
+
- `Style/ExplicitBlockArgument` triggers when a helper method just wraps another method with `yield` and no other code. Use `# rubocop:disable`/`# rubocop:enable` or add content inside the block.
|
|
274
|
+
|
|
275
|
+
### Testing Notes
|
|
276
|
+
|
|
277
|
+
- Concern logic is validated in the generated app (T32), not the gem suite. The gem suite only tests the generator code (file creation, idempotency, content checks).
|
|
278
|
+
- Fidelity specs render each `.tt` template with ERB and compare to the canonical fixture using `match_canonical`. Since the concern templates are verbatim (no ERB), the rendered output equals the fixture byte-for-byte.
|
|
279
|
+
|
|
280
|
+
## Strict Loading Generator (Task 16)
|
|
281
|
+
|
|
282
|
+
The strict_loading generator creates a single initializer `config/initializers/strict_loading.rb` that configures ActiveRecord strict loading without editing `config/application.rb`.
|
|
283
|
+
|
|
284
|
+
### Key Decisions
|
|
285
|
+
|
|
286
|
+
1. **Initializer over application.rb injection**: The task explicitly preferred the initializer approach to avoid fragile `inject_once` into `config/application.rb`. A single `config/initializers/strict_loading.rb.tt` template sets all three config values in one file.
|
|
287
|
+
|
|
288
|
+
2. **Per-env violation via case statement**: Instead of generating separate env-specific initializers or editing `config/environments/*.rb`, the initializer uses a `case Rails.env` block inside `Rails.application.configure do`. Test env raises (`:raise`); all other envs (development, production) log (`:log`). This keeps everything in one file and is evaluated at boot time per environment.
|
|
289
|
+
|
|
290
|
+
3. **Config values set**: `strict_loading_by_default = true`, `strict_loading_mode = :n_plus_one_only`, `strict_loading_violation` = `:raise` (test) / `:log` (dev, prod).
|
|
291
|
+
|
|
292
|
+
### Generator Methods
|
|
293
|
+
|
|
294
|
+
1. `create_strict_loading_initializer` - generates `config/initializers/strict_loading.rb` from template
|
|
295
|
+
|
|
296
|
+
### Idempotency
|
|
297
|
+
|
|
298
|
+
The `template` method overwrites the file on each run, making it idempotent by nature. No `inject_once` needed since no existing files are modified.
|
|
299
|
+
|
|
300
|
+
### Spec Coverage
|
|
301
|
+
|
|
302
|
+
9 tests covering:
|
|
303
|
+
- destination_root binding
|
|
304
|
+
- File existence
|
|
305
|
+
- frozen_string_literal comment
|
|
306
|
+
- strict_loading_by_default = true
|
|
307
|
+
- strict_loading_mode = :n_plus_one_only
|
|
308
|
+
- :raise in test env
|
|
309
|
+
- :log in dev/prod
|
|
310
|
+
- strict_loading_violation config key present
|
|
311
|
+
- Idempotency (run twice, verify identical output)
|
|
312
|
+
|
|
313
|
+
## Maintenance Generator (Task 17)
|
|
314
|
+
|
|
315
|
+
The maintenance generator adds the `maintenance_tasks` gem (Shopify), mounts the engine at `/maintenance_tasks`, and templates an example task plus a rake task.
|
|
316
|
+
|
|
317
|
+
### Key Decisions
|
|
318
|
+
|
|
319
|
+
1. **Example task structure**: Based on the maintenance_tasks gem API (`MaintenanceTasks::Task` with `collection`, `count`, `process`). The example uses `User.in_batches(of: 1000)` for collection, `User.count` for count, and `batch.update_all(updated_at: Time.current)` for process. This mirrors the real task patterns in looper_core (e.g. `BackfillMatchHasScreenshotTask`).
|
|
320
|
+
|
|
321
|
+
2. **Rake task pattern**: Authored a minimal `maintenance_counters.rake` with two tasks: `update_counters` (eager loads models, iterates descendants) and `purge_stale` (RETENTION_DAYS env var, cutoff date). Modeled on looper_core's `counters.rake` and `data_retention.rake` but kept minimal and illustrative.
|
|
322
|
+
|
|
323
|
+
3. **Route mounting**: Uses `insert_route` from base class with `mount MaintenanceTasks::Engine, at: "/maintenance_tasks"`. Idempotent via the base class's content-inclusion guard.
|
|
324
|
+
|
|
325
|
+
### Generator Methods
|
|
326
|
+
|
|
327
|
+
1. `add_maintenance_tasks_gem` - adds `maintenance_tasks` gem via `add_gem`
|
|
328
|
+
2. `insert_maintenance_tasks_route` - mounts engine at `/maintenance_tasks` via `insert_route`
|
|
329
|
+
3. `create_example_task` - generates `app/tasks/maintenance/example_task.rb` from template
|
|
330
|
+
4. `create_rake_task` - generates `lib/tasks/maintenance_counters.rake` from template
|
|
331
|
+
|
|
332
|
+
### Spec Coverage
|
|
333
|
+
|
|
334
|
+
13 tests covering:
|
|
335
|
+
- destination_root binding
|
|
336
|
+
- Gem addition
|
|
337
|
+
- Route insertion
|
|
338
|
+
- Example task file existence + structure (class, collection, process)
|
|
339
|
+
- frozen_string_literal on both generated files
|
|
340
|
+
- Rake task file existence + namespace/tasks
|
|
341
|
+
- Idempotency for gem, route, example task, and rake task (4 tests)
|
|
342
|
+
|
|
343
|
+
### maintenance_tasks Gem API
|
|
344
|
+
|
|
345
|
+
The gem requires tasks to be in `app/tasks/maintenance/` and subclass `MaintenanceTasks::Task`. Required methods: `collection` (returns AR Relation, BatchEnumerator, or Array) and `process(item)`. Optional: `count` for progress reporting. The gem provides a web UI at the mount point for queuing, pausing, and monitoring tasks.
|
|
346
|
+
|
|
347
|
+
## Frontend Core Generator (Task 18)
|
|
348
|
+
|
|
349
|
+
The frontend_core generator adds importmap-rails, pagy (conditionally), and annotaterb, plus their configuration files.
|
|
350
|
+
|
|
351
|
+
### Key Decisions
|
|
352
|
+
|
|
353
|
+
1. **class_option for skip_pagy**: Uses `class_option :skip_pagy, type: :boolean, default: false` to allow opting out of Pagy. In specs, pass `"skip_pagy" => true` as the options hash (second argument to `.new`). Thor does NOT apply class_option defaults when instantiating directly in tests, so `options[:skip_pagy]` is `nil` (falsy) by default and `true` when explicitly set. This works correctly with `return if options[:skip_pagy]` since `nil` is falsy.
|
|
354
|
+
|
|
355
|
+
2. **Importmap config**: Based on inscripto-v2's `config/importmap.rb` (fetched via `gh api`). Trimmed to a sensible default for a new Rails app: application pin, Hotwire (turbo-rails, stimulus, stimulus-loading), Trix/actiontext, activestorage, and `pin_all_from` for app/javascript/controllers.
|
|
356
|
+
|
|
357
|
+
3. **Pagy initializer**: Based on looper_core's pagy initializer but trimmed to only the 4 required extras (arel, array, countless, bootstrap) plus `Pagy::DEFAULT[:limit] = 25` and `Pagy::DEFAULT.freeze`. The looper_core version has many more extras commented out; we keep it minimal.
|
|
358
|
+
|
|
359
|
+
4. **Annotaterb config (models-only)**: The `.annotaterb.yml` uses symbol keys (e.g. `:models: true`) matching the YAML dump format from `AnnotateRb::ConfigGenerator.default_config_yml`. Models-only means: `models: true`, `routes: false`, `active_admin: false`, and all `exclude_*` keys set to `true` (tests, fixtures, factories, serializers, controllers, helpers, scaffolds). The `additional_file_patterns: []` key is also included.
|
|
360
|
+
|
|
361
|
+
### Generator Methods
|
|
362
|
+
|
|
363
|
+
1. `add_frontend_gems` - adds importmap-rails and annotaterb (group: :development)
|
|
364
|
+
2. `add_pagy_gem` - adds pagy gem (skipped when `options[:skip_pagy]`)
|
|
365
|
+
3. `create_importmap_config` - generates config/importmap.rb from template
|
|
366
|
+
4. `create_pagy_initializer` - generates config/initializers/pagy.rb (skipped when `options[:skip_pagy]`)
|
|
367
|
+
5. `create_annotaterb_config` - generates .annotaterb.yml from template
|
|
368
|
+
|
|
369
|
+
### Spec Coverage (Both Branches)
|
|
370
|
+
|
|
371
|
+
22 tests covering:
|
|
372
|
+
- destination_root binding
|
|
373
|
+
- Gem additions: importmap-rails, annotaterb (with development group), pagy (default), pagy absent with skip_pagy
|
|
374
|
+
- Importmap: file existence, pin content, frozen_string_literal
|
|
375
|
+
- Pagy: file existence (default), absent with skip_pagy, backend extras (arel/array/countless), bootstrap extra, frozen_string_literal
|
|
376
|
+
- Annotaterb: file existence, models-only config (models true, routes false, active_admin false, exclude_* true), generated with skip_pagy too
|
|
377
|
+
- Importmap generated with skip_pagy too
|
|
378
|
+
- Idempotency: gems, importmap, pagy initializer, annotaterb config (4 tests)
|
|
379
|
+
|
|
380
|
+
### Annotaterb Configuration Keys
|
|
381
|
+
|
|
382
|
+
From `lib/annotate_rb/options.rb` in the annotaterb gem:
|
|
383
|
+
- `FLAG_OPTIONS`: boolean keys like `exclude_tests`, `exclude_fixtures`, `exclude_factories`, `exclude_serializers`, `exclude_controllers`, `exclude_helpers`, `exclude_scaffolds`, `classified_sort`, `show_indexes`, `show_foreign_keys`
|
|
384
|
+
- `OTHER_OPTIONS`: `active_admin`, `models`, `routes`, `additional_file_patterns`
|
|
385
|
+
- `POSITION_OPTIONS`: `position` (nil/before/top/after/bottom)
|
|
386
|
+
|
|
387
|
+
The YAML uses symbol-prefixed keys (`:models: true`) because `YAML.dump` on a hash with symbol keys produces this format.
|
|
388
|
+
|
|
389
|
+
## Docs Render Generator (Task 19)
|
|
390
|
+
|
|
391
|
+
The docs_render generator adds Redcarpet (Markdown to HTML), Rouge (syntax highlighting), and Mermaid (client-side diagram rendering) to a Rails app, along with a DocsController and Document model for serving markdown documentation.
|
|
392
|
+
|
|
393
|
+
### Canonical Sources
|
|
394
|
+
|
|
395
|
+
- **Verbatim files**: `app/models/document.rb` (Document model with inline Redcarpet::Render::HTML subclass and Rouge syntax highlighting), `app/javascript/docs.js` (Mermaid client-side rendering), `app/assets/stylesheets/documentation.scss` (docs styling with dark mode support). All three are byte-identical to looper_core fixtures.
|
|
396
|
+
- **Adapted files**: `app/controllers/docs_controller.rb` is adapted from looper_core (changed `AdminController` to `ApplicationController` for generic use). `app/views/docs/show.html.erb.tt` is adapted from looper_core's `show.html.haml` (converted HAML to ERB, removed app-specific helpers like `content_header` and `shared/box` partial).
|
|
397
|
+
- **Authored files**: `config/initializers/redcarpet_rouge.rb` is authored (looper_core has no separate redcarpet/rouge initializer; the Document model handles all configuration inline).
|
|
398
|
+
|
|
399
|
+
### ERB-in-ERB Escaping for View Templates
|
|
400
|
+
|
|
401
|
+
The `show.html.erb.tt` template contains app-level ERB that must be preserved literally in the generated `.html.erb` file. Use `<%%` (double percent) to escape `<%` so Thor's template engine outputs literal `<%` instead of evaluating it at generation time:
|
|
402
|
+
|
|
403
|
+
```
|
|
404
|
+
<%% content_for :head do %>
|
|
405
|
+
<%%= javascript_include_tag 'docs', 'data-turbo-track': 'reload' %>
|
|
406
|
+
<%% end %>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
This produces the correct ERB output in the generated `show.html.erb`:
|
|
410
|
+
```erb
|
|
411
|
+
<% content_for :head do %>
|
|
412
|
+
<%= javascript_include_tag 'docs', 'data-turbo-track': 'reload' %>
|
|
413
|
+
<% end %>
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Document Model Architecture
|
|
417
|
+
|
|
418
|
+
The Document model is a plain Ruby class (includes `ActiveModel::Model`) that:
|
|
419
|
+
1. Reads markdown files from `docs/` directory via `Rails.root.glob`
|
|
420
|
+
2. Uses a custom `Redcarpet::Render::HTML` subclass (`Document::Renderer`) with:
|
|
421
|
+
- Obsidian wikilink support (`[[Note]]`, `[[Note#Heading]]`, `[[Note|Alias]]`)
|
|
422
|
+
- Callout blocks (`> [!NOTE]`, `> [!TIP]`, etc.)
|
|
423
|
+
- Mermaid diagram detection (language == 'mermaid' emits `<pre class="mermaid">` instead of syntax highlighting)
|
|
424
|
+
- Rouge syntax highlighting with Github dark theme
|
|
425
|
+
3. Sanitizes HTML output with extended allowed tags/attributes
|
|
426
|
+
|
|
427
|
+
### Route Pattern
|
|
428
|
+
|
|
429
|
+
The route `get "docs(/*id)" => "docs#show"` uses a glob parameter to match nested paths like `/docs`, `/docs/README`, `/docs/platform-guide/AUDITS`. The `(*id)` syntax makes the parameter optional.
|
|
430
|
+
|
|
431
|
+
### Generator Methods
|
|
432
|
+
|
|
433
|
+
1. `add_docs_gems` - adds redcarpet and rouge gems
|
|
434
|
+
2. `create_docs_controller` - generates app/controllers/docs_controller.rb
|
|
435
|
+
3. `create_document_model` - generates app/models/document.rb (verbatim)
|
|
436
|
+
4. `create_docs_views` - generates app/views/docs/show.html.erb (adapted from HAML)
|
|
437
|
+
5. `create_docs_javascript` - generates app/javascript/docs.js (verbatim)
|
|
438
|
+
6. `create_docs_stylesheet` - generates app/assets/stylesheets/documentation.scss (verbatim)
|
|
439
|
+
7. `create_redcarpet_rouge_initializer` - generates config/initializers/redcarpet_rouge.rb (authored)
|
|
440
|
+
8. `insert_docs_route` - inserts `get "docs(/*id)" => "docs#show"` route
|
|
441
|
+
|
|
442
|
+
### Spec Coverage
|
|
443
|
+
|
|
444
|
+
28 tests covering:
|
|
445
|
+
- destination_root binding
|
|
446
|
+
- Gem additions (redcarpet, rouge)
|
|
447
|
+
- Controller existence, inheritance, frozen_string_literal
|
|
448
|
+
- Document model existence, Redcarpet/Rouge references, frozen_string_literal
|
|
449
|
+
- View existence, document render, javascript include
|
|
450
|
+
- JS existence, mermaid import
|
|
451
|
+
- Stylesheet existence, documentation section
|
|
452
|
+
- Initializer existence, Redcarpet/Rouge mentions, frozen_string_literal
|
|
453
|
+
- Route insertion
|
|
454
|
+
- Idempotency for gems, route, controller, model, view, JS, stylesheet, initializer (8 tests)
|
|
455
|
+
|
|
456
|
+
### Fidelity Specs
|
|
457
|
+
|
|
458
|
+
3 fidelity specs for verbatim files:
|
|
459
|
+
- `document.rb` matches canonical fixture
|
|
460
|
+
- `docs.js` matches canonical fixture
|
|
461
|
+
- `documentation.scss` matches canonical fixture
|
|
462
|
+
|
|
463
|
+
## Doc Specs Generator (Task 20)
|
|
464
|
+
|
|
465
|
+
The doc_specs generator creates a self-documenting system spec pipeline: `bin/generate-docs` (parses `@category`/`@order` doc-comments + `doc_screenshot` calls from system specs to produce markdown + screenshots), a `doc_screenshot` Capybara helper, a `docs:check` rake task, and an example annotated system spec.
|
|
466
|
+
|
|
467
|
+
### Canonical Sources
|
|
468
|
+
|
|
469
|
+
- **Verbatim file**: `bin/generate-docs` is copied byte-for-byte from `LooperInsights/looper_core/bin/generate-docs`. It is a Ruby script (shebang `#!/usr/bin/env ruby`), not bash as the task description stated. It is excluded from SimpleCov coverage via the `/templates/` filter. Fidelity spec verifies the `.tt` template renders identically to the fixture.
|
|
470
|
+
- **Authored file**: `spec/support/doc_screenshot_helper.rb` is extracted from looper_core's `spec/rails_helper.rb` where `doc_screenshot` was defined as an inline anonymous module (`config.include Module.new { def doc_screenshot... }`). Authored as a standalone `DocScreenshotHelper` module with `RSpec.configure` to include it for `type: :system`.
|
|
471
|
+
- **Adapted file**: `lib/tasks/docs_check.rake` is extracted from looper_core's `Rakefile` where the `docs:check` namespace was defined inline. Adapted into a standalone rake file.
|
|
472
|
+
- **Authored file**: `spec/system/example_doc_spec.rb` demonstrates the `@category`, `@order`, and `doc_screenshot` pattern on a fresh app.
|
|
473
|
+
|
|
474
|
+
### Generator Methods
|
|
475
|
+
|
|
476
|
+
1. `create_generate_docs_script` - templates `bin/generate-docs` and calls `File.chmod(0o755, dest)` to make it executable
|
|
477
|
+
2. `create_doc_screenshot_helper` - templates `spec/support/doc_screenshot_helper.rb`
|
|
478
|
+
3. `create_docs_check_rake_task` - templates `lib/tasks/docs_check.rake`
|
|
479
|
+
4. `create_example_system_spec` - templates `spec/system/example_doc_spec.rb`
|
|
480
|
+
|
|
481
|
+
### Key Decisions
|
|
482
|
+
|
|
483
|
+
1. **No `File.exist?` guard on chmod**: Initially added `File.chmod(0o755, dest) if File.exist?(dest)` as defensive code, but this created an uncovered branch (the false path can't be reached since `template` always creates the file). Removed the guard for 100% branch coverage. The `template` method always creates the destination file, so the guard was unnecessary.
|
|
484
|
+
|
|
485
|
+
2. **ERB safety of bin/generate-docs**: The script contains `#{...}` Ruby string interpolations and `<<~` heredocs, but no `<%` or `<%=` sequences, so it is safe to process through Thor's ERB template engine as a `.tt` file.
|
|
486
|
+
|
|
487
|
+
3. **doc_screenshot helper location**: Placed in `spec/support/` (not `app/helpers/`) because it is a test-only helper included via `RSpec.configure` for `type: :system` specs, matching looper_core's pattern.
|
|
488
|
+
|
|
489
|
+
### Spec Coverage
|
|
490
|
+
|
|
491
|
+
22 tests covering:
|
|
492
|
+
- destination_root binding
|
|
493
|
+
- bin/generate-docs: existence, executable bit, ruby shebang, frozen_string_literal
|
|
494
|
+
- doc_screenshot_helper: existence, doc_screenshot method, frozen_string_literal, RSpec configure
|
|
495
|
+
- docs_check.rake: existence, namespace + task, frozen_string_literal
|
|
496
|
+
- example_doc_spec: existence, @category, @order, doc_screenshot call, frozen_string_literal
|
|
497
|
+
- Idempotency: bin/generate-docs, doc_screenshot_helper, docs_check.rake, example_doc_spec, executable bit preserved (5 tests)
|
|
498
|
+
|
|
499
|
+
### Fidelity Specs
|
|
500
|
+
|
|
501
|
+
1 fidelity spec for verbatim file:
|
|
502
|
+
- `bin/generate-docs` matches canonical fixture
|
|
503
|
+
|
|
504
|
+
## Agents Docs Generator (Task 21)
|
|
505
|
+
|
|
506
|
+
The agents_docs generator creates root `AGENTS.md`, three adapted docs (`docs/development.md`, `docs/testing.md`, `docs/performance.md`), a verbatim PR template, and Faraday + VCR/WebMock support files with an example API spec.
|
|
507
|
+
|
|
508
|
+
### Canonical Sources
|
|
509
|
+
|
|
510
|
+
- **Adapted files**: `AGENTS.md`, `docs/development.md`, `docs/testing.md`, `docs/performance.md` are adapted from `develoz-com/agent`. The source AGENTS.md is titled "# CLAUDE.md" but the adapted version uses "# AGENTS.md". All `bin/run` and `bin/develoz_agent` references are replaced with standard `bin/rails` / `bundle exec` commands. Agent-specific sections (develoz_agent CLI, ticket-state-machine docs, Jira API caching, opencode/git worktree stubs, database-backups container) are removed. Generic patterns (Docker-first, coverage requirements, N+1 detection, query patterns, caching) are retained.
|
|
511
|
+
- **Verbatim file**: `.github/pull_request_template.md` is copied byte-for-byte from `develoz-com/inscripto-v2`. The fixture already exists at `spec/fixtures/canonical/develoz-com-inscripto-v2/.github/pull_request_template.md` (from T6 demo), so no new fixture was needed. A new fidelity spec (`spec/fidelity/agents_docs_fidelity_spec.rb`) verifies the agents_docs template matches the same fixture.
|
|
512
|
+
- **Authored files**: `spec/support/vcr.rb.tt` (VCR config with WebMock), `spec/support/faraday.rb.tt` (FaradayFactory module with retry middleware), `spec/requests/example_api_spec.rb.tt` (demonstrates VCR + Faraday pattern), `spec/cassettes/Example_API/fetches_data_from_an_external_API.yml.tt` (stub VCR cassette fixture).
|
|
513
|
+
|
|
514
|
+
### Generator Methods
|
|
515
|
+
|
|
516
|
+
1. `add_api_gems` - adds faraday, faraday-retry (default group); vcr, webmock (test group)
|
|
517
|
+
2. `create_agents_md` - templates AGENTS.md
|
|
518
|
+
3. `create_docs` - templates docs/development.md, docs/testing.md, docs/performance.md
|
|
519
|
+
4. `create_pr_template` - templates .github/pull_request_template.md
|
|
520
|
+
5. `create_vcr_support` - templates spec/support/vcr.rb
|
|
521
|
+
6. `create_faraday_support` - templates spec/support/faraday.rb
|
|
522
|
+
7. `create_example_api_spec` - templates spec/requests/example_api_spec.rb + stub cassette
|
|
523
|
+
|
|
524
|
+
### Gem Idempotency Test Pitfall
|
|
525
|
+
|
|
526
|
+
When testing gem idempotency for `faraday` and `faraday-retry`, `gemfile.scan("faraday")` matches both gems (2 occurrences). Use a regex anchored to the gem declaration: `gemfile.scan(/^\s*gem\s+["']faraday["']/m).length` to count only exact `gem "faraday"` lines, not `gem "faraday-retry"`.
|
|
527
|
+
|
|
528
|
+
### VCR Cassette as Template
|
|
529
|
+
|
|
530
|
+
The stub VCR cassette is a YAML file with no ERB content, but it is processed through Thor's `template` method (with `.tt` extension) for consistency with the generator pattern. Since the YAML contains no `<%` sequences, ERB processing is a no-op and the output is identical to the source.
|
|
531
|
+
|
|
532
|
+
### Spec Coverage
|
|
533
|
+
|
|
534
|
+
39 tests covering:
|
|
535
|
+
- destination_root binding
|
|
536
|
+
- 4 gem additions (faraday, faraday-retry, vcr, webmock)
|
|
537
|
+
- AGENTS.md existence, content (Quality Expectations, docs references)
|
|
538
|
+
- 3 docs existence + content (development Command Reference, testing coverage requirement, performance N+1 Detection)
|
|
539
|
+
- PR template existence + content (Changes section)
|
|
540
|
+
- vcr.rb existence, frozen_string_literal, cassette_library_dir, hook_into :webmock
|
|
541
|
+
- faraday.rb existence, frozen_string_literal, FaradayFactory module, :retry middleware
|
|
542
|
+
- example spec existence, frozen_string_literal, :vcr metadata, FaradayFactory usage
|
|
543
|
+
- cassette existence, http_interactions content
|
|
544
|
+
- 7 idempotency tests (gems, AGENTS.md, docs, PR template, vcr.rb, faraday.rb, example spec, cassette)
|
|
545
|
+
|
|
546
|
+
### Fidelity Specs
|
|
547
|
+
|
|
548
|
+
1 fidelity spec for verbatim file:
|
|
549
|
+
- `.github/pull_request_template.md` matches canonical fixture (reuses existing inscripto-v2 fixture)
|
|
550
|
+
|
|
551
|
+
## Versioning Generator (Task 36)
|
|
552
|
+
|
|
553
|
+
The versioning generator replicates inscripto-v2's app versioning scheme: an `APP_VERSION` constant, an `app_version` helper, and a reusable display partial.
|
|
554
|
+
|
|
555
|
+
### Canonical Sources
|
|
556
|
+
|
|
557
|
+
- **Verbatim (method snippet)**: The `app_version` method (`def app_version; APP_VERSION; end`) is copied verbatim from inscripto-v2's `app/helpers/application_helper.rb`. Stored as fixture `spec/fixtures/canonical/develoz-com-inscripto-v2/app/helpers/app_version.rb`. Fidelity spec verifies the rendered `application_helper.rb.tt` template includes the fixture content.
|
|
558
|
+
- **Adapted (constant injection)**: The `APP_VERSION = ENV.fetch("APP_VERSION", "dev")` line is adapted from inscripto-v2's `config/constants.rb`. Instead of creating a separate file, it is injected into the app's existing `config/initializers/constants.rb` (created by T10's tooling generator) via `inject_once` after the `# additional constants appended by generators` marker.
|
|
559
|
+
- **Authored (partial)**: The `_app_version.html.erb.tt` partial is authored, extracted from inscripto-v2's inline sidebar version display (`app/views/layouts/_admin_sidebar_nav.html.erb`) into a reusable partial.
|
|
560
|
+
|
|
561
|
+
### Generator Methods
|
|
562
|
+
|
|
563
|
+
1. `inject_app_version_constant` - injects `APP_VERSION = ENV.fetch("APP_VERSION", "dev")` into `config/initializers/constants.rb` after the marker comment, idempotently via `inject_once`
|
|
564
|
+
2. `create_application_helper` - if `app/helpers/application_helper.rb` exists, injects the `app_version` method via `inject_once` with `before: /^end\s*$/`; else creates the file from template
|
|
565
|
+
3. `create_app_version_partial` - templates `app/views/shared/_app_version.html.erb` from `.erb.tt`
|
|
566
|
+
|
|
567
|
+
### Consumer-Side Integrations (NOT in T36)
|
|
568
|
+
|
|
569
|
+
T36 only provides the core constant + helper + partial. The following consumer-side integrations are handled by their respective tasks:
|
|
570
|
+
- **T25 (admin)**: Admin sidebar should render `<%= render "shared/app_version" %>` to display the version
|
|
571
|
+
- **T26 (PWA)**: Service worker should use `APP_VERSION` for `CACHE_VERSION`
|
|
572
|
+
- **T30 (kamal)**: Deploy config should pass `APP_VERSION` as an env var to the container
|
|
573
|
+
|
|
574
|
+
### Key Decisions
|
|
575
|
+
|
|
576
|
+
1. **inject_once for both constants.rb and application_helper.rb**: The `inject_once` base class helper provides idempotency through content-inclusion checking. For constants.rb, the `after:` parameter targets the marker comment. For application_helper.rb, the `before: /^end\s*$/` parameter targets the module's closing `end`.
|
|
577
|
+
|
|
578
|
+
2. **Both branches of application_helper.rb tested**: The `if File.exist?` branch has two paths - inject (file exists) and create (file absent). Both are tested with seeded fixtures. The inject branch seeds an existing helper with an `existing_method`, then verifies both methods coexist.
|
|
579
|
+
|
|
580
|
+
3. **ERB-in-ERB escaping for partial**: The `_app_version.html.erb.tt` template uses `<%%` and `<%%=` to escape ERB tags so Thor's template engine outputs literal `<%` and `<%=` in the generated `.erb` file.
|
|
581
|
+
|
|
582
|
+
4. **Anonymous block forwarding for with_tmp_dir**: Ruby 4.0's anonymous block forwarding (`&` without name) avoids `Style/ExplicitBlockArgument` and `Naming/BlockForwarding` cops.
|
|
583
|
+
|
|
584
|
+
### Spec Coverage
|
|
585
|
+
|
|
586
|
+
13 tests covering:
|
|
587
|
+
- destination_root binding
|
|
588
|
+
- APP_VERSION injection into constants.rb (content, position after marker, idempotency)
|
|
589
|
+
- application_helper.rb creation (content, frozen_string_literal, idempotency)
|
|
590
|
+
- application_helper.rb injection (method coexists with existing, idempotency)
|
|
591
|
+
- Partial creation, content, idempotency
|
|
592
|
+
|
|
593
|
+
### Fidelity Specs
|
|
594
|
+
|
|
595
|
+
1 fidelity spec:
|
|
596
|
+
- `app_version` method in rendered template matches canonical inscripto-v2 fixture
|
|
597
|
+
|
|
598
|
+
## Task 9 - InstallGenerator (Meta-Generator Invocation)
|
|
599
|
+
|
|
600
|
+
### The Core Problem
|
|
601
|
+
The install generator must invoke sub-generators (tooling, testing, solid, etc.) programmatically. Standard Thor invocation mechanisms ALL FAIL:
|
|
602
|
+
- `invoke_all` - treats base-class helpers (`inject_once`, `add_gem`) as tasks, raises "no such task"
|
|
603
|
+
- `generate` / `Rails::Generators.invoke` - same Thor task-resolution failure
|
|
604
|
+
- `Rails::Generators.invoke` with explicit args - same failure path
|
|
605
|
+
|
|
606
|
+
### The Working Approach: Direct Instantiation + public_instance_methods(false)
|
|
607
|
+
Instead of Thor's invocation chain, directly require the generator file, instantiate it bound to `destination_root`, and call each public method defined in the SUBCLASS:
|
|
608
|
+
|
|
609
|
+
```ruby
|
|
610
|
+
def invoke_generator(name)
|
|
611
|
+
require "generators/develoz/#{name}/#{name}_generator"
|
|
612
|
+
klass = Develoz::Generators.const_get("#{name.camelize}Generator")
|
|
613
|
+
gen = klass.new([], {}, destination_root: destination_root)
|
|
614
|
+
klass.public_instance_methods(false).each { |method| gen.public_send(method) }
|
|
615
|
+
rescue LoadError, NameError
|
|
616
|
+
say "generator develoz:#{name} not yet available", :yellow
|
|
617
|
+
end
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
**Why `public_instance_methods(false)`**: The `false` argument excludes inherited methods. This is critical because `Develoz::Generators::Base` defines helper methods (`inject_once`, `add_gem`, `insert_route`, etc.) that are NOT generator steps - they're utilities called BY the steps. Without `false`, Thor would try to call those helpers as standalone steps, causing errors or duplicate execution.
|
|
621
|
+
|
|
622
|
+
**Why `klass.new([], {}, destination_root: destination_root)`**: The `[]` is args (empty), `{}` is options (empty hash - sub-generators get their own class_options, not the install generator's), and `destination_root:` binds all file operations to the target directory. This is the same signature Thor uses internally.
|
|
623
|
+
|
|
624
|
+
### Rescue Branch for Not-Yet-Available Generators
|
|
625
|
+
Opt-in generators (api, auth, pwa, etc.) don't have generator files yet. The `require` raises `LoadError`, caught by the rescue, which prints a yellow warning. This is tested by calling `invoke_generator("api")` directly and asserting `/not yet available/` on stdout.
|
|
626
|
+
|
|
627
|
+
### Idempotency
|
|
628
|
+
The install generator is idempotent because every sub-generator's methods are idempotent (via `inject_once` content-checking, `add_gem` regex matching, Thor's `template` "identical" behavior). Running install twice produces no duplicate Gemfile entries.
|
|
629
|
+
|
|
630
|
+
### RuboCop Notes
|
|
631
|
+
- `build_options` originally had 24 ABC size (limit 17) due to 11 `options[:flag]` lookups. Fixed by extracting `OPTION_FLAGS` constant and using `index_with` to build the hash dynamically.
|
|
632
|
+
- Constants defined under `private` need `private_constant :NAME` - the `private` keyword does NOT affect constant visibility (triggers `Lint/UselessConstantScoping`).
|
|
633
|
+
|
|
634
|
+
## Task 8 - CLI (Thor-based entry point)
|
|
635
|
+
|
|
636
|
+
### Overview
|
|
637
|
+
|
|
638
|
+
The CLI (`lib/develoz/cli.rb`) is the user-facing entry point. It provides `develoz version` and `develoz new APP_NAME` commands. The `new` command resolves opt-in flags, resolves Ruby/Rails versions via `VersionResolver`, runs `rails new`, writes version files, and invokes the `InstallGenerator`.
|
|
639
|
+
|
|
640
|
+
### Thor Boolean Option Behavior (VERIFIED)
|
|
641
|
+
|
|
642
|
+
Thor boolean options WITHOUT `default:`:
|
|
643
|
+
- `options[:flag]` is `nil` when the flag is NOT passed
|
|
644
|
+
- `options[:flag]` is `true` when `--flag` is passed
|
|
645
|
+
|
|
646
|
+
So `options[flag].nil?` correctly means "not passed". This is used in `resolve_flag` to distinguish between "user explicitly passed `--api`" (use the value) and "user didn't pass it" (prompt or default to false with `--yes`).
|
|
647
|
+
|
|
648
|
+
### require_relative Path
|
|
649
|
+
|
|
650
|
+
The prompt specified `require_relative "develoz/version"` but `cli.rb` lives at `lib/develoz/cli.rb`. `require_relative` resolves relative to the current file, so `require_relative "develoz/version"` from `lib/develoz/cli.rb` looks for `lib/develoz/develoz/version.rb` (wrong). The correct path is `require_relative "version"` since the file is already inside the `develoz/` directory.
|
|
651
|
+
|
|
652
|
+
### RuboCop Fixes Applied
|
|
653
|
+
|
|
654
|
+
1. `each_with_object({}) { |flag, h| h[flag] = resolve_flag(flag) }` -> `to_h { |flag| [flag, resolve_flag(flag)] }` (Rails/IndexWith, Style/ReduceToHash)
|
|
655
|
+
2. Empty line after guard clause (`return false if options[:yes]`)
|
|
656
|
+
3. `Performance/ConstantRegexp` - append `/o` to `/develoz #{Develoz::VERSION}/o`
|
|
657
|
+
4. `RSpec/AnyInstance` and `RSpec/MultipleExpectations` - disabled with `# rubocop:disable`/`# rubocop:enable` block, matching the pattern in `canonical_fetcher_spec.rb`
|
|
658
|
+
|
|
659
|
+
### Spec Strategy
|
|
660
|
+
|
|
661
|
+
The spec stubs `VersionResolver`, `InstallGenerator`, `system`, and `Dir.chdir` to test the CLI in isolation without actually running `rails new`. `allow_any_instance_of` is used for `system` because the CLI calls it as a private method on the instance, and there's no way to get the instance reference before `described_class.start` creates it.
|
|
662
|
+
|
|
663
|
+
## API Generator (Task 22)
|
|
664
|
+
|
|
665
|
+
The api generator adds Blueprinter, RSwag (api/ui/specs), generates a versioned API base controller, blueprinter initializer with LowerCamelTransformer, rswag initializers, mounts rswag routes, and creates an example rswag request spec.
|
|
666
|
+
|
|
667
|
+
### Canonical Sources
|
|
668
|
+
|
|
669
|
+
- **Adapted files**: All templates are adapted from `LooperInsights/looper_core`:
|
|
670
|
+
- `app/controllers/api/v1/base_controller.rb.tt` adapted from looper_core's `app/controllers/api/v3/base_controller.rb`. Key changes: v3→v1 namespace, removed all Cognito/auth_validation dependencies (`context`, `ensure_authenticated`, `valid_token?`, `admin_logged_in?`, `scoped_client`, `user_id`, `user_email`), replaced with a simple `def authenticate; end` stub (no-op by default, override in subclasses). Retained: `Pagy::Backend`, `render_json_error`, `render_resource`/`render_resources`, `render_json`, `paginated`, `meta`, `common_params`, `default_page_size` class method, `rescue_from` for RecordNotFound/RecordInvalid.
|
|
671
|
+
- `config/initializers/blueprinter.rb.tt` adapted from looper_core's `config/initializers/blueprinter.rb`. Kept `LowerCamelTransformer` class (verbatim) and `default_transformers` + `datetime_format`. Removed `BlueprinterActiveRecord::Preloader` (requires separate `blueprinter-activerecord` gem not added) and the `unless` lambda (app-specific).
|
|
672
|
+
- `config/initializers/rswag_api.rb.tt` adapted from looper_core (added `# frozen_string_literal: true`, otherwise verbatim).
|
|
673
|
+
- `config/initializers/rswag_ui.rb.tt` adapted from looper_core (endpoint label changed from 'Looper Core API V3' to 'API V1', path v3→v1, added frozen_string_literal).
|
|
674
|
+
- **Authored files**: `app/blueprints/example_blueprint.rb.tt` (minimal blueprint inheriting `Blueprinter::Base` directly, since the generated app has no `BaseBlueprint`), `spec/requests/api/v1/examples_spec.rb.tt` (minimal rswag spec demonstrating `path`/`response`/`schema` DSL, no auth dependencies).
|
|
675
|
+
|
|
676
|
+
### No v3→v1 Chain Pattern
|
|
677
|
+
|
|
678
|
+
LooperInsights/looper_core has `Api::V3::BaseController < ApplicationController` directly. There is no intermediate `Api::ApplicationController` or `Api::V3::ApplicationController`. Therefore, the `app/controllers/api/v1/application_controller.rb.tt` template was NOT created (the task said "if looper_core has the v3→v1 chain pattern").
|
|
679
|
+
|
|
680
|
+
### No Fidelity Specs
|
|
681
|
+
|
|
682
|
+
All T22 templates are adapted (not verbatim) from looper_core. The primary differences are: added `# frozen_string_literal: true` (looper_core files lack it), removed Cognito dependencies, simplified for a new Rails app. Since there are no byte-identical verbatim files, no fidelity specs were created.
|
|
683
|
+
|
|
684
|
+
### Install Generator Spec Update
|
|
685
|
+
|
|
686
|
+
The install generator spec (`install_generator_spec.rb`) had a test "warns when a generator is not yet available" that used `invoke_generator("api")` as the not-yet-available example. Since T22 creates the api generator, this test was updated to use `invoke_generator("auth")` instead (auth is still not implemented).
|
|
687
|
+
|
|
688
|
+
### Generator Methods
|
|
689
|
+
|
|
690
|
+
1. `add_api_gems` - adds blueprinter, rswag-api, rswag-ui (default group); rswag-specs (test group)
|
|
691
|
+
2. `create_base_controller` - templates `app/controllers/api/v1/base_controller.rb`
|
|
692
|
+
3. `create_blueprinter_initializer` - templates `config/initializers/blueprinter.rb`
|
|
693
|
+
4. `create_example_blueprint` - templates `app/blueprints/example_blueprint.rb`
|
|
694
|
+
5. `create_rswag_api_initializer` - templates `config/initializers/rswag_api.rb`
|
|
695
|
+
6. `create_rswag_ui_initializer` - templates `config/initializers/rswag_ui.rb`
|
|
696
|
+
7. `insert_rswag_routes` - mounts `Rswag::Ui::Engine` and `Rswag::Api::Engine` at `/api-docs` via `insert_route`
|
|
697
|
+
8. `create_example_request_spec` - templates `spec/requests/api/v1/examples_spec.rb`
|
|
698
|
+
|
|
699
|
+
### Spec Coverage
|
|
700
|
+
|
|
701
|
+
36 tests covering:
|
|
702
|
+
- destination_root binding
|
|
703
|
+
- 4 gem additions (blueprinter, rswag-api, rswag-ui, rswag-specs)
|
|
704
|
+
- Base controller: existence, frozen_string_literal, inheritance, Pagy::Backend, authenticate stub, render_json_error
|
|
705
|
+
- Blueprinter initializer: existence, LowerCamelTransformer, default_transformers, frozen_string_literal
|
|
706
|
+
- Example blueprint: existence, Blueprinter::Base inheritance
|
|
707
|
+
- RSwag initializers: rswag_api existence + openapi_root, rswag_ui existence + openapi_endpoint + v1 swagger
|
|
708
|
+
- Routes: rswag ui mount, rswag api mount
|
|
709
|
+
- Example spec: existence, frozen_string_literal, path DSL, response DSL
|
|
710
|
+
- Idempotency: gems, base controller, blueprinter initializer, example blueprint, rswag_api, rswag_ui, routes, example spec (8 tests)
|
|
711
|
+
|
|
712
|
+
### RuboCop Notes
|
|
713
|
+
|
|
714
|
+
- `Style/RedundantRegexpArgument` triggers on `routes.scan(/mount Rswag::Ui::Engine/)` when the argument contains no regex metacharacters. Use a plain string instead: `routes.scan("mount Rswag::Ui::Engine")`.
|
|
715
|
+
|
|
716
|
+
## Auth Generator (Task 23)
|
|
717
|
+
|
|
718
|
+
The auth generator scaffolds Rails 8's built-in authentication (equivalent to `bin/rails generate authentication`), adapted for the develoz-rails template pattern.
|
|
719
|
+
|
|
720
|
+
### Canonical Sources
|
|
721
|
+
|
|
722
|
+
- **Adapted files**: All templates are adapted from the Rails 8 authentication generator source (`rails/rails` repo, `railties/lib/rails/generators/rails/authentication/templates/`):
|
|
723
|
+
- `app/models/user.rb.tt` adapted from Rails 8's `user.rb.tt`. Key changes: uses `email` instead of `email_address`, adds explicit `validates :email` with `URI::MailTo::EMAIL_REGEXP` (Rails 8 relies on `normalizes` + DB uniqueness only). Kept `has_secure_password` and `normalizes :email`.
|
|
724
|
+
- `app/models/current.rb.tt` adapted from Rails 8's `current.rb.tt`. Key change: uses `attribute :user` directly instead of `attribute :session` with `delegate :user, to: :session` (no Session model in this simplified version).
|
|
725
|
+
- `app/controllers/concerns/authentication.rb.tt` adapted from Rails 8's `authentication.rb.tt`. Key changes: uses `Current.user` instead of `Current.session`, `cookies.signed[:user_id]` instead of `cookies.signed[:session_id]`, renamed `allow_unauthenticated_access` to `allow_authentication_as` (per task spec), removed `start_new_session_for`/`terminate_session` Session model logic (simplified to set `Current.user` directly).
|
|
726
|
+
- `app/controllers/sessions_controller.rb.tt` adapted from Rails 8's `sessions_controller.rb.tt`. Key change: uses `email` instead of `email_address` in `authenticate_by` params.
|
|
727
|
+
- `app/controllers/passwords_controller.rb.tt` adapted from Rails 8's `passwords_controller.rb.tt`. Key changes: uses `email` instead of `email_address`, removed conditional `<%- if defined?(ActionMailer::Railtie) -%>` ERB guards (always include mailer-dependent code since these are templates for a Rails app that will have ActionMailer).
|
|
728
|
+
- `app/mailers/passwords_mailer.rb.tt` adapted from Rails 8's `passwords_mailer.rb.tt`. Key change: `to: user.email` instead of `to: user.email_address`.
|
|
729
|
+
- `app/views/sessions/new.html.erb.tt` adapted from Rails 8 ERB template engine's `sessions/new.html.erb`. Key change: `:email` field instead of `:email_address`.
|
|
730
|
+
- `app/views/passwords/edit.html.erb.tt` adapted from Rails 8 ERB template engine's `passwords/edit.html.erb`. Verbatim except for `<%%` ERB escaping.
|
|
731
|
+
- `app/views/passwords_mailer/reset.html.erb.tt` adapted from Rails 8's `passwords_mailer/reset.html.erb.tt`. Verbatim except for `<%%` ERB escaping.
|
|
732
|
+
- **Authored files**: `db/migrate/create_users.rb.tt` (Rails 8 generates migrations via `rails g migration` at runtime, not as a template; we author a static migration template with `email` + `password_digest` columns and unique index). `spec/requests/sessions_spec.rb.tt` and `spec/requests/passwords_spec.rb.tt` (Rails 8 generates these via `hook_for :test_framework`; we author minimal request specs demonstrating sign in/out and password reset flows).
|
|
733
|
+
|
|
734
|
+
### Rails 8 Authentication Architecture (Latest vs Task Spec)
|
|
735
|
+
|
|
736
|
+
The Rails 8 authentication generator on `main` has evolved to use a `Session` model (with `ip_address` and `user_agent` columns) and `email_address` field. The task spec asks for the simpler earlier version: `Current.user` directly (no Session model), `email` field. The templates follow the task spec, not the latest `main` branch.
|
|
737
|
+
|
|
738
|
+
### Rails 8 Password Reset Token
|
|
739
|
+
|
|
740
|
+
Rails 8's `has_secure_password` includes `password_reset_token` and `find_by_password_reset_token!` methods (derived from the password digest via `ActiveSupport::MessageVerifier`). The passwords controller and mailer view use these methods. No additional database columns are needed for password reset tokens.
|
|
741
|
+
|
|
742
|
+
### ERB-in-ERB Escaping for View Templates
|
|
743
|
+
|
|
744
|
+
All `.html.erb.tt` view templates use `<%%` and `<%%=` to escape ERB tags so Thor's template engine outputs literal `<%` and `<%=` in the generated `.erb` files. This is the same pattern used by T19 (docs_render) and T36 (versioning).
|
|
745
|
+
|
|
746
|
+
### Install Generator Spec Update
|
|
747
|
+
|
|
748
|
+
The install generator spec had a test "warns when a generator is not yet available" that used `invoke_generator("auth")` as the not-yet-available example (set in T22 when api was created). Since T23 creates the auth generator, this test was updated to use `invoke_generator("pwa")` instead (pwa is still not implemented). This is the same pattern as T22's update from "api" to "auth".
|
|
749
|
+
|
|
750
|
+
### Generator Methods
|
|
751
|
+
|
|
752
|
+
1. `add_bcrypt_gem` - adds bcrypt gem via `add_gem`
|
|
753
|
+
2. `create_user_model` - templates `app/models/user.rb`
|
|
754
|
+
3. `create_current_model` - templates `app/models/current.rb`
|
|
755
|
+
4. `create_authentication_concern` - templates `app/controllers/concerns/authentication.rb`
|
|
756
|
+
5. `create_sessions_controller` - templates `app/controllers/sessions_controller.rb`
|
|
757
|
+
6. `create_passwords_controller` - templates `app/controllers/passwords_controller.rb`
|
|
758
|
+
7. `create_passwords_mailer` - templates `app/mailers/passwords_mailer.rb`
|
|
759
|
+
8. `create_sessions_views` - templates `app/views/sessions/new.html.erb`
|
|
760
|
+
9. `create_passwords_views` - templates `app/views/passwords/edit.html.erb`
|
|
761
|
+
10. `create_passwords_mailer_views` - templates `app/views/passwords_mailer/reset.html.erb`
|
|
762
|
+
11. `create_users_migration` - templates `db/migrate/create_users.rb`
|
|
763
|
+
12. `insert_auth_routes` - inserts `resource :session` and `resources :passwords, param: :token` routes via `insert_route`
|
|
764
|
+
13. `create_sessions_request_spec` - templates `spec/requests/sessions_spec.rb`
|
|
765
|
+
14. `create_passwords_request_spec` - templates `spec/requests/passwords_spec.rb`
|
|
766
|
+
|
|
767
|
+
### Spec Coverage
|
|
768
|
+
|
|
769
|
+
59 tests covering:
|
|
770
|
+
- destination_root binding
|
|
771
|
+
- bcrypt gem addition
|
|
772
|
+
- User model: existence, has_secure_password, normalizes email, validates email, frozen_string_literal
|
|
773
|
+
- Current model: existence, attribute :user, CurrentAttributes inheritance
|
|
774
|
+
- Authentication concern: existence, before_action, resume_session, request_authentication, after_authentication_url, allow_authentication_as, frozen_string_literal
|
|
775
|
+
- Sessions controller: existence, new/create/destroy actions, frozen_string_literal
|
|
776
|
+
- Passwords controller: existence, edit/update actions, frozen_string_literal
|
|
777
|
+
- Passwords mailer: existence, reset method, frozen_string_literal
|
|
778
|
+
- Sessions view: existence, form_with
|
|
779
|
+
- Passwords view: existence, form_with
|
|
780
|
+
- Mailer view: existence, edit_password_url
|
|
781
|
+
- Migration: existence, email column, password_digest column, unique index, frozen_string_literal
|
|
782
|
+
- Routes: session route, passwords route
|
|
783
|
+
- Request specs: sessions existence + frozen_string_literal + describe, passwords existence + frozen_string_literal + describe
|
|
784
|
+
- Idempotency: bcrypt gem, user model, current model, authentication concern, sessions controller, passwords controller, passwords mailer, sessions view, passwords view, mailer view, migration, routes, sessions spec, passwords spec (14 tests)
|
|
785
|
+
|
|
786
|
+
## UI Generator (Task 24)
|
|
787
|
+
|
|
788
|
+
The ui generator sets up the `develoz_ui` gem as a git submodule at `vendor/develoz-ui`, with env-conditional Gemfile entries, a `.gitmodules` section, a setup script, and importmap pins for Stimulus controllers.
|
|
789
|
+
|
|
790
|
+
### Key Decisions
|
|
791
|
+
|
|
792
|
+
1. **inject_once for Gemfile (not add_gem)**: The base class `add_gem` has an idempotency guard that checks for ANY `gem "develoz_ui"` line in the Gemfile. Since we need TWO declarations (one in `group :development, :test` with `path:`, one in `group :production` with `github:`), calling `add_gem` twice would skip the second call. Instead, use `inject_once` with a multi-line content block containing both group declarations. The content-inclusion guard ensures the entire block is only injected once.
|
|
793
|
+
|
|
794
|
+
2. **.gitmodules creation + injection**: The base class `inject_once` returns early if the target file doesn't exist (`return unless File.exist?(file_path)`). For `.gitmodules`, which may not exist yet, the generator first creates an empty file via `File.write` if it doesn't exist, then calls `inject_once` to append the submodule section. Both branches (file exists / doesn't exist) are tested for 100% coverage.
|
|
795
|
+
|
|
796
|
+
3. **Bash setup script (not Ruby)**: The `bin/setup_develoz_ui` script is a bash script (`#!/usr/bin/env bash`) rather than Ruby, since it runs `git submodule update --init --recursive`. The script includes error handling: if the submodule directory is empty or missing after the update, it prints an error message and exits 1. The `.tt` template is processed through Thor's ERB engine, but since the script contains no `<%` sequences, ERB processing is a no-op.
|
|
797
|
+
|
|
798
|
+
4. **Importmap pins**: Injects two pins into `config/importmap.rb` via `inject_once`: `pin "develoz-ui"` for the main entry point and `pin_all_from` for Stimulus controllers under `develoz-ui/controllers`. The spec seeds a minimal importmap.rb fixture with existing pins to verify both preservation of existing content and injection of new pins.
|
|
799
|
+
|
|
800
|
+
### Generator Methods
|
|
801
|
+
|
|
802
|
+
1. `add_develoz_ui_gems` - injects two group blocks into Gemfile via `inject_once` (dev/test with `path:`, production with `github:`)
|
|
803
|
+
2. `create_gitmodules` - creates `.gitmodules` if absent, then injects submodule section via `inject_once`
|
|
804
|
+
3. `create_setup_script` - templates `bin/setup_develoz_ui` and chmods to 0o755
|
|
805
|
+
4. `inject_importmap_pins` - injects develoz-ui Stimulus controller pins into `config/importmap.rb` via `inject_once`
|
|
806
|
+
|
|
807
|
+
### Spec Coverage
|
|
808
|
+
|
|
809
|
+
17 tests covering:
|
|
810
|
+
- destination_root binding
|
|
811
|
+
- Gemfile: develoz_ui path in dev/test group, github in production group, idempotency
|
|
812
|
+
- .gitmodules: creation when absent, submodule section content, append to existing, idempotency
|
|
813
|
+
- Setup script: existence, executable bit, git submodule command, error documentation, idempotency, executable bit preserved on re-run
|
|
814
|
+
- Importmap: pin injection, existing content preservation, idempotency
|
|
815
|
+
|
|
816
|
+
### RuboCop Notes
|
|
817
|
+
|
|
818
|
+
- Long string literals with `\n` and `\t` escapes can exceed the 120-char line limit. Use Ruby string continuation (`\` at end of line) to split across multiple lines: `"[submodule ...]\n" \` followed by `"\tpath = ...\n" \`.
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
## DB Backup Generator (Task 31)
|
|
822
|
+
|
|
823
|
+
The db_backup generator creates a `bin/db-backup` shell script (pg_dump + gzip + retention pruning), a `lib/tasks/backup.rake` rake fallback, and optionally injects a `db-backup` compose service when `--docker` is present.
|
|
824
|
+
|
|
825
|
+
### Key Decisions
|
|
826
|
+
|
|
827
|
+
1. **Spec path follows existing convention (`spec/develoz/generators/`)**: The task referenced `spec/generators/db_backup_generator_spec.rb`, but all 19 existing generator specs live at `spec/develoz/generators/`. Placing the spec there keeps consistency and ensures the `RSpec/SpecFilePathFormat` cop passes. The task path was treated as shorthand.
|
|
828
|
+
|
|
829
|
+
2. **Compose service via template + ERB rendering**: The `compose_service.tt` template contains `<%= app_name %>` ERB. Since `inject_once` takes a raw content string (not a Thor template render), the generator reads the template file and renders it with `ERB.new(raw, trim_mode: "-").result(binding)`. The generator's binding provides `app_name` from the base helper. This keeps the service definition in a template file (as required) while still using `inject_once` for idempotent injection.
|
|
830
|
+
|
|
831
|
+
3. **`class_option :docker` with default false**: The install generator invokes sub-generators with `klass.new([], {}, destination_root: ...)` (empty options), so `options[:docker]` defaults to false in the install flow. When run standalone with `--docker`, the compose service is injected. This satisfies "Do NOT require docker" - the rake fallback is always created; compose injection is opt-in.
|
|
832
|
+
|
|
833
|
+
4. **`inject_once` with marker guard for compose**: The `marker:` parameter ("# db-backup service (develoz:db_backup)") provides idempotency independent of the full content block. The `after: /^services:\n/` anchor inserts the service right after the `services:` line, preserving existing services.
|
|
834
|
+
|
|
835
|
+
5. **bin/db-backup is a shell script (excluded from coverage)**: The spec_helper filters `/bin/` and `.sh` files from coverage. The rake task IS Ruby and gets coverage. The rake task uses `Open3.popen3` + `Zlib::GzipWriter` for the Ruby fallback path.
|
|
836
|
+
|
|
837
|
+
### Generator Methods
|
|
838
|
+
|
|
839
|
+
1. `create_backup_script` - templates `bin/db-backup` and chmods to 0o755
|
|
840
|
+
2. `create_backup_rake` - templates `lib/tasks/backup.rake`
|
|
841
|
+
3. `inject_compose_service` - returns early unless `options[:docker]`; injects compose service snippet via `inject_once` with marker guard
|
|
842
|
+
4. `ensure_backups_gitignored` - adds `/backups/` to `.gitignore` via `ensure_gitignore`
|
|
843
|
+
|
|
844
|
+
### Spec Coverage
|
|
845
|
+
|
|
846
|
+
22 tests covering:
|
|
847
|
+
- destination_root binding
|
|
848
|
+
- bin/db-backup: existence, executable bit, pg_dump+gzip, .sql.gz, retention pruning, idempotency, executable bit preserved on re-run
|
|
849
|
+
- rake task: existence, backup namespace with create+prune tasks, frozen_string_literal, retention env var, idempotency
|
|
850
|
+
- gitignore: /backups/ added, idempotency
|
|
851
|
+
- compose service: NOT injected when docker=false, NOT injected when docker unset, injected when docker=true, 6h schedule (21600s), retention env, depends_on postgres, preserves existing services, idempotency
|
|
852
|
+
|
|
853
|
+
### RuboCop Notes
|
|
854
|
+
|
|
855
|
+
- `Layout/TrailingEmptyLines: Final newline missing` appeared on both generated files after initial write. Fixed via `rubocop -A` autocorrect. Always verify files end with exactly one newline.
|