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,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "develoz"
|
|
4
|
+
|
|
5
|
+
module Develoz
|
|
6
|
+
module Generators
|
|
7
|
+
class AgentsDocsGenerator < Develoz::Generators::Base
|
|
8
|
+
def self.source_root
|
|
9
|
+
File.expand_path("templates", __dir__)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def add_api_gems
|
|
13
|
+
add_gem "faraday"
|
|
14
|
+
add_gem "faraday-retry"
|
|
15
|
+
add_gem "vcr", group: %i[test]
|
|
16
|
+
add_gem "webmock", group: %i[test]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create_agents_md
|
|
20
|
+
template "AGENTS.md.tt", "AGENTS.md"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create_docs
|
|
24
|
+
template "docs/development.md.tt", "docs/development.md"
|
|
25
|
+
template "docs/testing.md.tt", "docs/testing.md"
|
|
26
|
+
template "docs/performance.md.tt", "docs/performance.md"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create_pr_template
|
|
30
|
+
template ".github/pull_request_template.md.tt",
|
|
31
|
+
".github/pull_request_template.md"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def create_vcr_support
|
|
35
|
+
template "spec/support/vcr.rb.tt", "spec/support/vcr.rb"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def create_faraday_support
|
|
39
|
+
template "spec/support/faraday.rb.tt", "spec/support/faraday.rb"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def create_example_api_spec
|
|
43
|
+
template "spec/requests/example_api_spec.rb.tt",
|
|
44
|
+
"spec/requests/example_api_spec.rb"
|
|
45
|
+
template "spec/cassettes/Example_API/fetches_data_from_an_external_API.yml.tt",
|
|
46
|
+
"spec/cassettes/Example_API/fetches_data_from_an_external_API.yml"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
> [!NOTE]
|
|
2
|
+
> **Changes:** <!-- Describe what has been changed in this PR and, most importantly, why it's changed. Use 3-5 bullet points. -->
|
|
3
|
+
>
|
|
4
|
+
|
|
5
|
+
> [!TIP]
|
|
6
|
+
> **How to review:** <!-- Describe what the reviewer of this PR should be looking for and what results are expected. -->
|
|
7
|
+
>
|
|
8
|
+
|
|
9
|
+
> [!WARNING]
|
|
10
|
+
> **Risks:** <!-- Is there any risk attached to merging this PR? Select the appropriate box. -->
|
|
11
|
+
> - [x] none
|
|
12
|
+
> - [ ] low
|
|
13
|
+
> - [ ] high
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This file gives high-level guidance for coding agents working in this repository.
|
|
4
|
+
|
|
5
|
+
## Development Environment
|
|
6
|
+
|
|
7
|
+
- Development is Docker-first and uses `docker compose`.
|
|
8
|
+
- Run project commands through `bin/rails` or `bundle exec` as appropriate.
|
|
9
|
+
- For one-off Ruby or Rails runner scripts, prefer checked-in temp files under `tmp/` plus `bin/rails runner tmp/script.rb`.
|
|
10
|
+
- Avoid inline `bin/rails runner "..."` snippets when possible; shell quoting can be fragile.
|
|
11
|
+
- Discover available commands with `bin/rails --help` and `bin/rails runner --help`.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle exec rspec # run specs (single process)
|
|
15
|
+
bundle exec rake spec:parallel # run specs in parallel (faster, use for full suite)
|
|
16
|
+
bundle exec rubocop
|
|
17
|
+
bin/rails db:migrate
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Documentation-First Policy
|
|
21
|
+
|
|
22
|
+
To avoid filling agent context, keep this file brief and reference detailed docs.
|
|
23
|
+
|
|
24
|
+
- [docs/development.md](docs/development.md)
|
|
25
|
+
- [docs/testing.md](docs/testing.md)
|
|
26
|
+
- [docs/performance.md](docs/performance.md)
|
|
27
|
+
|
|
28
|
+
## Quality Expectations
|
|
29
|
+
|
|
30
|
+
- Add or update specs for every behavior change.
|
|
31
|
+
- 100% line and 100% branch coverage is required. Review `coverage/index.html` after running specs.
|
|
32
|
+
- In tests, stub real git commands and filesystem side effects instead of creating real repos or mutating files on disk, unless a true integration path explicitly requires it.
|
|
33
|
+
- Tests must never spawn real external CLI processes. Global stubs in `spec/support/` block these by default — see [docs/testing.md](docs/testing.md) for details.
|
|
34
|
+
- Run relevant checks before finishing work.
|
|
35
|
+
- Keep changes small, explicit, and aligned with existing patterns.
|
|
36
|
+
- Update the relevant document in [docs/](docs/) when behavior changes.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Development Guide
|
|
2
|
+
|
|
3
|
+
This guide covers day-to-day local development.
|
|
4
|
+
|
|
5
|
+
## Core Rules
|
|
6
|
+
|
|
7
|
+
- Use Docker-first workflows (`docker compose`).
|
|
8
|
+
- Run project commands through `bin/rails` or `bundle exec`.
|
|
9
|
+
|
|
10
|
+
## Boot Local Environment
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
cp .env.example .env
|
|
14
|
+
docker compose up --build -d app
|
|
15
|
+
bin/rails db:migrate
|
|
16
|
+
bin/rails db:seed
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Useful lifecycle commands:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
docker compose logs -f app
|
|
23
|
+
docker compose down
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Command Reference
|
|
27
|
+
|
|
28
|
+
### Tests
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bundle exec rspec
|
|
32
|
+
bundle exec rspec spec/system/
|
|
33
|
+
bundle exec rspec spec/requests/
|
|
34
|
+
bundle exec rspec spec/models/
|
|
35
|
+
bundle exec rspec spec/system/tickets_spec.rb:25
|
|
36
|
+
bundle exec rspec --only-failures
|
|
37
|
+
bundle exec rspec --next-failure
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Lint and Security
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bundle exec rubocop
|
|
44
|
+
bundle exec rubocop -A
|
|
45
|
+
bundle exec brakeman
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Rails and DB
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bin/rails db:migrate
|
|
52
|
+
bin/rails db:seed
|
|
53
|
+
bin/rails console
|
|
54
|
+
bin/rails console --sandbox
|
|
55
|
+
bin/rails runner --help
|
|
56
|
+
RAILS_ENV=test bin/rails console
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Runner Scripts and One-Offs
|
|
60
|
+
|
|
61
|
+
- Prefer checked-in scripts under `tmp/` for one-off investigation, data fixes, and runner tasks.
|
|
62
|
+
- If you need Rails loaded, either require the environment from a Ruby script or pass a script file to Rails runner.
|
|
63
|
+
|
|
64
|
+
Recommended patterns:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Plain Ruby with the Rails environment loaded manually
|
|
68
|
+
ruby tmp/my_script.rb
|
|
69
|
+
|
|
70
|
+
# Rails runner with a script file
|
|
71
|
+
bin/rails runner tmp/my_runner.rb
|
|
72
|
+
|
|
73
|
+
# Discover runner usage and available flags
|
|
74
|
+
bin/rails runner --help
|
|
75
|
+
bin/rails --help
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Example `tmp/my_script.rb`:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
require './config/environment'
|
|
82
|
+
|
|
83
|
+
record = Record.find_by(name: 'example')
|
|
84
|
+
pp(record&.attributes&.slice('name', 'status'))
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Use inline runner snippets only when the command is trivial and shell quoting is not a risk.
|
|
88
|
+
|
|
89
|
+
### Frontend Assets
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
bin/rails tailwindcss:build
|
|
93
|
+
bin/rails tailwindcss:watch
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Recommended Change Workflow
|
|
97
|
+
|
|
98
|
+
1. Add or update tests first.
|
|
99
|
+
2. Implement in small, reviewable steps.
|
|
100
|
+
3. Run targeted tests while iterating.
|
|
101
|
+
4. Run full checks before finishing:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
bundle exec rspec
|
|
105
|
+
bundle exec rubocop
|
|
106
|
+
bundle exec brakeman
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
5. Update the matching document in this folder when behavior changes.
|
|
110
|
+
|
|
111
|
+
## Troubleshooting
|
|
112
|
+
|
|
113
|
+
- Docker unavailable: start Docker and rerun commands.
|
|
114
|
+
- Migration issues: run `bin/rails db:migrate:status`.
|
|
115
|
+
- App startup issues: check `docker compose logs -f app postgres redis`.
|
|
116
|
+
- Stale test schema/data: rerun `bin/rails db:migrate` and `bundle exec rspec`.
|
|
117
|
+
|
|
118
|
+
## Related Docs
|
|
119
|
+
|
|
120
|
+
- [docs/testing.md](testing.md)
|
|
121
|
+
- [docs/performance.md](performance.md)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Performance Guide
|
|
2
|
+
|
|
3
|
+
This document summarizes current query-performance practices and optimizations.
|
|
4
|
+
|
|
5
|
+
## N+1 Detection in Development
|
|
6
|
+
|
|
7
|
+
- `config/environments/development.rb` enables strict-loading N+1 checks.
|
|
8
|
+
- `verbose_query_logs = true` helps trace query origin quickly.
|
|
9
|
+
- Goal: fail in development when lazy-loading patterns introduce N+1 behavior.
|
|
10
|
+
|
|
11
|
+
## Query Patterns to Prefer
|
|
12
|
+
|
|
13
|
+
- Use `includes` when iterating associated records.
|
|
14
|
+
- Use DB predicates (`where`) instead of Ruby filtering.
|
|
15
|
+
- Use `pluck` when only IDs or specific columns are needed.
|
|
16
|
+
- Use `find_each` for large batch processing.
|
|
17
|
+
- Use `select` to limit columns on read-heavy paths.
|
|
18
|
+
|
|
19
|
+
## Index Strategy
|
|
20
|
+
|
|
21
|
+
- Add indexes for columns used in `where`, `order`, and `join` clauses.
|
|
22
|
+
- Use partial indexes for boolean conditions that filter most rows.
|
|
23
|
+
- Use composite indexes for multi-column query patterns.
|
|
24
|
+
- Review `pg_stat_user_indexes` to identify unused indexes.
|
|
25
|
+
|
|
26
|
+
## Verification Queries
|
|
27
|
+
|
|
28
|
+
```sql
|
|
29
|
+
SELECT schemaname, relname, indexrelname, idx_scan, idx_tup_read
|
|
30
|
+
FROM pg_stat_user_indexes
|
|
31
|
+
ORDER BY idx_scan DESC;
|
|
32
|
+
|
|
33
|
+
EXPLAIN ANALYZE SELECT * FROM records WHERE status = 'active';
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Caching
|
|
37
|
+
|
|
38
|
+
- Use `Rails.cache` for expensive computations and external API metadata.
|
|
39
|
+
- Scope cache keys per resource to prevent cross-project data leaks.
|
|
40
|
+
- Set TTLs short enough to avoid stale data and long enough to keep UI fast.
|
|
41
|
+
- Invalidate caches when underlying data changes.
|
|
42
|
+
|
|
43
|
+
## Related Docs
|
|
44
|
+
|
|
45
|
+
- [docs/development.md](development.md)
|
|
46
|
+
- [docs/testing.md](testing.md)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Testing Guide
|
|
2
|
+
|
|
3
|
+
This guide documents how to run and write tests.
|
|
4
|
+
|
|
5
|
+
## Test Stack
|
|
6
|
+
|
|
7
|
+
- RSpec
|
|
8
|
+
- Capybara (system specs)
|
|
9
|
+
- FactoryBot and Faker
|
|
10
|
+
- SimpleCov (line and branch thresholds)
|
|
11
|
+
- WebMock and VCR (`spec/cassettes/`)
|
|
12
|
+
|
|
13
|
+
## Run Tests
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle exec rake spec:parallel # full suite in parallel (fastest)
|
|
17
|
+
bundle exec rspec # full suite single process
|
|
18
|
+
|
|
19
|
+
bundle exec rspec spec/system/
|
|
20
|
+
bundle exec rspec spec/requests/
|
|
21
|
+
bundle exec rspec spec/models/
|
|
22
|
+
bundle exec rspec spec/services/
|
|
23
|
+
|
|
24
|
+
bundle exec rspec spec/system/tickets_spec.rb
|
|
25
|
+
bundle exec rspec spec/system/tickets_spec.rb:25
|
|
26
|
+
|
|
27
|
+
bundle exec rspec --only-failures
|
|
28
|
+
bundle exec rspec --next-failure
|
|
29
|
+
bundle exec rspec --format documentation
|
|
30
|
+
bundle exec rspec --profile 10
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Coverage Requirement (Mandatory)
|
|
34
|
+
|
|
35
|
+
100% line and 100% branch coverage is required. This is enforced by SimpleCov in `spec/spec_helper.rb`.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bundle exec rspec
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then review `coverage/index.html` for any uncovered lines or branches.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
xdg-open coverage/index.html # Linux
|
|
45
|
+
open coverage/index.html # macOS
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## What to Test
|
|
49
|
+
|
|
50
|
+
- `spec/system/` for user-facing workflows
|
|
51
|
+
- `spec/requests/` for endpoint behavior
|
|
52
|
+
- `spec/models/` for model behavior and validations
|
|
53
|
+
- `spec/services/` and `spec/lib/` for workflow/domain logic
|
|
54
|
+
|
|
55
|
+
Prefer request and system specs over controller specs.
|
|
56
|
+
|
|
57
|
+
## Testing Conventions
|
|
58
|
+
|
|
59
|
+
- Test behavior, not internal implementation.
|
|
60
|
+
- Use descriptive example names.
|
|
61
|
+
- Use FactoryBot (`create`, `build`) for test data.
|
|
62
|
+
- Use `aggregate_failures` for grouped UI assertions when useful.
|
|
63
|
+
- Stub external systems only when necessary.
|
|
64
|
+
- Stub real git commands and filesystem side effects in tests. Do not create real git repos, shell out to git, or write temporary fixture files just to exercise parsing or state handling when a stubbed command/service response covers the behavior.
|
|
65
|
+
- If a test truly needs filesystem or git integration, keep it explicit about why the integration boundary matters.
|
|
66
|
+
|
|
67
|
+
## External API Tests
|
|
68
|
+
|
|
69
|
+
- Use `:vcr` for external calls.
|
|
70
|
+
- Keep cassettes in `spec/cassettes/`.
|
|
71
|
+
- Avoid storing secrets in cassette data.
|
|
72
|
+
- Refresh stale cassettes when upstream API behavior changes.
|
|
73
|
+
|
|
74
|
+
## Validation Before PR
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
bundle exec rspec
|
|
78
|
+
bundle exec rubocop
|
|
79
|
+
bundle exec brakeman
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Troubleshooting
|
|
83
|
+
|
|
84
|
+
- Migration mismatch: `bin/rails db:migrate`.
|
|
85
|
+
- Coverage below threshold: inspect `coverage/index.html` and add missing specs.
|
|
86
|
+
- JS/system issues: verify Selenium is up with `docker compose ps`.
|
|
87
|
+
- VCR mismatch: refresh cassette and rerun the failing spec.
|
|
88
|
+
|
|
89
|
+
## Related Docs
|
|
90
|
+
|
|
91
|
+
- [docs/development.md](development.md)
|
|
92
|
+
- [docs/performance.md](performance.md)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
http_interactions:
|
|
3
|
+
- request:
|
|
4
|
+
method: get
|
|
5
|
+
uri: https://api.example.com/data
|
|
6
|
+
body:
|
|
7
|
+
encoding: US-ASCII
|
|
8
|
+
string: ""
|
|
9
|
+
headers:
|
|
10
|
+
User-Agent:
|
|
11
|
+
- Faraday
|
|
12
|
+
response:
|
|
13
|
+
status:
|
|
14
|
+
code: 200
|
|
15
|
+
message: OK
|
|
16
|
+
headers:
|
|
17
|
+
Content-Type:
|
|
18
|
+
- application/json
|
|
19
|
+
body:
|
|
20
|
+
encoding: UTF-8
|
|
21
|
+
string: '{"status":"ok"}'
|
|
22
|
+
recorded_at: Mon, 01 Jan 2024 00:00:00 GMT
|
|
23
|
+
recorded_with: VCR
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails_helper"
|
|
4
|
+
require "support/faraday"
|
|
5
|
+
|
|
6
|
+
RSpec.describe "Example API", :vcr do
|
|
7
|
+
it "fetches data from an external API" do
|
|
8
|
+
# VCR cassette: spec/cassettes/Example_API/fetches_data_from_an_external_API.yml
|
|
9
|
+
# This example demonstrates the VCR + Faraday pattern.
|
|
10
|
+
# Record a real cassette by running this spec once with VCR record mode enabled.
|
|
11
|
+
response = FaradayFactory.build(url: "https://api.example.com").get("/data")
|
|
12
|
+
expect(response.status).to eq(200)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/retry"
|
|
5
|
+
|
|
6
|
+
module FaradayFactory
|
|
7
|
+
def self.build(url: nil)
|
|
8
|
+
Faraday.new(url: url) do |conn|
|
|
9
|
+
conn.request :retry, {
|
|
10
|
+
max: 3,
|
|
11
|
+
interval: 0.5,
|
|
12
|
+
backoff_factor: 2
|
|
13
|
+
}
|
|
14
|
+
conn.request :json
|
|
15
|
+
conn.response :json
|
|
16
|
+
conn.adapter Faraday.default_adapter
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "develoz"
|
|
4
|
+
|
|
5
|
+
module Develoz
|
|
6
|
+
module Generators
|
|
7
|
+
class ApiGenerator < Develoz::Generators::Base
|
|
8
|
+
def self.source_root
|
|
9
|
+
File.expand_path("templates", __dir__)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def add_api_gems
|
|
13
|
+
add_gem "blueprinter"
|
|
14
|
+
add_gem "rswag-api"
|
|
15
|
+
add_gem "rswag-ui"
|
|
16
|
+
add_gem "rswag-specs", group: %i[test]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create_base_controller
|
|
20
|
+
template "app/controllers/api/v1/base_controller.rb.tt",
|
|
21
|
+
"app/controllers/api/v1/base_controller.rb"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create_blueprinter_initializer
|
|
25
|
+
template "config/initializers/blueprinter.rb.tt",
|
|
26
|
+
"config/initializers/blueprinter.rb"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create_example_blueprint
|
|
30
|
+
template "app/blueprints/example_blueprint.rb.tt",
|
|
31
|
+
"app/blueprints/example_blueprint.rb"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def create_rswag_api_initializer
|
|
35
|
+
template "config/initializers/rswag_api.rb.tt",
|
|
36
|
+
"config/initializers/rswag_api.rb"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create_rswag_ui_initializer
|
|
40
|
+
template "config/initializers/rswag_ui.rb.tt",
|
|
41
|
+
"config/initializers/rswag_ui.rb"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def insert_rswag_routes
|
|
45
|
+
insert_route "mount Rswag::Ui::Engine => '/api-docs'"
|
|
46
|
+
insert_route "mount Rswag::Api::Engine => '/api-docs'"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def create_example_request_spec
|
|
50
|
+
template "spec/requests/api/v1/examples_spec.rb.tt",
|
|
51
|
+
"spec/requests/api/v1/examples_spec.rb"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Api
|
|
4
|
+
module V1
|
|
5
|
+
class BaseController < ApplicationController
|
|
6
|
+
include Pagy::Backend
|
|
7
|
+
|
|
8
|
+
skip_forgery_protection
|
|
9
|
+
before_action :authenticate
|
|
10
|
+
|
|
11
|
+
rescue_from ActiveRecord::RecordNotFound do |e|
|
|
12
|
+
render_json_error(:not_found, e.message)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
rescue_from ActiveRecord::RecordInvalid do |e|
|
|
16
|
+
render_json_error(:unprocessable_content, e.message)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.default_page_size(size = nil)
|
|
20
|
+
@default_page_size = size if size
|
|
21
|
+
@default_page_size || 20
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
delegate :default_page_size, to: :class
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# Override in subclasses to implement authentication
|
|
29
|
+
# (e.g. Cognito, Devise, API token). The default stub allows
|
|
30
|
+
# all requests through so the API is usable out of the box.
|
|
31
|
+
def authenticate; end
|
|
32
|
+
|
|
33
|
+
def render_json_error(status, message, extra = {})
|
|
34
|
+
status = Rack::Utils::SYMBOL_TO_STATUS_CODE[status] if status.is_a?(Symbol)
|
|
35
|
+
|
|
36
|
+
error = { detail: message, status: }.merge(extra)
|
|
37
|
+
|
|
38
|
+
render json: { error: }, status:
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def render_resource(resource, options = {})
|
|
42
|
+
if (pagination_options = Array(options[:paginate])).present?
|
|
43
|
+
resource = resource.limit(limit) if pagination_options.include?(:force_limit) && common_params[:page].blank?
|
|
44
|
+
pager = :arel if pagination_options.include?(:arel) || common_params[:countless] == "false"
|
|
45
|
+
pager ||= :countless
|
|
46
|
+
resource = paginated(resource, force: pagination_options.include?(:force), pager:)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
options = { meta:, view: }.merge(options).compact
|
|
50
|
+
|
|
51
|
+
render json: render_json(resource, options)
|
|
52
|
+
end
|
|
53
|
+
alias render_resources render_resource
|
|
54
|
+
|
|
55
|
+
def render_json(resource, options = {})
|
|
56
|
+
return resource.serialize_as_json(options) if resource.respond_to?(:serialize_as_json)
|
|
57
|
+
|
|
58
|
+
return options[:blueprint].render(resource, options) if options[:blueprint].present?
|
|
59
|
+
|
|
60
|
+
Rails.logger.warn "Couldn't find blueprint for #{resource}, falling back to ruby JSON serializer."
|
|
61
|
+
resource.to_json
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def view
|
|
65
|
+
common_params[:view]&.to_sym
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def paginated(resources, force: false, pager: :arel)
|
|
69
|
+
return resources unless common_params[:page].present? || force
|
|
70
|
+
|
|
71
|
+
@pagy, resources = send(["pagy", pager].join("_"), resources, limit:, countless_minimal: true)
|
|
72
|
+
resources
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def limit
|
|
76
|
+
common_params[:limit].presence || default_page_size
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def meta
|
|
80
|
+
meta = common_params.to_h.compact
|
|
81
|
+
|
|
82
|
+
if @pagy
|
|
83
|
+
meta[:recordCount] = @pagy.count
|
|
84
|
+
meta[:pageCount] = @pagy.pages
|
|
85
|
+
meta[:countless] = @pagy.is_a?(Pagy::Countless)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
meta
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def common_params
|
|
92
|
+
params.permit(:filter, :page, :limit, :sort, :view, :countless)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LowerCamelTransformer < Blueprinter::Transformer
|
|
4
|
+
def transform_key(key)
|
|
5
|
+
key.to_s.camelize(:lower)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def transform(hash, _object = nil, _options = nil)
|
|
9
|
+
hash.deep_transform_keys! { |key| transform_key(key).to_sym }
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
Blueprinter.configure do |config|
|
|
14
|
+
config.default_transformers = [LowerCamelTransformer]
|
|
15
|
+
config.datetime_format = ->(datetime) { datetime&.to_fs(:api) }
|
|
16
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Rswag::Api.configure do |c|
|
|
4
|
+
# Specify a root folder where Swagger JSON files are located
|
|
5
|
+
# This is used by the Swagger middleware to serve requests for API descriptions
|
|
6
|
+
# NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure
|
|
7
|
+
# that it's configured to generate files in the same folder
|
|
8
|
+
c.openapi_root = Rails.root.join('swagger')
|
|
9
|
+
|
|
10
|
+
# Inject a lambda function to alter the returned Swagger prior to serialization
|
|
11
|
+
# The function will have access to the rack env for the current request
|
|
12
|
+
# For example, you could leverage this to dynamically assign the "host" property
|
|
13
|
+
#
|
|
14
|
+
# c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] }
|
|
15
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Rswag::Ui.configure do |c|
|
|
4
|
+
# List the Swagger endpoints that you want to be documented through the
|
|
5
|
+
# swagger-ui. The first parameter is the path (absolute or relative to the UI
|
|
6
|
+
# host) to the corresponding endpoint and the second is a title that will be
|
|
7
|
+
# displayed in the document selector.
|
|
8
|
+
# NOTE: If you're using rspec-api to expose Swagger files
|
|
9
|
+
# (under openapi_root) as JSON or YAML endpoints, then the list below should
|
|
10
|
+
# correspond to the relative paths for those endpoints.
|
|
11
|
+
|
|
12
|
+
c.openapi_endpoint '/api-docs/v1/swagger.yaml', 'API V1'
|
|
13
|
+
|
|
14
|
+
# Add Basic Auth in case your API is private
|
|
15
|
+
# c.basic_auth_enabled = true
|
|
16
|
+
# c.basic_auth_credentials 'username', 'password'
|
|
17
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'swagger_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'Examples', type: :request do
|
|
6
|
+
path '/api/v1/examples' do
|
|
7
|
+
get 'Lists examples' do
|
|
8
|
+
tags 'Examples'
|
|
9
|
+
produces 'application/json'
|
|
10
|
+
description 'Returns a paginated list of examples'
|
|
11
|
+
|
|
12
|
+
response '200', 'successfully' do
|
|
13
|
+
schema type: :object,
|
|
14
|
+
properties: {
|
|
15
|
+
examples: { type: :array, items: { type: :object } },
|
|
16
|
+
meta: { type: :object }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
run_test! do
|
|
20
|
+
expect(response).to have_http_status(:ok)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|