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.
Files changed (172) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +31 -0
  3. data/.github/workflows/release.yml +459 -0
  4. data/.rubocop.yml +52 -0
  5. data/.sisyphus/notepads/develoz-rails-template/learnings.md +855 -0
  6. data/.tool-versions +2 -0
  7. data/CHANGELOG.md +28 -0
  8. data/CONTRIBUTING.md +120 -0
  9. data/README.md +76 -0
  10. data/Rakefile +18 -0
  11. data/config/generators.yml +105 -0
  12. data/docs/guide/installation.md +574 -0
  13. data/exe/develoz +6 -0
  14. data/install +98 -0
  15. data/lib/develoz/canonical_fetcher.rb +56 -0
  16. data/lib/develoz/cli.rb +84 -0
  17. data/lib/develoz/generators/base.rb +134 -0
  18. data/lib/develoz/manifest.rb +44 -0
  19. data/lib/develoz/options.rb +55 -0
  20. data/lib/develoz/version.rb +5 -0
  21. data/lib/develoz/version_resolver.rb +103 -0
  22. data/lib/develoz.rb +15 -0
  23. data/lib/generators/develoz/active_resource/active_resource_generator.rb +25 -0
  24. data/lib/generators/develoz/active_resource/templates/application_resource.rb.tt +7 -0
  25. data/lib/generators/develoz/active_resource/templates/example_resource.rb.tt +5 -0
  26. data/lib/generators/develoz/admin/admin_generator.rb +44 -0
  27. data/lib/generators/develoz/admin/templates/admin_base_controller.rb.tt +7 -0
  28. data/lib/generators/develoz/admin/templates/admin_layout.html.erb.tt +38 -0
  29. data/lib/generators/develoz/admin/templates/dashboard_controller.rb.tt +7 -0
  30. data/lib/generators/develoz/admin/templates/dashboard_index.html.erb.tt +2 -0
  31. data/lib/generators/develoz/admin/templates/routes.rb.tt +3 -0
  32. data/lib/generators/develoz/agents_docs/agents_docs_generator.rb +50 -0
  33. data/lib/generators/develoz/agents_docs/templates/.github/pull_request_template.md.tt +13 -0
  34. data/lib/generators/develoz/agents_docs/templates/AGENTS.md.tt +36 -0
  35. data/lib/generators/develoz/agents_docs/templates/docs/development.md.tt +121 -0
  36. data/lib/generators/develoz/agents_docs/templates/docs/performance.md.tt +46 -0
  37. data/lib/generators/develoz/agents_docs/templates/docs/testing.md.tt +92 -0
  38. data/lib/generators/develoz/agents_docs/templates/spec/cassettes/Example_API/fetches_data_from_an_external_API.yml.tt +23 -0
  39. data/lib/generators/develoz/agents_docs/templates/spec/requests/example_api_spec.rb.tt +14 -0
  40. data/lib/generators/develoz/agents_docs/templates/spec/support/faraday.rb.tt +19 -0
  41. data/lib/generators/develoz/agents_docs/templates/spec/support/vcr.rb.tt +11 -0
  42. data/lib/generators/develoz/api/api_generator.rb +55 -0
  43. data/lib/generators/develoz/api/templates/app/blueprints/example_blueprint.rb.tt +6 -0
  44. data/lib/generators/develoz/api/templates/app/controllers/api/v1/base_controller.rb.tt +96 -0
  45. data/lib/generators/develoz/api/templates/config/initializers/blueprinter.rb.tt +16 -0
  46. data/lib/generators/develoz/api/templates/config/initializers/rswag_api.rb.tt +15 -0
  47. data/lib/generators/develoz/api/templates/config/initializers/rswag_ui.rb.tt +17 -0
  48. data/lib/generators/develoz/api/templates/spec/requests/api/v1/examples_spec.rb.tt +25 -0
  49. data/lib/generators/develoz/auth/auth_generator.rb +82 -0
  50. data/lib/generators/develoz/auth/templates/app/controllers/concerns/authentication.rb.tt +53 -0
  51. data/lib/generators/develoz/auth/templates/app/controllers/passwords_controller.rb.tt +35 -0
  52. data/lib/generators/develoz/auth/templates/app/controllers/sessions_controller.rb.tt +22 -0
  53. data/lib/generators/develoz/auth/templates/app/mailers/passwords_mailer.rb.tt +8 -0
  54. data/lib/generators/develoz/auth/templates/app/models/current.rb.tt +5 -0
  55. data/lib/generators/develoz/auth/templates/app/models/user.rb.tt +7 -0
  56. data/lib/generators/develoz/auth/templates/app/views/passwords/edit.html.erb.tt +9 -0
  57. data/lib/generators/develoz/auth/templates/app/views/passwords_mailer/reset.html.erb.tt +6 -0
  58. data/lib/generators/develoz/auth/templates/app/views/sessions/new.html.erb.tt +11 -0
  59. data/lib/generators/develoz/auth/templates/db/migrate/create_users.rb.tt +13 -0
  60. data/lib/generators/develoz/auth/templates/spec/requests/passwords_spec.rb.tt +60 -0
  61. data/lib/generators/develoz/auth/templates/spec/requests/sessions_spec.rb.tt +37 -0
  62. data/lib/generators/develoz/ci/ci_generator.rb +66 -0
  63. data/lib/generators/develoz/ci/templates/.github/workflows/ci.yml.tt +18 -0
  64. data/lib/generators/develoz/ci/templates/.haml-lint.yml.tt +12 -0
  65. data/lib/generators/develoz/ci/templates/.markdownlint.json.tt +10 -0
  66. data/lib/generators/develoz/ci/templates/.reek.yml.tt +28 -0
  67. data/lib/generators/develoz/ci/templates/.rubocop.yml.tt +22 -0
  68. data/lib/generators/develoz/ci/templates/.stylelintrc.json.tt +4 -0
  69. data/lib/generators/develoz/ci/templates/.yamllint.tt +14 -0
  70. data/lib/generators/develoz/ci/templates/Rakefile.tt +73 -0
  71. data/lib/generators/develoz/ci/templates/bin/ci.tt +6 -0
  72. data/lib/generators/develoz/ci/templates/biome.json.tt +41 -0
  73. data/lib/generators/develoz/ci/templates/config/ci.rb.tt +23 -0
  74. data/lib/generators/develoz/concerns/concerns_generator.rb +48 -0
  75. data/lib/generators/develoz/concerns/templates/app/models/concerns/configurable.rb.tt +120 -0
  76. data/lib/generators/develoz/concerns/templates/app/models/concerns/optimized_finders.rb.tt +93 -0
  77. data/lib/generators/develoz/concerns/templates/app/models/concerns/searchable_concern.rb.tt +31 -0
  78. data/lib/generators/develoz/concerns/templates/app/models/concerns/transitionable.rb.tt +144 -0
  79. data/lib/generators/develoz/concerns/templates/app/models/configuration.rb.tt +104 -0
  80. data/lib/generators/develoz/concerns/templates/db/migrate/add_status_transitions.rb.tt +30 -0
  81. data/lib/generators/develoz/concerns/templates/db/migrate/create_configurations.rb.tt +25 -0
  82. data/lib/generators/develoz/concerns/templates/spec/models/concerns/configurable_spec.rb.tt +172 -0
  83. data/lib/generators/develoz/concerns/templates/spec/models/concerns/optimized_finders_spec.rb.tt +200 -0
  84. data/lib/generators/develoz/concerns/templates/spec/models/concerns/searchable_concern_spec.rb.tt +84 -0
  85. data/lib/generators/develoz/concerns/templates/spec/models/concerns/transitionable_spec.rb.tt +234 -0
  86. data/lib/generators/develoz/concerns/templates/spec/models/configuration_spec.rb.tt +223 -0
  87. data/lib/generators/develoz/database/database_generator.rb +44 -0
  88. data/lib/generators/develoz/database/templates/config/database.yml.tt +53 -0
  89. data/lib/generators/develoz/database/templates/config/initializers/pg_search.rb.tt +5 -0
  90. data/lib/generators/develoz/database/templates/db/migrate/create_pg_extensions.rb.tt +13 -0
  91. data/lib/generators/develoz/db_backup/db_backup_generator.rb +47 -0
  92. data/lib/generators/develoz/db_backup/templates/backup_rake.tt +48 -0
  93. data/lib/generators/develoz/db_backup/templates/bin_db_backup.tt +25 -0
  94. data/lib/generators/develoz/db_backup/templates/compose_service.tt +14 -0
  95. data/lib/generators/develoz/doc_specs/doc_specs_generator.rb +33 -0
  96. data/lib/generators/develoz/doc_specs/templates/bin/generate-docs.tt +392 -0
  97. data/lib/generators/develoz/doc_specs/templates/lib/tasks/docs_check.rake.tt +8 -0
  98. data/lib/generators/develoz/doc_specs/templates/spec/support/doc_screenshot_helper.rb.tt +17 -0
  99. data/lib/generators/develoz/doc_specs/templates/spec/system/example_doc_spec.rb.tt +38 -0
  100. data/lib/generators/develoz/docker/docker_generator.rb +54 -0
  101. data/lib/generators/develoz/docker/templates/bin_dev.tt +5 -0
  102. data/lib/generators/develoz/docker/templates/bin_docker_entrypoint.tt +8 -0
  103. data/lib/generators/develoz/docker/templates/bin_run.tt +152 -0
  104. data/lib/generators/develoz/docker/templates/bin_setup.tt +15 -0
  105. data/lib/generators/develoz/docker/templates/docker-compose.yml.tt +59 -0
  106. data/lib/generators/develoz/docker/templates/dockerfile_dev.tt +24 -0
  107. data/lib/generators/develoz/docs_render/docs_render_generator.rb +46 -0
  108. data/lib/generators/develoz/docs_render/templates/app/assets/stylesheets/documentation.scss.tt +145 -0
  109. data/lib/generators/develoz/docs_render/templates/app/controllers/docs_controller.rb.tt +7 -0
  110. data/lib/generators/develoz/docs_render/templates/app/javascript/docs.js.tt +15 -0
  111. data/lib/generators/develoz/docs_render/templates/app/models/document.rb.tt +189 -0
  112. data/lib/generators/develoz/docs_render/templates/app/views/docs/show.html.erb.tt +7 -0
  113. data/lib/generators/develoz/docs_render/templates/config/initializers/redcarpet_rouge.rb.tt +16 -0
  114. data/lib/generators/develoz/frontend_core/frontend_core_generator.rb +40 -0
  115. data/lib/generators/develoz/frontend_core/templates/.annotaterb.yml.tt +16 -0
  116. data/lib/generators/develoz/frontend_core/templates/config/importmap.rb.tt +12 -0
  117. data/lib/generators/develoz/frontend_core/templates/config/initializers/pagy.rb.tt +10 -0
  118. data/lib/generators/develoz/install/install_generator.rb +51 -0
  119. data/lib/generators/develoz/kamal/kamal_generator.rb +59 -0
  120. data/lib/generators/develoz/kamal/templates/accessories_postgres.yml.tt +14 -0
  121. data/lib/generators/develoz/kamal/templates/deploy.yml.tt +43 -0
  122. data/lib/generators/develoz/kamal/templates/dockerfile_prod.tt +54 -0
  123. data/lib/generators/develoz/kamal/templates/kamal_secrets.tt +22 -0
  124. data/lib/generators/develoz/maintenance/maintenance_generator.rb +29 -0
  125. data/lib/generators/develoz/maintenance/templates/app/tasks/maintenance/example_task.rb.tt +20 -0
  126. data/lib/generators/develoz/maintenance/templates/lib/tasks/maintenance_counters.rake.tt +27 -0
  127. data/lib/generators/develoz/push/push_generator.rb +61 -0
  128. data/lib/generators/develoz/push/templates/app/javascript/pwa/subscription.js.tt +57 -0
  129. data/lib/generators/develoz/push/templates/app/models/push_subscription.rb.tt +14 -0
  130. data/lib/generators/develoz/push/templates/app/services/push_notification_service.rb.tt +36 -0
  131. data/lib/generators/develoz/push/templates/app/views/pwa/sw_push_handlers.js.tt +49 -0
  132. data/lib/generators/develoz/push/templates/constants_env.tt +5 -0
  133. data/lib/generators/develoz/push/templates/db/migrate/create_push_subscriptions.rb.tt +15 -0
  134. data/lib/generators/develoz/pwa/pwa_generator.rb +46 -0
  135. data/lib/generators/develoz/pwa/templates/app/controllers/pwa_controller.rb.tt +17 -0
  136. data/lib/generators/develoz/pwa/templates/app/javascript/pwa/registration.js.tt +25 -0
  137. data/lib/generators/develoz/pwa/templates/app/views/pwa/manifest.json.erb.tt +24 -0
  138. data/lib/generators/develoz/pwa/templates/app/views/pwa/offline.html.erb.tt +51 -0
  139. data/lib/generators/develoz/pwa/templates/app/views/pwa/service-worker.js.tt +109 -0
  140. data/lib/generators/develoz/pwa/templates/routes.rb.tt +3 -0
  141. data/lib/generators/develoz/solid/solid_generator.rb +52 -0
  142. data/lib/generators/develoz/solid/templates/app/jobs/application_job.rb.tt +7 -0
  143. data/lib/generators/develoz/solid/templates/config/cable.yml.tt +10 -0
  144. data/lib/generators/develoz/solid/templates/config/cache.yml.tt +16 -0
  145. data/lib/generators/develoz/solid/templates/config/initializers/mission_control.rb.tt +8 -0
  146. data/lib/generators/develoz/solid/templates/config/initializers/solid.rb.tt +7 -0
  147. data/lib/generators/develoz/solid/templates/config/queue.yml.tt +18 -0
  148. data/lib/generators/develoz/solid/templates/config/recurring.yml.tt +20 -0
  149. data/lib/generators/develoz/strict_loading/strict_loading_generator.rb +17 -0
  150. data/lib/generators/develoz/strict_loading/templates/config/initializers/strict_loading.rb.tt +13 -0
  151. data/lib/generators/develoz/testing/templates/rspec.tt +2 -0
  152. data/lib/generators/develoz/testing/templates/rspec_parallel.tt +2 -0
  153. data/lib/generators/develoz/testing/templates/spec/rails_helper.rb.tt +32 -0
  154. data/lib/generators/develoz/testing/templates/spec/spec_helper.rb.tt +31 -0
  155. data/lib/generators/develoz/testing/testing_generator.rb +39 -0
  156. data/lib/generators/develoz/tooling/templates/constants.rb.tt +17 -0
  157. data/lib/generators/develoz/tooling/templates/env.example.tt +4 -0
  158. data/lib/generators/develoz/tooling/templates/vscode/extensions.json.tt +8 -0
  159. data/lib/generators/develoz/tooling/templates/vscode/settings.json.tt +26 -0
  160. data/lib/generators/develoz/tooling/templates/vscode/tasks.json.tt +26 -0
  161. data/lib/generators/develoz/tooling/tooling_generator.rb +33 -0
  162. data/lib/generators/develoz/ui/templates/bin/setup_develoz_ui.tt +20 -0
  163. data/lib/generators/develoz/ui/ui_generator.rb +57 -0
  164. data/lib/generators/develoz/versioning/templates/app/helpers/application_helper.rb.tt +7 -0
  165. data/lib/generators/develoz/versioning/templates/app/views/shared/_app_version.html.erb.tt +3 -0
  166. data/lib/generators/develoz/versioning/templates/spec/helpers/application_helper_spec.rb.tt +11 -0
  167. data/lib/generators/develoz/versioning/versioning_generator.rb +48 -0
  168. data/lib/tasks/canonical.rake +27 -0
  169. data/templates/.keep +0 -0
  170. data/templates/CANONICAL_SOURCES.md +93 -0
  171. data/templates/pull_request_template.md.tt +13 -0
  172. 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "vcr"
4
+ require "webmock"
5
+
6
+ VCR.configure do |config|
7
+ config.cassette_library_dir = "spec/cassettes"
8
+ config.hook_into :webmock
9
+ config.configure_rspec_metadata!
10
+ config.ignore_localhost = true
11
+ 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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ExampleBlueprint < Blueprinter::Base
4
+ identifier :id
5
+ fields :name, :created_at, :updated_at
6
+ 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