From 9249e0991d2b3d062e41bee1bcde0730acde581c Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Sun, 7 Dec 2025 23:32:07 +0800 Subject: [PATCH 01/18] Add CLAUDE.local.md to gitignore Local planning file for refactoring discussions that should not be tracked. Co-Authored-By: Claude --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8ecbd72..8630f9b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .claude/ CLAUDE.md +CLAUDE.local.md _snippets/ .bundle/ config/secrets.yml From 6dd6a1bd4dd5ae68cfd8f6d25e3f7c13459d65cc Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Sun, 21 Dec 2025 12:37:32 +0800 Subject: [PATCH 02/18] refactor: separate worker output from test output in acceptance tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Worker output now redirected to spec/logs/worker.log (gitignored) to provide cleaner test output. A summary of worker activity is shown after tests complete. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .gitignore | 3 ++- spec/acceptance_tests | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 8630f9b..30e6906 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ coverage/* !coverage/.resultset.json *.db repostore/**/ -_cache/ \ No newline at end of file +_cache/ +spec/logs/ \ No newline at end of file diff --git a/spec/acceptance_tests b/spec/acceptance_tests index 5c77fc1..a574b07 100755 --- a/spec/acceptance_tests +++ b/spec/acceptance_tests @@ -2,9 +2,22 @@ # see: https://stackoverflow.com/questions/360201/how-do-i-kill-background-processes-jobs-when-my-shell-script-exits trap "kill 0" EXIT -# Run application server as backround process (using '&') +# Create logs directory if needed (gitignored via spec/logs/) +mkdir -p spec/logs + +# Run worker as background process, redirecting output to log file # see: https://kb.iu.edu/d/afnz -rake worker:run:test & +echo "Starting worker (output in spec/logs/worker.log)..." +rake worker:run:test > spec/logs/worker.log 2>&1 & + +# Give worker time to start +sleep 2 -# Run acceptance tests on browser +# Run acceptance tests +echo "Running tests..." bundle exec rake spec + +# Show worker log summary +echo "" +echo "=== Worker Log Summary ===" +grep -E "(INFO:|ERROR:|Progress:.*100)" spec/logs/worker.log 2>/dev/null | tail -20 || echo "No worker log found" From 8a689b89759b14bc97a079875ddd4c9d0af1e284 Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Sun, 21 Dec 2025 12:38:05 +0800 Subject: [PATCH 03/18] refactor: add Value::Appraisal domain object for worker-based caching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces Value::Appraisal as an immutable Dry::Struct representing the result of appraising a project folder. Supports both success and error states with factory methods. - Attributes: status, project, folder_path, folder, error_type, error_message - Methods: success?, error?, cache_key, ttl - TTL constants: SUCCESS_TTL (1 day), ERROR_TTL (10 seconds) Part of Phase 1 of the worker-based appraisal refactoring. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/domain/contributions/values/appraisal.rb | 78 ++++++ coverage/.resultset.json | 274 ++++++++++++------- 2 files changed, 256 insertions(+), 96 deletions(-) create mode 100644 app/domain/contributions/values/appraisal.rb diff --git a/app/domain/contributions/values/appraisal.rb b/app/domain/contributions/values/appraisal.rb new file mode 100644 index 0000000..ebf729d --- /dev/null +++ b/app/domain/contributions/values/appraisal.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'dry-types' +require 'dry-struct' + +# Require dependencies that load after this file alphabetically +require_relative '../../projects/entities/project' +require_relative '../entities/folder_contributions' + +module CodePraise + module Value + # Value object representing the result of appraising a project folder + # Immutable snapshot of appraisal outcome (success or error) + class Appraisal < Dry::Struct + include Dry.Types + + # Cache TTL constants + SUCCESS_TTL = 86_400 # 1 day in seconds + ERROR_TTL = 10 # 10 seconds + + # Status must be :ok or :error + attribute :status, Strict::Symbol.enum(:ok, :error) + + # Project is always required (needed for cache key) + attribute :project, Instance(Entity::Project) + + # Folder path being appraised (empty string for root) + attribute :folder_path, Strict::String + + # Folder contributions (present on success, nil on error) + attribute :folder, Instance(Entity::FolderContributions).optional + + # Error details (present on error, nil on success) + attribute :error_type, Strict::String.optional + attribute :error_message, Strict::String.optional + + def success? + status == :ok + end + + def error? + status == :error + end + + def cache_key + "appraisal:#{project.fullname}/#{folder_path}" + end + + def ttl + success? ? SUCCESS_TTL : ERROR_TTL + end + + # Factory method for successful appraisal + def self.success(project:, folder_path:, folder:) + new( + status: :ok, + project:, + folder_path:, + folder:, + error_type: nil, + error_message: nil + ) + end + + # Factory method for failed appraisal + def self.error(project:, folder_path:, error_type:, error_message:) + new( + status: :error, + project:, + folder_path:, + folder: nil, + error_type:, + error_message: + ) + end + end + end +end diff --git a/coverage/.resultset.json b/coverage/.resultset.json index 1d6d286..2e019e4 100644 --- a/coverage/.resultset.json +++ b/coverage/.resultset.json @@ -16,7 +16,7 @@ 1, null, 1, - 60, + 61, null, null ] @@ -525,6 +525,157 @@ null ] }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/values/appraisal.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + null, + null, + 1, + null, + null, + 1, + null, + null, + 1, + null, + null, + 1, + 1, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/projects/entities/project.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 7, + null, + null, + 1, + 9, + null, + null, + 1, + null, + 15, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/projects/entities/member.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + 1, + 1, + null, + 1, + 57, + null, + null, + null, + null + ] + }, "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/values/contributors.rb": { "lines": [ 1, @@ -732,75 +883,6 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/projects/entities/member.rb": { - "lines": [ - null, - null, - 1, - 1, - null, - 1, - 1, - null, - 1, - 1, - null, - 1, - 1, - 1, - 1, - null, - 1, - 57, - null, - null, - null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/projects/entities/project.rb": { - "lines": [ - null, - null, - 1, - 1, - null, - 1, - null, - 1, - 1, - null, - 1, - 1, - null, - 1, - null, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - null, - 1, - 7, - null, - null, - 1, - 10, - null, - null, - 1, - null, - 15, - null, - null, - null, - null - ] - }, "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/cache/local_cache.rb": { "lines": [ null, @@ -1086,16 +1168,16 @@ 1, null, 1, - 194, - 194, - 194, - 194, + 193, + 193, + 193, + 193, null, null, 1, - 3, - 3, - 3, + 2, + 2, + 2, null, null, 1, @@ -1110,21 +1192,21 @@ null, null, 1, - 3, - 3, + 2, + 2, null, null, 1, - 3, - 3, + 2, + 2, null, null, 1, - 388, + 386, null, null, 1, - 194, + 193, null, null, null, @@ -1135,8 +1217,8 @@ null, null, 1, - 2, - 10, + 1, + 5, null, null, null, @@ -1504,14 +1586,14 @@ null, null, 1, - 19, + 18, null, null, 1, - 2, - 2, + 1, + 1, null, - 12, + 6, null, null, null @@ -1543,8 +1625,8 @@ null, null, 1, - 12, - 2, + 6, + 1, null, null, 1, @@ -1563,7 +1645,7 @@ null, null, 1, - 37, + 36, null, null, 1, @@ -1610,7 +1692,7 @@ null, null, 1, - 2, + 1, null, null, null, @@ -2770,8 +2852,8 @@ null, 1, 30, - 2005, - 2005, + 1955, + 1955, null, null, 30, @@ -3446,6 +3528,6 @@ ] } }, - "timestamp": 1764941503 + "timestamp": 1766291660 } } From 7d5466504f032be233bd91194ab22a423356de38 Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Sun, 21 Dec 2025 14:26:15 +0800 Subject: [PATCH 04/18] refactor: add Representer::Appraisal for JSON serialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add presentation layer representer for Value::Appraisal: - Serializes status symbol as string for JSON output - Conditionally includes folder contributions on success - Conditionally includes error_type and message on error - Reuses existing Project and FolderContributions representers Part of Phase 1: Domain + Infrastructure Foundation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../representers/appraisal_representer.rb | 36 ++ coverage/.resultset.json | 440 ++++++++++-------- 2 files changed, 276 insertions(+), 200 deletions(-) create mode 100644 app/presentation/representers/appraisal_representer.rb diff --git a/app/presentation/representers/appraisal_representer.rb b/app/presentation/representers/appraisal_representer.rb new file mode 100644 index 0000000..03ff0e4 --- /dev/null +++ b/app/presentation/representers/appraisal_representer.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'ostruct' +require 'roar/decorator' +require 'roar/json' + +require_relative 'project_representer' +require_relative 'folder_contributions_representer' + +module CodePraise + module Representer + # Represents appraisal result with status wrapper + # Success: { "status": "ok", "project": {...}, "folder": {...} } + # Error: { "status": "error", "project": {...}, "error_type": "...", "message": "..." } + class Appraisal < Roar::Decorator + include Roar::JSON + + property :status, exec_context: :decorator + property :project, extend: Representer::Project, class: OpenStruct + property :folder_path + + # Success case: include folder contributions + property :folder, extend: Representer::FolderContributions, class: OpenStruct, + if: ->(represented:, **) { represented.success? } + + # Error case: include error details + property :error_type, if: ->(represented:, **) { represented.error? } + property :error_message, as: :message, if: ->(represented:, **) { represented.error? } + + # Convert symbol status to string for JSON + def status + represented.status.to_s + end + end + end +end diff --git a/coverage/.resultset.json b/coverage/.resultset.json index 2e019e4..78307dc 100644 --- a/coverage/.resultset.json +++ b/coverage/.resultset.json @@ -16,7 +16,7 @@ 1, null, 1, - 61, + 62, null, null ] @@ -128,7 +128,7 @@ null, null, 1, - 7555, + 5158, null, null, null, @@ -136,7 +136,7 @@ null, null, 1, - 6940, + 4746, null, null, null, @@ -159,29 +159,29 @@ 1, null, 1, - 190, - 190, + 129, + 129, null, null, 1, - 711, + 276, null, null, 1, - 199, + 12, null, null, 1, - 711, + 276, null, - 711, + 276, null, - 5360, + 3651, null, null, null, 1, - 66, + 5, null, null, 1, @@ -202,7 +202,7 @@ null, 1, 1, - 199, + 12, null, null, 1, @@ -246,7 +246,7 @@ null, null, 1, - 6496, + 4267, null, null, 1, @@ -261,7 +261,7 @@ null, null, 1, - 5360, + 3651, null, null, 1, @@ -269,7 +269,7 @@ null, null, 1, - 11870, + 4122, null, null, null, @@ -397,20 +397,20 @@ 1, null, 1, - 48, + 33, null, - 48, - 48, - 48, - 48, + 33, + 33, + 33, + 33, null, null, 1, - 17, + 2, null, null, 1, - 17, + 2, null, null, 1, @@ -418,51 +418,51 @@ null, null, 1, - 17, + 2, null, null, 1, - 17, + 2, null, null, 1, - 58, + 28, null, null, 1, - 17, + 2, null, null, 1, null, 1, - 584, + 393, null, null, 1, - 48, + 33, null, null, 1, - 48, - 387, - 190, + 33, + 261, + 129, null, null, 1, - 48, + 33, null, - 197, - 197, + 132, + 132, null, null, null, 1, - 48, - 43, + 33, + 29, null, null, - 91, + 62, null, null, null, @@ -488,14 +488,14 @@ 1, null, 1, - 5360, - 5360, - 5360, - 5360, + 3651, + 3651, + 3651, + 3651, null, null, 1, - 11870, + 4122, null, null, null, @@ -511,14 +511,14 @@ null, 1, 1, - 139, + 95, null, null, null, null, 1, 1, - 1845, + 1266, null, null, null, @@ -685,7 +685,7 @@ 1, null, 1, - 356, + 245, null, null, null, @@ -693,15 +693,15 @@ null, 1, null, - 356, - 356, + 245, + 245, null, - 356, + 245, null, - 1091, - 1091, + 751, + 751, null, - 1091, + 751, null, 0, 0, @@ -710,20 +710,20 @@ 0, 0, null, - 1091, + 751, null, - 86, - 86, - 86, + 58, + 58, + 58, null, null, - 1005, - 1005, - 1005, + 693, + 693, + 693, null, null, null, - 356, + 245, null, null, null, @@ -744,8 +744,8 @@ 1, null, 1, - 546, - 546, + 374, + 374, null, null, null, @@ -772,29 +772,29 @@ null, null, 1, - 199, + 12, null, null, 1, - 6365, - 6365, - 6365, - 6365, + 4344, + 4344, + 4344, + 4344, null, null, 1, - 356, + 245, null, null, null, - 356, - 446, + 245, + 315, null, null, - 356, - 1005, - 2096, - 1005, + 245, + 693, + 1444, + 693, null, null, null, @@ -810,7 +810,7 @@ 1, null, 1, - 13305, + 9074, null, null, null, @@ -844,38 +844,38 @@ 1, null, 1, - 760, - 760, + 562, + 562, null, null, 1, - 12887, + 8450, null, null, 1, - 6451, + 4231, null, - 6436, + 4219, null, null, 1, - 380, + 304, null, null, 1, null, - 197, + 132, null, - 197, - 197, + 132, + 132, null, null, 1, null, 1, - 760, - 760, - 760, + 562, + 562, + 562, null, null, null, @@ -1168,10 +1168,10 @@ 1, null, 1, - 193, - 193, - 193, - 193, + 132, + 132, + 132, + 132, null, null, 1, @@ -1181,14 +1181,14 @@ null, null, 1, - 191, - 191, - 191, + 130, + 130, + 130, null, null, 1, - 191, - 191, + 130, + 130, null, null, 1, @@ -1202,18 +1202,18 @@ null, null, 1, - 386, + 264, null, null, 1, - 193, + 132, null, null, null, null, null, 1, - 190, + 129, null, null, 1, @@ -1235,12 +1235,12 @@ null, 1, 1, - 5360, - 5360, + 3651, + 3651, null, null, 1, - 5360, + 3651, null, null, null, @@ -1250,7 +1250,7 @@ 1, null, 1, - 5360, + 3651, null, null, null, @@ -1266,31 +1266,31 @@ null, 1, 1, - 6, + 5, null, null, 1, - 6, - null, 5, null, + 4, + null, null, null, null, null, 1, 1, - 5, - 190, + 4, + 129, null, null, null, null, 1, 1, - 190, - 190, - 190, + 129, + 129, + 129, null, null, null, @@ -1309,11 +1309,11 @@ null, 1, 1, - 190, + 129, null, null, 1, - 190, + 129, null, null, null, @@ -1322,20 +1322,20 @@ 1, null, 1, - 380, + 258, null, null, 1, - 5360, + 3651, null, null, 1, - 190, + 129, null, null, 1, - 190, - 5360, + 129, + 3651, null, null, null, @@ -1343,9 +1343,9 @@ null, 1, 1, - 5360, - 5360, - 5360, + 3651, + 3651, + 3651, null, null, 1, @@ -1355,14 +1355,14 @@ 1, null, 1, - 5360, + 3651, null, null, null, 1, null, 1, - 5360, + 3651, null, null, null, @@ -1383,20 +1383,20 @@ null, null, 1, - 5, - 5, + 4, + 4, null, null, 1, - 5, + 4, null, null, null, null, null, 1, - 5, - 190, + 4, + 129, null, null, null, @@ -1416,13 +1416,13 @@ 1, null, 1, - 190, - 5360, + 129, + 3651, null, null, 1, - 190, - 190, + 129, + 129, null, 0, 0, @@ -1430,28 +1430,28 @@ null, null, 1, - 5360, + 3651, null, - 5360, + 3651, null, null, null, - 5360, - 56896, - 56896, + 3651, + 38756, + 38756, null, null, - 5360, + 3651, null, null, 1, - 5360, - 5360, - 5360, + 3651, + 3651, + 3651, null, null, 1, - 56896, + 38756, null, null, null, @@ -1473,17 +1473,17 @@ 1, null, 1, - 6, - 6, - 6, + 5, + 5, + 5, null, null, 1, - 6, + 5, null, - 310, + 248, null, - 10, + 8, null, null, 1, @@ -1500,13 +1500,13 @@ null, null, 1, - 190, + 129, null, null, 1, null, 1, - 6, + 5, null, 6, null, @@ -1522,10 +1522,10 @@ null, null, 1, - 5, - 190, - 190, - 190, + 4, + 129, + 129, + 129, null, null, null, @@ -1544,11 +1544,11 @@ 1, null, 1, - 190, + 129, null, null, 1, - 190, + 129, null, null, null, @@ -1578,7 +1578,7 @@ null, null, 1, - 6, + 5, null, null, 1, @@ -1586,7 +1586,7 @@ null, null, 1, - 18, + 17, null, null, 1, @@ -1630,22 +1630,22 @@ null, null, 1, - 5, + 4, null, - 5, - 5, - 460, + 4, + 4, + 368, null, null, null, null, 1, - 13, - 26, + 11, + 22, null, null, 1, - 36, + 32, null, null, 1, @@ -1659,7 +1659,7 @@ 1, null, 1, - 18, + 15, null, null, null, @@ -1713,11 +1713,11 @@ 1, 1, 11, - 16, + 14, null, null, 1, - 16, + 14, null, null, null, @@ -1956,13 +1956,13 @@ 1, null, 1, - 3, - 3, + 4, + 4, null, null, null, null, - 3, + 4, null, null, null, @@ -1970,7 +1970,7 @@ null, null, 1, - 3, + 4, null, null, null, @@ -1988,7 +1988,7 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/clone_request_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/appraisal_representer.rb": { "lines": [ null, null, @@ -1996,15 +1996,33 @@ 1, 1, null, + 1, + 1, null, 1, 1, null, + null, + null, 1, 1, null, 1, 1, + 1, + null, + null, + 1, + 0, + null, + null, + 1, + 1, + null, + null, + 1, + 0, + null, null, null, null @@ -2080,6 +2098,39 @@ null ] }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/folder_contributions_representer.rb": { + "lines": [ + null, + null, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + null, + null + ] + }, "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/contributor_representer.rb": { "lines": [ null, @@ -2202,7 +2253,7 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/folder_contributions_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/clone_request_representer.rb": { "lines": [ null, null, @@ -2210,10 +2261,6 @@ 1, 1, null, - 1, - 1, - 1, - 1, null, 1, 1, @@ -2223,13 +2270,6 @@ null, 1, 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, null, null, null @@ -2457,14 +2497,14 @@ null, null, 8, - 6, - 6, + 7, + 7, null, null, - 2, - 2, + 1, + 1, null, - 2, + 1, null, null, null, @@ -2608,7 +2648,7 @@ 1, null, 1, - 3, + 2, null, null, 1, @@ -2738,10 +2778,10 @@ 1, 7, null, - 3, + 4, null, null, - 3, + 4, null, null, null, @@ -2751,11 +2791,11 @@ null, null, 1, - 4, + 3, null, null, - 3, - 3, + 2, + 2, null, null, 1, @@ -2774,8 +2814,8 @@ null, null, 1, - 3, - 3, + 4, + 4, null, null, null, @@ -2852,8 +2892,8 @@ null, 1, 30, - 1955, - 1955, + 1730, + 1730, null, null, 30, @@ -3528,6 +3568,6 @@ ] } }, - "timestamp": 1766291660 + "timestamp": 1766298341 } } From ab41b13c120203bae9b42512e811bee65b7ba8df Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Sun, 21 Dec 2025 14:37:52 +0800 Subject: [PATCH 05/18] refactor: add Redis cache methods and Phase 1 unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cache::Remote: - Add get(key), set(key, value, ttl:), exists?(key) methods - Uses setex for atomic set with expiration Test infrastructure: - Add fakeredis gem for isolated Redis testing in unit tests - Add CacheHelper for creating fake cache instances - Add unit tests for Value::Appraisal and Representer::Appraisal - Add unit tests for Cache::Remote new methods All tests passing (68 runs, 191 assertions, 96% coverage) Part of Phase 1: Domain + Infrastructure Foundation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- Gemfile | 3 + Gemfile.lock | 3 + app/infrastructure/cache/redis_cache.rb | 24 +- coverage/.resultset.json | 757 +++++++++++++++++------- spec/helpers/cache_helper.rb | 19 + spec/tests/unit/appraisal_spec.rb | 194 ++++++ spec/tests/unit/remote_cache_spec.rb | 72 +++ 7 files changed, 852 insertions(+), 220 deletions(-) create mode 100644 spec/helpers/cache_helper.rb create mode 100644 spec/tests/unit/appraisal_spec.rb create mode 100644 spec/tests/unit/remote_cache_spec.rb diff --git a/Gemfile b/Gemfile index bc60558..5317c36 100644 --- a/Gemfile +++ b/Gemfile @@ -71,6 +71,9 @@ group :test do gem 'simplecov', '~> 0.0' gem 'vcr', '~> 6.0' gem 'webmock', '~> 3.0' + + # Redis mocking for isolated tests + gem 'fakeredis', '~> 0.9' end # Development diff --git a/Gemfile.lock b/Gemfile.lock index 55494e4..aced5c8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -94,6 +94,8 @@ GEM base64 eventmachine (>= 1.0.0.beta.4) eventmachine (1.2.7) + fakeredis (0.9.2) + redis (~> 4.8) faye (1.4.1) cookiejar (>= 0.3.0) em-http-request (>= 1.1.6) @@ -276,6 +278,7 @@ DEPENDENCIES dry-transaction (~> 0) dry-types (~> 1.0) dry-validation (~> 1.0) + fakeredis (~> 0.9) faye (~> 1.0) figaro (~> 1.0) flog diff --git a/app/infrastructure/cache/redis_cache.rb b/app/infrastructure/cache/redis_cache.rb index 2ba5cef..9099075 100644 --- a/app/infrastructure/cache/redis_cache.rb +++ b/app/infrastructure/cache/redis_cache.rb @@ -4,12 +4,34 @@ module CodePraise module Cache - # Redis client utility + # Redis client utility for caching with TTL support class Remote def initialize(config) @redis = Redis.new(url: config.REDISCLOUD_URL) end + # Store a value with expiration + # @param key [String] cache key + # @param value [String] value to store (caller handles serialization) + # @param ttl [Integer] time-to-live in seconds + def set(key, value, ttl:) + @redis.setex(key, ttl, value) + end + + # Retrieve a cached value + # @param key [String] cache key + # @return [String, nil] cached value or nil if not found/expired + def get(key) + @redis.get(key) + end + + # Check if a key exists + # @param key [String] cache key + # @return [Boolean] true if key exists + def exists?(key) + @redis.exists?(key) + end + def keys @redis.keys end diff --git a/coverage/.resultset.json b/coverage/.resultset.json index 78307dc..f5535ba 100644 --- a/coverage/.resultset.json +++ b/coverage/.resultset.json @@ -128,7 +128,7 @@ null, null, 1, - 5158, + 7555, null, null, null, @@ -136,7 +136,7 @@ null, null, 1, - 4746, + 6940, null, null, null, @@ -159,29 +159,29 @@ 1, null, 1, - 129, - 129, + 190, + 190, null, null, 1, - 276, + 711, null, null, 1, - 12, + 199, null, null, 1, - 276, + 711, null, - 276, + 711, null, - 3651, + 5360, null, null, null, 1, - 5, + 66, null, null, 1, @@ -202,7 +202,7 @@ null, 1, 1, - 12, + 199, null, null, 1, @@ -246,7 +246,7 @@ null, null, 1, - 4267, + 6496, null, null, 1, @@ -261,7 +261,7 @@ null, null, 1, - 3651, + 5360, null, null, 1, @@ -269,7 +269,7 @@ null, null, 1, - 4122, + 11870, null, null, null, @@ -397,20 +397,20 @@ 1, null, 1, - 33, + 55, null, - 33, - 33, - 33, - 33, + 55, + 55, + 55, + 55, null, null, 1, - 2, + 17, null, null, 1, - 2, + 17, null, null, 1, @@ -418,51 +418,51 @@ null, null, 1, - 2, + 17, null, null, 1, - 2, + 17, null, null, 1, - 28, + 58, null, null, 1, - 2, + 17, null, null, 1, null, 1, - 393, + 584, null, null, 1, - 33, + 55, null, null, 1, - 33, - 261, - 129, + 55, + 387, + 190, null, null, 1, - 33, + 55, null, - 132, - 132, + 197, + 197, null, null, null, 1, - 33, - 29, + 55, + 43, null, null, - 62, + 98, null, null, null, @@ -488,14 +488,14 @@ 1, null, 1, - 3651, - 3651, - 3651, - 3651, + 5360, + 5360, + 5360, + 5360, null, null, 1, - 4122, + 11870, null, null, null, @@ -511,14 +511,14 @@ null, 1, 1, - 95, + 153, null, null, null, null, 1, 1, - 1266, + 1845, null, null, null, @@ -564,24 +564,24 @@ 1, null, 1, - 0, + 12, null, null, 1, - 0, + 14, null, null, 1, - 0, + 2, null, null, 1, - 0, + 4, null, null, null, 1, - 0, + 7, null, null, null, @@ -593,7 +593,7 @@ null, null, 1, - 0, + 14, null, null, null, @@ -634,11 +634,11 @@ 1, null, 1, - 7, + 15, null, null, 1, - 9, + 10, null, null, 1, @@ -685,7 +685,7 @@ 1, null, 1, - 245, + 356, null, null, null, @@ -693,15 +693,15 @@ null, 1, null, - 245, - 245, + 356, + 356, null, - 245, + 356, null, - 751, - 751, + 1091, + 1091, null, - 751, + 1091, null, 0, 0, @@ -710,20 +710,20 @@ 0, 0, null, - 751, + 1091, null, - 58, - 58, - 58, + 86, + 86, + 86, null, null, - 693, - 693, - 693, + 1005, + 1005, + 1005, null, null, null, - 245, + 356, null, null, null, @@ -744,8 +744,8 @@ 1, null, 1, - 374, - 374, + 546, + 546, null, null, null, @@ -772,29 +772,29 @@ null, null, 1, - 12, + 199, null, null, 1, - 4344, - 4344, - 4344, - 4344, + 6365, + 6365, + 6365, + 6365, null, null, 1, - 245, + 356, null, null, null, - 245, - 315, + 356, + 446, null, null, - 245, - 693, - 1444, - 693, + 356, + 1005, + 2096, + 1005, null, null, null, @@ -810,7 +810,7 @@ 1, null, 1, - 9074, + 13305, null, null, null, @@ -844,38 +844,38 @@ 1, null, 1, - 562, - 562, + 760, + 760, null, null, 1, - 8450, + 12887, null, null, 1, - 4231, + 6451, null, - 4219, + 6436, null, null, 1, - 304, + 380, null, null, 1, null, - 132, + 197, null, - 132, - 132, + 197, + 197, null, null, 1, null, 1, - 562, - 562, - 562, + 760, + 760, + 760, null, null, null, @@ -927,15 +927,37 @@ null, 1, 1, - 0, + 7, + null, + null, + null, + null, null, null, 1, - 0, + 7, + null, + null, + null, null, null, 1, - 0, + 3, + null, + null, + null, + null, + null, + 1, + 3, + null, + null, + 1, + 17, + null, + null, + 1, + 22, null, null, null, @@ -1168,57 +1190,57 @@ 1, null, 1, - 132, - 132, - 132, - 132, + 194, + 194, + 194, + 194, null, null, 1, - 2, - 2, - 2, + 3, + 3, + 3, null, null, 1, - 130, - 130, - 130, + 191, + 191, + 191, null, null, 1, - 130, - 130, + 191, + 191, null, null, 1, - 2, - 2, + 3, + 3, null, null, 1, - 2, - 2, + 3, + 3, null, null, 1, - 264, + 388, null, null, 1, - 132, + 194, null, null, null, null, null, 1, - 129, + 190, null, null, 1, - 1, - 5, + 2, + 10, null, null, null, @@ -1235,12 +1257,12 @@ null, 1, 1, - 3651, - 3651, + 5360, + 5360, null, null, 1, - 3651, + 5360, null, null, null, @@ -1250,7 +1272,7 @@ 1, null, 1, - 3651, + 5360, null, null, null, @@ -1266,13 +1288,13 @@ null, 1, 1, - 5, + 6, null, null, 1, - 5, + 6, null, - 4, + 5, null, null, null, @@ -1280,17 +1302,17 @@ null, 1, 1, - 4, - 129, + 5, + 190, null, null, null, null, 1, 1, - 129, - 129, - 129, + 190, + 190, + 190, null, null, null, @@ -1309,11 +1331,11 @@ null, 1, 1, - 129, + 190, null, null, 1, - 129, + 190, null, null, null, @@ -1322,20 +1344,20 @@ 1, null, 1, - 258, + 380, null, null, 1, - 3651, + 5360, null, null, 1, - 129, + 190, null, null, 1, - 129, - 3651, + 190, + 5360, null, null, null, @@ -1343,9 +1365,9 @@ null, 1, 1, - 3651, - 3651, - 3651, + 5360, + 5360, + 5360, null, null, 1, @@ -1355,14 +1377,14 @@ 1, null, 1, - 3651, + 5360, null, null, null, 1, null, 1, - 3651, + 5360, null, null, null, @@ -1383,20 +1405,20 @@ null, null, 1, - 4, - 4, + 5, + 5, null, null, 1, - 4, + 5, null, null, null, null, null, 1, - 4, - 129, + 5, + 190, null, null, null, @@ -1416,13 +1438,13 @@ 1, null, 1, - 129, - 3651, + 190, + 5360, null, null, 1, - 129, - 129, + 190, + 190, null, 0, 0, @@ -1430,28 +1452,28 @@ null, null, 1, - 3651, + 5360, null, - 3651, + 5360, null, null, null, - 3651, - 38756, - 38756, + 5360, + 56896, + 56896, null, null, - 3651, + 5360, null, null, 1, - 3651, - 3651, - 3651, + 5360, + 5360, + 5360, null, null, 1, - 38756, + 56896, null, null, null, @@ -1473,17 +1495,17 @@ 1, null, 1, - 5, - 5, - 5, + 6, + 6, + 6, null, null, 1, - 5, + 6, null, - 248, + 310, null, - 8, + 10, null, null, 1, @@ -1500,13 +1522,13 @@ null, null, 1, - 129, + 190, null, null, 1, null, 1, - 5, + 6, null, 6, null, @@ -1522,10 +1544,10 @@ null, null, 1, - 4, - 129, - 129, - 129, + 5, + 190, + 190, + 190, null, null, null, @@ -1544,11 +1566,11 @@ 1, null, 1, - 129, + 190, null, null, 1, - 129, + 190, null, null, null, @@ -1578,7 +1600,7 @@ null, null, 1, - 5, + 6, null, null, 1, @@ -1586,14 +1608,14 @@ null, null, 1, - 17, + 19, null, null, 1, - 1, - 1, + 2, + 2, null, - 6, + 12, null, null, null @@ -1625,27 +1647,27 @@ null, null, 1, - 6, - 1, + 12, + 2, null, null, 1, - 4, + 5, null, - 4, - 4, - 368, + 5, + 5, + 460, null, null, null, null, 1, - 11, - 22, + 13, + 26, null, null, 1, - 32, + 37, null, null, 1, @@ -1659,7 +1681,7 @@ 1, null, 1, - 15, + 18, null, null, null, @@ -1692,7 +1714,7 @@ null, null, 1, - 1, + 2, null, null, null, @@ -1713,11 +1735,11 @@ 1, 1, 11, - 14, + 16, null, null, 1, - 14, + 16, null, null, null, @@ -1956,13 +1978,13 @@ 1, null, 1, - 4, - 4, + 3, + 3, null, null, null, null, - 4, + 3, null, null, null, @@ -1970,7 +1992,7 @@ null, null, 1, - 4, + 3, null, null, null, @@ -2013,15 +2035,15 @@ null, null, 1, - 0, + 6, null, null, - 1, - 1, + 7, + 7, null, null, 1, - 0, + 6, null, null, null, @@ -2057,17 +2079,17 @@ 1, null, 1, - 7, + 13, null, null, 1, null, 1, - 7, + 13, null, null, 1, - 7, + 13, null, null, null, @@ -2497,14 +2519,14 @@ null, null, 8, - 7, - 7, + 6, + 6, null, null, - 1, - 1, + 2, + 2, null, - 1, + 2, null, null, null, @@ -2648,7 +2670,7 @@ 1, null, 1, - 2, + 3, null, null, 1, @@ -2778,10 +2800,10 @@ 1, 7, null, - 4, + 3, null, null, - 4, + 3, null, null, null, @@ -2791,11 +2813,11 @@ null, null, 1, - 3, + 4, null, null, - 2, - 2, + 3, + 3, null, null, 1, @@ -2814,8 +2836,8 @@ null, null, 1, - 4, - 4, + 3, + 3, null, null, null, @@ -2892,8 +2914,8 @@ null, 1, 30, - 1730, - 1730, + 1950, + 1950, null, null, 30, @@ -3416,6 +3438,204 @@ null ] }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/unit/appraisal_spec.rb": { + "lines": [ + null, + null, + 1, + null, + 1, + 1, + null, + 15, + null, + null, + null, + null, + null, + null, + 15, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + 7, + null, + null, + null, + null, + 7, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + 1, + null, + null, + null, + 1, + 1, + 7, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + 1, + null, + null, + null, + 1, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + 1, + 6, + null, + null, + null, + null, + null, + null, + 6, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 6, + null, + null, + null, + null, + null, + null, + 6, + 6, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + null + ] + }, "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/unit/gateway_git_spec.rb": { "lines": [ null, @@ -3538,6 +3758,105 @@ null ] }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/unit/remote_cache_spec.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + 1, + 1, + 7, + 7, + null, + null, + 1, + 7, + null, + null, + 1, + 1, + 1, + null, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + 1, + null, + 1, + 1, + 1, + null, + null, + null, + 1, + 1, + 1, + null, + 1, + null, + null, + 1, + 1, + null, + null, + null, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + null, + null, + null, + 1, + 1, + 1, + 1, + null, + 1, + null, + 1, + 1, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/helpers/cache_helper.rb": { + "lines": [ + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + 1, + 7, + 7, + null, + null, + null, + 1, + 14, + null, + null + ] + }, "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/unit/result_spec.rb": { "lines": [ null, @@ -3568,6 +3887,6 @@ ] } }, - "timestamp": 1766298341 + "timestamp": 1766299043 } } diff --git a/spec/helpers/cache_helper.rb b/spec/helpers/cache_helper.rb new file mode 100644 index 0000000..a220eeb --- /dev/null +++ b/spec/helpers/cache_helper.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'fakeredis' + +# Helper for Redis cache testing +# Provides in-memory Redis mock via fakeredis gem +module CacheHelper + # Create a fake Redis-backed cache instance for testing + # Returns a Cache::Remote that uses fakeredis (in-memory) + def self.create_fake_cache + config = OpenStruct.new(REDISCLOUD_URL: 'redis://localhost:6379/15') + CodePraise::Cache::Remote.new(config) + end + + # Wipe all keys from fake Redis + def self.wipe_cache(cache) + cache.wipe + end +end diff --git a/spec/tests/unit/appraisal_spec.rb b/spec/tests/unit/appraisal_spec.rb new file mode 100644 index 0000000..cab3377 --- /dev/null +++ b/spec/tests/unit/appraisal_spec.rb @@ -0,0 +1,194 @@ +# frozen_string_literal: true + +require_relative '../../helpers/spec_helper' + +describe 'Unit test of Value::Appraisal' do + before do + # Create minimal test fixtures + @owner = CodePraise::Entity::Member.new( + id: nil, + origin_id: 123, + username: 'testowner', + email: 'test@example.com' + ) + + @project = CodePraise::Entity::Project.new( + id: nil, + origin_id: 456, + name: 'test-project', + size: 100, + ssh_url: 'git@github.com:testowner/test-project.git', + http_url: 'https://github.com/testowner/test-project', + owner: @owner, + contributors: [] + ) + end + + describe 'success factory method' do + before do + # Create a minimal folder contributions entity for success case + # FolderContributions takes path: and files: (array of FileContributions) + @folder = CodePraise::Entity::FolderContributions.new( + path: '', + files: [] + ) + + @appraisal = CodePraise::Value::Appraisal.success( + project: @project, + folder_path: '', + folder: @folder + ) + end + + it 'should create a success appraisal' do + _(@appraisal.success?).must_equal true + _(@appraisal.error?).must_equal false + end + + it 'should have :ok status' do + _(@appraisal.status).must_equal :ok + end + + it 'should include project' do + _(@appraisal.project).must_equal @project + end + + it 'should include folder contributions' do + _(@appraisal.folder).must_equal @folder + end + + it 'should have nil error fields' do + _(@appraisal.error_type).must_be_nil + _(@appraisal.error_message).must_be_nil + end + + it 'should generate correct cache key' do + _(@appraisal.cache_key).must_equal 'appraisal:testowner/test-project/' + end + + it 'should return success TTL' do + _(@appraisal.ttl).must_equal CodePraise::Value::Appraisal::SUCCESS_TTL + _(@appraisal.ttl).must_equal 86_400 + end + end + + describe 'error factory method' do + before do + @appraisal = CodePraise::Value::Appraisal.error( + project: @project, + folder_path: 'nonexistent/path', + error_type: 'not_found', + error_message: 'Folder does not exist' + ) + end + + it 'should create an error appraisal' do + _(@appraisal.error?).must_equal true + _(@appraisal.success?).must_equal false + end + + it 'should have :error status' do + _(@appraisal.status).must_equal :error + end + + it 'should include project' do + _(@appraisal.project).must_equal @project + end + + it 'should have nil folder' do + _(@appraisal.folder).must_be_nil + end + + it 'should include error details' do + _(@appraisal.error_type).must_equal 'not_found' + _(@appraisal.error_message).must_equal 'Folder does not exist' + end + + it 'should generate correct cache key with folder path' do + _(@appraisal.cache_key).must_equal 'appraisal:testowner/test-project/nonexistent/path' + end + + it 'should return error TTL' do + _(@appraisal.ttl).must_equal CodePraise::Value::Appraisal::ERROR_TTL + _(@appraisal.ttl).must_equal 10 + end + end + + describe 'immutability' do + it 'should not allow attribute modification' do + appraisal = CodePraise::Value::Appraisal.error( + project: @project, + folder_path: '', + error_type: 'test', + error_message: 'test' + ) + + # Dry::Struct instances are immutable - no setter methods exist + _(appraisal.respond_to?(:status=)).must_equal false + _(appraisal.respond_to?(:error_type=)).must_equal false + end + end +end + +describe 'Unit test of Representer::Appraisal' do + before do + @owner = CodePraise::Entity::Member.new( + id: nil, + origin_id: 123, + username: 'testowner', + email: 'test@example.com' + ) + + @project = CodePraise::Entity::Project.new( + id: nil, + origin_id: 456, + name: 'test-project', + size: 100, + ssh_url: 'git@github.com:testowner/test-project.git', + http_url: 'https://github.com/testowner/test-project', + owner: @owner, + contributors: [] + ) + end + + # Note: Full success case serialization with FolderContributions is tested + # in integration tests. Unit tests focus on the Appraisal wrapper behavior. + + describe 'error case serialization' do + before do + @appraisal = CodePraise::Value::Appraisal.error( + project: @project, + folder_path: 'bad/path', + error_type: 'not_found', + error_message: 'Folder not found' + ) + + @json = CodePraise::Representer::Appraisal.new(@appraisal).to_json + @parsed = JSON.parse(@json) + end + + it 'should serialize status as string' do + _(@parsed['status']).must_equal 'error' + end + + it 'should include project' do + _(@parsed['project']).wont_be_nil + end + + it 'should include folder_path' do + _(@parsed['folder_path']).must_equal 'bad/path' + end + + it 'should NOT include folder' do + _(@parsed.key?('folder')).must_equal false + end + + it 'should include error_type' do + _(@parsed['error_type']).must_equal 'not_found' + end + + it 'should include message (aliased from error_message)' do + _(@parsed['message']).must_equal 'Folder not found' + end + end +end diff --git a/spec/tests/unit/remote_cache_spec.rb b/spec/tests/unit/remote_cache_spec.rb new file mode 100644 index 0000000..f98da4e --- /dev/null +++ b/spec/tests/unit/remote_cache_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require_relative '../../helpers/spec_helper' +require_relative '../../helpers/cache_helper' + +describe 'Unit test of Cache::Remote with fakeredis' do + before do + @cache = CacheHelper.create_fake_cache + CacheHelper.wipe_cache(@cache) + end + + after do + CacheHelper.wipe_cache(@cache) + end + + describe 'set and get' do + it 'should store and retrieve a value' do + @cache.set('test:key', 'hello world', ttl: 3600) + + _(@cache.get('test:key')).must_equal 'hello world' + end + + it 'should return nil for non-existent key' do + _(@cache.get('nonexistent')).must_be_nil + end + + it 'should store JSON strings' do + json = '{"status":"ok","data":{"count":42}}' + @cache.set('json:key', json, ttl: 3600) + + result = @cache.get('json:key') + _(result).must_equal json + _(JSON.parse(result)['data']['count']).must_equal 42 + end + end + + describe 'exists?' do + it 'should return true for existing key' do + @cache.set('exists:key', 'value', ttl: 3600) + + _(@cache.exists?('exists:key')).must_equal true + end + + it 'should return false for non-existent key' do + _(@cache.exists?('nonexistent')).must_equal false + end + end + + describe 'keys' do + it 'should list all keys' do + @cache.set('key1', 'value1', ttl: 3600) + @cache.set('key2', 'value2', ttl: 3600) + + keys = @cache.keys + _(keys).must_include 'key1' + _(keys).must_include 'key2' + _(keys.length).must_equal 2 + end + end + + describe 'wipe' do + it 'should remove all keys' do + @cache.set('key1', 'value1', ttl: 3600) + @cache.set('key2', 'value2', ttl: 3600) + + @cache.wipe + + _(@cache.keys).must_be_empty + _(@cache.exists?('key1')).must_equal false + end + end +end From 18aec78bdcb458c4927d042ef3c4febc0a3ced8c Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Sun, 21 Dec 2025 15:03:21 +0800 Subject: [PATCH 06/18] refactor: add worker appraisal service with Redis caching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Worker service (Worker::AppraiseProject): - Dry::Transaction with steps: prepare_inputs → clone_repo → appraise_contributions → cache_result - Converts OpenStruct project to Entity::Project - Creates Value::Appraisal (success or error), serializes with Representer - Stores JSON in Redis with TTL Worker modifications: - git_clone_worker.rb: routes AppraisalRequest to new flow, CloneRequest to legacy - job_reporter.rb: parses both request formats, exposes folder_path and progress_callback - clone_monitor.rb: adds AppraisalMonitor with new progress phases (clone 0-50%, appraise 50-90%, cache 90-100%) Presentation layer: - Response::AppraisalRequest struct (project, folder_path, id) - Representer::AppraisalRequest for JSON serialization Tests: 78 runs, 212 assertions, 94.6% coverage Part of Phase 2: Modify Worker to Appraise 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../appraisal_request_representer.rb | 19 + app/presentation/responses/clone_request.rb | 5 + coverage/.resultset.json | 366 +++++++++++++++++- spec/tests/unit/worker_appraise_spec.rb | 147 +++++++ workers/clone_monitor.rb | 28 ++ workers/git_clone_worker.rb | 41 +- workers/job_reporter.rb | 35 +- workers/services/appraise_project.rb | 153 ++++++++ 8 files changed, 769 insertions(+), 25 deletions(-) create mode 100644 app/presentation/representers/appraisal_request_representer.rb create mode 100644 spec/tests/unit/worker_appraise_spec.rb create mode 100644 workers/services/appraise_project.rb diff --git a/app/presentation/representers/appraisal_request_representer.rb b/app/presentation/representers/appraisal_request_representer.rb new file mode 100644 index 0000000..31b937a --- /dev/null +++ b/app/presentation/representers/appraisal_request_representer.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'roar/decorator' +require 'roar/json' +require_relative 'project_representer' + +module CodePraise + module Representer + # Representer for appraisal request sent to worker queue + # Includes full project info so worker doesn't need database access + class AppraisalRequest < Roar::Decorator + include Roar::JSON + + property :project, extend: Representer::Project, class: OpenStruct + property :folder_path + property :id + end + end +end diff --git a/app/presentation/responses/clone_request.rb b/app/presentation/responses/clone_request.rb index a61b8a3..e77e66f 100644 --- a/app/presentation/responses/clone_request.rb +++ b/app/presentation/responses/clone_request.rb @@ -2,6 +2,11 @@ module CodePraise module Response + # Request to clone a project (legacy - for backwards compatibility) CloneRequest = Struct.new :project, :id + + # Request to appraise a project folder + # Includes full project info so worker doesn't need database access + AppraisalRequest = Struct.new :project, :folder_path, :id end end diff --git a/coverage/.resultset.json b/coverage/.resultset.json index f5535ba..2563bcf 100644 --- a/coverage/.resultset.json +++ b/coverage/.resultset.json @@ -16,7 +16,7 @@ 1, null, 1, - 62, + 63, null, null ] @@ -634,7 +634,7 @@ 1, null, 1, - 15, + 18, null, null, 1, @@ -927,7 +927,7 @@ null, 1, 1, - 7, + 14, null, null, null, @@ -953,11 +953,11 @@ null, null, 1, - 17, + 31, null, null, 1, - 22, + 36, null, null, null, @@ -1735,11 +1735,11 @@ 1, 1, 11, - 16, + 15, null, null, 1, - 16, + 15, null, null, null, @@ -2079,17 +2079,17 @@ 1, null, 1, - 13, + 15, null, null, 1, null, 1, - 13, + 15, null, null, 1, - 13, + 15, null, null, null, @@ -2275,6 +2275,29 @@ null ] }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/appraisal_request_representer.rb": { + "lines": [ + null, + null, + 1, + 1, + 1, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + 1, + 1, + null, + null, + null + ] + }, "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/clone_request_representer.rb": { "lines": [ null, @@ -2437,6 +2460,11 @@ null, 1, 1, + null, + 1, + null, + null, + null, 1, null, null @@ -2914,8 +2942,8 @@ null, 1, 30, - 1950, - 1950, + 1835, + 1835, null, null, 30, @@ -3846,47 +3874,355 @@ null, null, 1, + 14, + 14, + null, + null, + null, + 1, + 28, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/unit/result_spec.rb": { + "lines": [ + null, + null, + 1, + null, + 1, + 1, + 1, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + 1, + null, + null, + 1, + 1, + 1, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/unit/worker_appraise_spec.rb": { + "lines": [ + null, + null, + 1, + 1, + 1, + null, + 1, + 1, 7, 7, null, null, + 7, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 7, + 7, + null, null, 1, - 14, + 7, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + null, + 1, + 1, + 1, + 1, + 1, + null, + null, + 1, + 1, + 1, + null, + 1, + null, + null, + 1, + 1, + null, + null, + null, + 1, + 1, + null, + 1, + 1, + null, + null, + null, + 1, + 1, + 1, + 1, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + 1, + 1, + null, + null, + null, + null, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + null, + null, + null, + 1, + 1, + 2, + null, + null, + null, + null, + null, + null, + 2, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 2, + null, + null, + 1, + 1, + 1, + null, + 1, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + null, + null, + null, + 1, + 1, + 1, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/unit/result_spec.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/services/appraise_project.rb": { "lines": [ null, null, 1, null, 1, + null, + null, 1, 1, null, 1, 1, + 1, + 1, + null, + 1, + null, + null, + null, + null, + null, + null, null, null, 1, + null, + 0, + 0, + null, + 0, + 0, + null, + null, 1, + 0, + null, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + null, null, 1, + null, + 0, + null, + 0, + null, + 0, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, 1, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + 0, + 0, + 0, + null, null, null, 1, + 4, + null, + null, + null, + null, + null, + null, + null, + 4, + 4, + null, + null, + null, 1, + 3, + 4, + null, + 3, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, 1, + 4, + null, + null, + null, + null, + null, null, null, null ] } }, - "timestamp": 1766299043 + "timestamp": 1766299882 } } diff --git a/spec/tests/unit/worker_appraise_spec.rb b/spec/tests/unit/worker_appraise_spec.rb new file mode 100644 index 0000000..b9e1779 --- /dev/null +++ b/spec/tests/unit/worker_appraise_spec.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +require_relative '../../helpers/spec_helper' +require_relative '../../helpers/cache_helper' +require_relative '../../../workers/services/appraise_project' + +describe 'Unit test of Worker::AppraiseProject' do + before do + @cache = CacheHelper.create_fake_cache + CacheHelper.wipe_cache(@cache) + + # Create test project OpenStruct (simulating deserialized JSON) + @project_ostruct = OpenStruct.new( + origin_id: 123, + name: 'test-project', + size: 100, + ssh_url: 'git@github.com:testowner/test-project.git', + http_url: 'https://github.com/testowner/test-project', + fullname: 'testowner/test-project', + owner: OpenStruct.new( + origin_id: 456, + username: 'testowner', + email: 'test@example.com' + ), + contributors: [] + ) + + @progress_calls = [] + @progress_callback = ->(percent) { @progress_calls << percent } + end + + after do + CacheHelper.wipe_cache(@cache) + end + + describe 'build_project_entity helper' do + it 'should convert OpenStruct to Entity::Project' do + service = Worker::AppraiseProject.new + + # Access private method for testing + entity = service.send(:build_project_entity, @project_ostruct) + + _(entity).must_be_instance_of CodePraise::Entity::Project + _(entity.name).must_equal 'test-project' + _(entity.origin_id).must_equal 123 + _(entity.owner.username).must_equal 'testowner' + _(entity.fullname).must_equal 'testowner/test-project' + end + + it 'should handle empty contributors' do + service = Worker::AppraiseProject.new + entity = service.send(:build_project_entity, @project_ostruct) + + _(entity.contributors).must_equal [] + end + + it 'should convert contributors' do + @project_ostruct.contributors = [ + OpenStruct.new(origin_id: 789, username: 'contributor1', email: 'c1@example.com') + ] + + service = Worker::AppraiseProject.new + entity = service.send(:build_project_entity, @project_ostruct) + + _(entity.contributors.length).must_equal 1 + _(entity.contributors.first.username).must_equal 'contributor1' + end + end + + describe 'scale_clone_progress helper' do + it 'should scale Cloning to 25' do + service = Worker::AppraiseProject.new + _(service.send(:scale_clone_progress, 'Cloning into...')).must_equal 25 + end + + it 'should scale Receiving to 40' do + service = Worker::AppraiseProject.new + _(service.send(:scale_clone_progress, 'Receiving objects: 50%')).must_equal 40 + end + + it 'should scale Checking to 50' do + service = Worker::AppraiseProject.new + _(service.send(:scale_clone_progress, 'Checking connectivity...')).must_equal 50 + end + + it 'should default unknown stages to 30' do + service = Worker::AppraiseProject.new + _(service.send(:scale_clone_progress, 'Unknown stage')).must_equal 30 + end + end +end + +describe 'Unit test of AppraisalRequest' do + it 'should create AppraisalRequest struct' do + project = OpenStruct.new(name: 'test') + request = CodePraise::Response::AppraisalRequest.new(project, 'app/models', 'request-123') + + _(request.project.name).must_equal 'test' + _(request.folder_path).must_equal 'app/models' + _(request.id).must_equal 'request-123' + end +end + +describe 'Unit test of Representer::AppraisalRequest' do + before do + @owner = CodePraise::Entity::Member.new( + id: nil, + origin_id: 123, + username: 'testowner', + email: 'test@example.com' + ) + + @project = CodePraise::Entity::Project.new( + id: nil, + origin_id: 456, + name: 'test-project', + size: 100, + ssh_url: 'git@github.com:testowner/test-project.git', + http_url: 'https://github.com/testowner/test-project', + owner: @owner, + contributors: [] + ) + + @request = CodePraise::Response::AppraisalRequest.new(@project, 'app/models', 'req-123') + end + + it 'should serialize to JSON' do + json = CodePraise::Representer::AppraisalRequest.new(@request).to_json + parsed = JSON.parse(json) + + _(parsed['folder_path']).must_equal 'app/models' + _(parsed['id']).must_equal 'req-123' + _(parsed['project']['name']).must_equal 'test-project' + end + + it 'should deserialize from JSON' do + json = CodePraise::Representer::AppraisalRequest.new(@request).to_json + + deserialized = CodePraise::Representer::AppraisalRequest + .new(OpenStruct.new) + .from_json(json) + + _(deserialized.folder_path).must_equal 'app/models' + _(deserialized.id).must_equal 'req-123' + _(deserialized.project.name).must_equal 'test-project' + end +end diff --git a/workers/clone_monitor.rb b/workers/clone_monitor.rb index b93749e..1697f90 100644 --- a/workers/clone_monitor.rb +++ b/workers/clone_monitor.rb @@ -2,6 +2,7 @@ module GitClone # Infrastructure to clone while yielding progress + # Legacy module for backwards compatibility module CloneMonitor CLONE_PROGRESS = { 'STARTED' => 15, @@ -33,4 +34,31 @@ def self.first_word_of(line) line.match(/^[A-Za-z]+/).to_s end end + + # Progress phases for full appraisal flow (clone + appraise + cache) + # Progress distribution: + # Clone: 0-50% (or skip if already cloned) + # Appraise: 50-90% + # Cache: 90-100% + module AppraisalMonitor + PHASES = { + started: 15, + cloning: 25, + clone_receiving: 40, + clone_resolving: 45, + clone_done: 50, + appraising: 55, + appraise_done: 85, + caching: 90, + finished: 100 + }.freeze + + def self.starting_percent + PHASES[:started].to_s + end + + def self.finished_percent + PHASES[:finished].to_s + end + end end diff --git a/workers/git_clone_worker.rb b/workers/git_clone_worker.rb index e63270d..28f71d1 100644 --- a/workers/git_clone_worker.rb +++ b/workers/git_clone_worker.rb @@ -3,13 +3,14 @@ require_relative '../require_app' require_relative 'clone_monitor' require_relative 'job_reporter' +require_relative 'services/appraise_project' require_app require 'figaro' require 'shoryuken' module GitClone - # Shoryuken worker class to clone repos in parallel + # Shoryuken worker class to clone repos and appraise contributions class Worker # Environment variables setup Figaro.application = Figaro::Application.new( @@ -33,6 +34,41 @@ def self.config = Figaro.env def perform(_sqs_msg, request) job = JobReporter.new(request, Worker.config) + if appraisal_request?(request) + perform_appraisal(job) + else + perform_clone_only(job) + end + rescue CodePraise::GitRepo::Errors::CannotOverwriteLocalGitRepo + # worker should crash fail early - only catch errors we expect! + puts 'CLONE EXISTS -- ignoring request' + end + + private + + # Check if this is the new AppraisalRequest format + def appraisal_request?(request) + JSON.parse(request).key?('folder_path') + end + + # New flow: clone + appraise + cache + def perform_appraisal(job) + gitrepo = CodePraise::GitRepo.new(job.project, Worker.config) + + Worker::AppraiseProject.new.call( + project: job.project, + folder_path: job.folder_path, + config: Worker.config, + gitrepo: gitrepo, + progress: job.progress_callback + ) + + # Keep sending finished status to any latecoming subscribers + job.report_each_second(5) { AppraisalMonitor.finished_percent } + end + + # Legacy flow: clone only (for backwards compatibility) + def perform_clone_only(job) job.report(CloneMonitor.starting_percent) CodePraise::GitRepo.new(job.project, Worker.config).clone_locally do |line| job.report CloneMonitor.progress(line) @@ -40,9 +76,6 @@ def perform(_sqs_msg, request) # Keep sending finished status to any latecoming subscribers job.report_each_second(5) { CloneMonitor.finished_percent } - rescue CodePraise::GitRepo::Errors::CannotOverwriteLocalGitRepo - # worker should crash fail early - only catch errors we expect! - puts 'CLONE EXISTS -- ignoring request' end end end diff --git a/workers/job_reporter.rb b/workers/job_reporter.rb index 0398582..c9883b5 100644 --- a/workers/job_reporter.rb +++ b/workers/job_reporter.rb @@ -5,15 +5,14 @@ module GitClone # Reports job progress to client class JobReporter - attr_accessor :project + attr_reader :project, :folder_path def initialize(request_json, config) - clone_request = CodePraise::Representer::CloneRequest - .new(OpenStruct.new) - .from_json(request_json) + request = parse_request(request_json) - @project = clone_request.project - @publisher = ProgressPublisher.new(config, clone_request.id) + @project = request.project + @folder_path = request.respond_to?(:folder_path) ? (request.folder_path || '') : '' + @publisher = ProgressPublisher.new(config, request.id) end def report(msg) @@ -26,5 +25,29 @@ def report_each_second(seconds, &operation) report(operation.call) end end + + # Returns a proc that can be passed to services for progress reporting + def progress_callback + ->(percent) { report(percent.to_s) } + end + + private + + # Parse request - handles both CloneRequest and AppraisalRequest formats + def parse_request(request_json) + parsed = JSON.parse(request_json) + + if parsed.key?('folder_path') + # New AppraisalRequest format + CodePraise::Representer::AppraisalRequest + .new(OpenStruct.new) + .from_json(request_json) + else + # Legacy CloneRequest format + CodePraise::Representer::CloneRequest + .new(OpenStruct.new) + .from_json(request_json) + end + end end end diff --git a/workers/services/appraise_project.rb b/workers/services/appraise_project.rb new file mode 100644 index 0000000..be4de0f --- /dev/null +++ b/workers/services/appraise_project.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +require 'dry/transaction' + +module Worker + # Service to clone repo, appraise contributions, and cache result + # Uses Dry::Transaction for composable steps with railway-oriented error handling + class AppraiseProject + include Dry::Transaction + + step :prepare_inputs + step :clone_repo + step :appraise_contributions + step :cache_result + + private + + # Input: { project:, folder_path:, config:, gitrepo:, progress: } + # project: OpenStruct from deserializing AppraisalRequest + # folder_path: String path to folder being appraised + # config: Worker config with Redis URL + # gitrepo: GitRepo instance + # progress: Proc to report progress + + def prepare_inputs(input) + # Convert OpenStruct project to Entity::Project for Value::Appraisal + input[:project_entity] = build_project_entity(input[:project]) + Success(input) + rescue StandardError => e + puts "PREPARE ERROR: #{e.message}" + Failure(input.merge(error: { type: 'prepare_failed', message: e.message })) + end + + def clone_repo(input) + input[:progress].call(15) # STARTED + + if input[:gitrepo].exists_locally? + input[:progress].call(50) # Skip to post-clone + else + input[:gitrepo].clone_locally do |line| + # Scale clone progress from 15 to 50 + percent = scale_clone_progress(line) + input[:progress].call(percent) + end + end + + Success(input) + rescue StandardError => e + # Create error appraisal and cache it + input[:appraisal] = CodePraise::Value::Appraisal.error( + project: input[:project_entity], + folder_path: input[:folder_path], + error_type: 'clone_failed', + error_message: e.message + ) + # Continue to cache the error + Success(input) + end + + def appraise_contributions(input) + # Skip if already errored in clone step + return Success(input) if input[:appraisal]&.error? + + input[:progress].call(55) # Starting appraisal + + folder = CodePraise::Mapper::Contributions + .new(input[:gitrepo]) + .for_folder(input[:folder_path]) + + input[:progress].call(85) # Appraisal complete + + # Build successful appraisal value object + input[:appraisal] = CodePraise::Value::Appraisal.success( + project: input[:project_entity], + folder_path: input[:folder_path], + folder: folder + ) + + Success(input) + rescue StandardError => e + # Create error appraisal + input[:appraisal] = CodePraise::Value::Appraisal.error( + project: input[:project_entity], + folder_path: input[:folder_path], + error_type: 'appraisal_failed', + error_message: e.message + ) + + # Still cache the error - return Success to continue to cache_result + Success(input) + end + + def cache_result(input) + input[:progress].call(90) # Caching + + appraisal = input[:appraisal] + json = CodePraise::Representer::Appraisal.new(appraisal).to_json + + cache = CodePraise::Cache::Remote.new(input[:config]) + cache.set(appraisal.cache_key, json, ttl: appraisal.ttl) + + input[:progress].call(100) # FINISHED + + Success(input) + rescue StandardError => e + # Cache failure - still report 100% so client can retry + puts "CACHE ERROR: #{e.message}" + input[:progress].call(100) + Success(input) + end + + # Scale git clone progress (15-50 range) + def scale_clone_progress(line) + clone_stages = { + 'Cloning' => 25, + 'remote' => 35, + 'Receiving' => 40, + 'Resolving' => 45, + 'Checking' => 50 + } + + first_word = line.match(/^[A-Za-z]+/).to_s + clone_stages[first_word] || 30 + end + + # Convert OpenStruct project (from JSON) to Entity::Project + def build_project_entity(project_ostruct) + owner = build_member_entity(project_ostruct.owner) + contributors = (project_ostruct.contributors || []).map { |c| build_member_entity(c) } + + CodePraise::Entity::Project.new( + id: nil, + origin_id: project_ostruct.origin_id, + name: project_ostruct.name, + size: project_ostruct.size, + ssh_url: project_ostruct.ssh_url, + http_url: project_ostruct.http_url, + owner: owner, + contributors: contributors + ) + end + + # Convert OpenStruct member to Entity::Member + def build_member_entity(member_ostruct) + CodePraise::Entity::Member.new( + id: nil, + origin_id: member_ostruct.origin_id, + username: member_ostruct.username, + email: member_ostruct.email || '' + ) + end + end +end From 31bb391fc4ca381ac879e6c8ab36205ba3263d7b Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Sun, 21 Dec 2025 22:18:39 +0800 Subject: [PATCH 07/18] refactor: complete Phase 3 - simplify API with Redis cache pass-through MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 Implementation: - Add FetchOrRequestAppraisal service (replaces AppraiseProject) - API checks Redis cache first, returns cached JSON directly - Cache miss sends AppraisalRequest to worker, returns 202 - Worker stores serialized JSON in Redis with TTL Redis Infrastructure: - Use key prefix approach (test: prefix) instead of DB numbers - Cloud Redis doesn't support multiple databases - Local Redis for dev/test (redis://localhost:6379) - Production uses cloud Redis Developer Experience: - rake redis:start/stop/remove/ensure tasks for Docker container - rake spec automatically ensures Redis is running - GitHub Actions: Redis service (Linux) + Homebrew (macOS) - Updated README and CLAUDE.md with setup docs Test Updates: - Updated acceptance tests for new response format - Integration tests for FetchOrRequestAppraisal - Removed fakeredis (using real Redis with test prefix) 82 tests pass, 93.66% coverage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/api.yml | 30 +- Gemfile | 3 - Gemfile.lock | 3 - README.md | 72 +- Rakefile | 96 +- app/application/controllers/app.rb | 21 +- .../services/fetch_or_request_appraisal.rb | 104 ++ app/infrastructure/cache/redis_cache.rb | 19 +- config/secrets_example.yml | 6 +- coverage/.resultset.json | 911 ++++++++++++------ spec/acceptance_tests | 4 + spec/helpers/cache_helper.rb | 21 +- spec/tests/acceptance/api_spec.rb | 16 +- .../fetch_or_request_appraisal_spec.rb | 155 +++ spec/tests/unit/remote_cache_spec.rb | 11 +- spec/tests/unit/worker_appraise_spec.rb | 2 +- workers/git_clone_worker.rb | 5 +- 17 files changed, 1119 insertions(+), 360 deletions(-) create mode 100644 app/application/services/fetch_or_request_appraisal.rb create mode 100644 spec/tests/integration/services/fetch_or_request_appraisal_spec.rb diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index 791a00d..ab19be0 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -28,12 +28,37 @@ jobs: runs-on: ${{ matrix.os }}-latest # Runs on latest builds of matrix OSes env: BUNDLE_WITHOUT: production # skip installing production gem (pg) + + # Redis service container for caching (Linux only - macOS uses brew) + services: + redis: + image: redis + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + # Service containers only work on Linux runners + # macOS will install Redis via Homebrew in the steps below + # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v4 - - # Builds on a predefined action that has Ruby installed + + # Install Redis on macOS (service containers only work on Linux) + - name: Install Redis (macOS) + if: runner.os == 'macOS' + run: | + brew install redis + brew services start redis + # Wait for Redis to be ready + sleep 2 + redis-cli ping + + # Builds on a predefined action that has Ruby installed - uses: ruby/setup-ruby@v1 with: bundler-cache: true # runs 'bundle install' and caches installed gems automatically @@ -49,6 +74,7 @@ jobs: DB_FILENAME: ${{ secrets.DB_FILENAME }} REPOSTORE_PATH: ${{ secrets.REPOSTORE_PATH }} API_HOST: ${{ secrets.API_HOST }} + REDISCLOUD_URL: redis://localhost:6379 CLONE_QUEUE_URL: ${{ secrets.CLONE_QUEUE_URL }} AWS_REGION: ${{ secrets.AWS_REGION }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} diff --git a/Gemfile b/Gemfile index 5317c36..bc60558 100644 --- a/Gemfile +++ b/Gemfile @@ -71,9 +71,6 @@ group :test do gem 'simplecov', '~> 0.0' gem 'vcr', '~> 6.0' gem 'webmock', '~> 3.0' - - # Redis mocking for isolated tests - gem 'fakeredis', '~> 0.9' end # Development diff --git a/Gemfile.lock b/Gemfile.lock index aced5c8..55494e4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -94,8 +94,6 @@ GEM base64 eventmachine (>= 1.0.0.beta.4) eventmachine (1.2.7) - fakeredis (0.9.2) - redis (~> 4.8) faye (1.4.1) cookiejar (>= 0.3.0) em-http-request (>= 1.1.6) @@ -278,7 +276,6 @@ DEPENDENCIES dry-transaction (~> 0) dry-types (~> 1.0) dry-validation (~> 1.0) - fakeredis (~> 0.9) faye (~> 1.0) figaro (~> 1.0) flog diff --git a/README.md b/README.md index 7968a0b..5793b6d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,76 @@ # CodePraise Web API -Web API that allowsGithub *projects* to be *appraised* for inidividual *contributions* by *members* of a team. +Web API that allows Github *projects* to be *appraised* for individual *contributions* by *members* of a team. + +## Setup + +### Prerequisites + +1. **Ruby** - See `.ruby-version` for required version +2. **Redis** - Required for caching appraisal results +3. **AWS Account** - For SQS message queue (worker communication) + +### Install Redis + +**Using rake tasks (recommended):** + +```bash +rake redis:start # Start Redis Docker container +rake redis:stop # Stop Redis when done +rake redis:status # Check connectivity +``` + +**Or manually with Homebrew (macOS):** + +```bash +brew install redis +brew services start redis +``` + +### Install Dependencies + +```bash +bundle install +``` + +### Configure Secrets + +```bash +cp config/secrets_example.yml config/secrets.yml +``` + +Edit `config/secrets.yml` and add your: + +- `GITHUB_TOKEN` - GitHub personal access token +- AWS credentials for SQS queue + +### Setup Database + +```bash +bundle exec rake db:migrate # Development database +RACK_ENV=test bundle exec rake db:migrate # Test database +``` + +### Verify Setup + +```bash +rake redis:status # Check Redis connectivity +rake queues:status # Check SQS queue status +``` + +## Running the Application + +```bash +rake run # Start API server on port 9090 +rake worker:run:dev # Start background worker (in separate terminal) +``` + +## Testing + +```bash +rake spec # Run unit and integration tests +bash spec/acceptance_tests # Run full acceptance tests (starts worker automatically) +``` ## Routes diff --git a/Rakefile b/Rakefile index e87656a..a2e0284 100644 --- a/Rakefile +++ b/Rakefile @@ -8,11 +8,14 @@ task :default do end desc 'Run unit and integration tests' -Rake::TestTask.new(:spec) do |t| +Rake::TestTask.new(:spec_only) do |t| t.pattern = 'spec/tests/**/*_spec.rb' t.warning = false end +# Run specs with Redis check +task spec: ['redis:ensure', :spec_only] + desc 'Keep rerunning unit/integration tests upon changes' task :respec do sh "rerun -c 'rake spec' --ignore 'coverage/*' --ignore 'repostore/*'" @@ -108,6 +111,97 @@ namespace :repos do end end +namespace :redis do + CONTAINER_NAME = 'redis-codepraise' + + task :config do # rubocop:disable Rake/Desc + require 'redis' + require_relative 'config/environment' + @api = CodePraise::App + end + + desc 'Check Redis connectivity' + task status: :config do + redis_url = @api.config.REDISCLOUD_URL + puts "Checking Redis at: #{redis_url}" + redis = Redis.new(url: redis_url) + response = redis.ping + puts "Redis responded: #{response}" + puts 'Redis connection successful!' + rescue Redis::CannotConnectError => e + puts "Redis connection FAILED: #{e.message}" + puts '' + puts 'To start Redis locally:' + puts ' rake redis:start' + exit 1 + end + + desc 'Start Redis Docker container' + task :start do + # Check if container exists + container_exists = system("docker ps -a --format '{{.Names}}' | grep -q '^#{CONTAINER_NAME}$'") + + if container_exists + # Container exists, check if running + container_running = system("docker ps --format '{{.Names}}' | grep -q '^#{CONTAINER_NAME}$'") + if container_running + puts "Redis container '#{CONTAINER_NAME}' is already running" + else + puts "Starting existing Redis container '#{CONTAINER_NAME}'..." + sh "docker start #{CONTAINER_NAME}" + end + else + # Create and start new container + puts "Creating and starting Redis container '#{CONTAINER_NAME}'..." + sh "docker run -d --name #{CONTAINER_NAME} -p 6379:6379 redis:latest" + end + + # Wait for Redis to be ready + puts 'Waiting for Redis to be ready...' + sleep 2 + Rake::Task['redis:status'].invoke + end + + desc 'Stop Redis Docker container' + task :stop do + container_running = system("docker ps --format '{{.Names}}' | grep -q '^#{CONTAINER_NAME}$'") + if container_running + puts "Stopping Redis container '#{CONTAINER_NAME}'..." + sh "docker stop #{CONTAINER_NAME}" + puts 'Redis container stopped' + else + puts "Redis container '#{CONTAINER_NAME}' is not running" + end + end + + desc 'Remove Redis Docker container' + task :remove do + Rake::Task['redis:stop'].invoke + container_exists = system("docker ps -a --format '{{.Names}}' | grep -q '^#{CONTAINER_NAME}$'") + if container_exists + puts "Removing Redis container '#{CONTAINER_NAME}'..." + sh "docker rm #{CONTAINER_NAME}" + puts 'Redis container removed' + else + puts "Redis container '#{CONTAINER_NAME}' does not exist" + end + end + + desc 'Ensure Redis is running (start if needed)' + task :ensure do + require 'redis' + require_relative 'config/environment' + redis_url = CodePraise::App.config.REDISCLOUD_URL + + redis = Redis.new(url: redis_url) + redis.ping + puts 'Redis is running' + rescue Redis::CannotConnectError + puts 'Redis not running, starting Docker container...' + Rake::Task['redis:start'].invoke + end +end + namespace :cache do task :config do # rubocop:disable Rake/Desc require_relative 'app/infrastructure/cache/local_cache' diff --git a/app/application/controllers/app.rb b/app/application/controllers/app.rb index 90f1862..285b57e 100644 --- a/app/application/controllers/app.rb +++ b/app/application/controllers/app.rb @@ -42,9 +42,9 @@ class App < Roda owner_name, project_name, request ) - result = Service::AppraiseProject.new.call( + result = Service::FetchOrRequestAppraisal.new.call( requested: path_request, - request_id: request_id, + request_id:, config: App.config ) @@ -53,12 +53,17 @@ class App < Roda routing.halt failed.http_status_code, failed.to_json end - http_response = Representer::HttpResponse.new(result.value!) - response.status = http_response.http_status_code - - Representer::ProjectFolderContributions.new( - result.value!.message - ).to_json + # Cache hit - return pre-serialized JSON directly + appraisal_result = result.value! + if appraisal_result[:cache_hit] + response.status = 200 + appraisal_result[:cached_json] + else + # Should not reach here - success means cache hit + # Worker requests return Failure with :processing status + response.status = 200 + appraisal_result[:cached_json] + end end # POST /projects/{owner_name}/{project_name} diff --git a/app/application/services/fetch_or_request_appraisal.rb b/app/application/services/fetch_or_request_appraisal.rb new file mode 100644 index 0000000..a3ff910 --- /dev/null +++ b/app/application/services/fetch_or_request_appraisal.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'dry/transaction' + +module CodePraise + module Service + # Fetches appraisal from cache or requests worker to create it + # Does NOT perform appraisal - that's delegated to the worker + class FetchOrRequestAppraisal + include Dry::Transaction + + step :find_project_details + step :check_project_eligibility + step :check_cache + step :request_appraisal_worker + + private + + NO_PROJ_ERR = 'Project not found' + DB_ERR = 'Having trouble accessing the database' + REQUEST_ERR = 'Could not request appraisal' + TOO_LARGE_ERR = 'Project is too large to analyze' + PROCESSING_MSG = 'Processing the appraisal request' + + # input hash keys expected: :requested, :request_id, :config + def find_project_details(input) + input[:project] = Repository::For.klass(Entity::Project).find_full_name( + input[:requested].owner_name, input[:requested].project_name + ) + + if input[:project] + Success(input) + else + Failure(Response::ApiResult.new(status: :not_found, message: NO_PROJ_ERR)) + end + rescue StandardError + Failure(Response::ApiResult.new(status: :internal_error, message: DB_ERR)) + end + + def check_project_eligibility(input) + if input[:project].too_large? + Failure(Response::ApiResult.new(status: :forbidden, message: TOO_LARGE_ERR)) + else + Success(input) + end + end + + def check_cache(input) + cache = Cache::Remote.new(input[:config]) + cache_key = appraisal_cache_key(input) + + cached_json = cache.get(cache_key) + return Success(input) unless cached_json + + # Cache hit - return the cached JSON directly + # Mark as cache hit so controller knows to pass through + input[:cached_json] = cached_json + input[:cache_hit] = true + Success(input) + rescue StandardError => e + # Cache errors should not fail the request - continue to worker + App.logger.warn "Cache error: #{e.message}" + Success(input) + end + + def request_appraisal_worker(input) + # If cache hit, we're done - return success with cached data + return Success(input) if input[:cache_hit] + + # Cache miss - send request to worker + Messaging::Queue.new(App.config.CLONE_QUEUE_URL, App.config) + .send(appraisal_request_json(input)) + + Failure(Response::ApiResult.new( + status: :processing, + message: { request_id: input[:request_id], msg: PROCESSING_MSG } + )) + rescue StandardError => e + log_error(e) + Failure(Response::ApiResult.new(status: :internal_error, message: REQUEST_ERR)) + end + + # Helper methods + + def appraisal_cache_key(input) + folder_path = input[:requested].folder_name || '' + "appraisal:#{input[:project].fullname}/#{folder_path}" + end + + def appraisal_request_json(input) + Response::AppraisalRequest.new( + input[:project], + input[:requested].folder_name || '', + input[:request_id] + ).then { Representer::AppraisalRequest.new(_1) } + .then(&:to_json) + end + + def log_error(error) + App.logger.error [error.inspect, error.backtrace].flatten.join("\n") + end + end + end +end diff --git a/app/infrastructure/cache/redis_cache.rb b/app/infrastructure/cache/redis_cache.rb index 9099075..9367d4f 100644 --- a/app/infrastructure/cache/redis_cache.rb +++ b/app/infrastructure/cache/redis_cache.rb @@ -5,9 +5,14 @@ module CodePraise module Cache # Redis client utility for caching with TTL support + # Uses key prefixes to separate namespaces: + # - 'appraisal:' prefix for appraisal cache + # - 'test:appraisal:' prefix for test environment + # This avoids conflicts with Rack::Cache which uses its own key patterns class Remote def initialize(config) @redis = Redis.new(url: config.REDISCLOUD_URL) + @key_prefix = ENV['RACK_ENV'] == 'test' ? 'test:' : '' end # Store a value with expiration @@ -15,30 +20,36 @@ def initialize(config) # @param value [String] value to store (caller handles serialization) # @param ttl [Integer] time-to-live in seconds def set(key, value, ttl:) - @redis.setex(key, ttl, value) + @redis.setex(prefixed(key), ttl, value) end # Retrieve a cached value # @param key [String] cache key # @return [String, nil] cached value or nil if not found/expired def get(key) - @redis.get(key) + @redis.get(prefixed(key)) end # Check if a key exists # @param key [String] cache key # @return [Boolean] true if key exists def exists?(key) - @redis.exists?(key) + @redis.exists?(prefixed(key)) end def keys - @redis.keys + @redis.keys("#{@key_prefix}*") end def wipe keys.each { |key| @redis.del(key) } end + + private + + def prefixed(key) + "#{@key_prefix}#{key}" + end end end end diff --git a/config/secrets_example.yml b/config/secrets_example.yml index c80db59..333a6c6 100644 --- a/config/secrets_example.yml +++ b/config/secrets_example.yml @@ -7,7 +7,7 @@ development: REPOSTORE_PATH: repostore LOCAL_CACHE: _cache/rack API_HOST: http://localhost:9090 - REDISCLOUD_URL: url-assigned-by-Redis-provider-on-Heroku + REDISCLOUD_URL: redis://localhost:6379 AWS_ACCESS_KEY_ID: AWS_SECRET_ACCESS_KEY: AWS_REGION: @@ -21,7 +21,7 @@ app_test: REPOSTORE_PATH: repostore LOCAL_CACHE: _cache/rack API_HOST: http://localhost:9090 - REDISCLOUD_URL: url-assigned-by-Redis-provider-on-Heroku + REDISCLOUD_URL: redis://localhost:6379 AWS_ACCESS_KEY_ID: AWS_SECRET_ACCESS_KEY: AWS_REGION: @@ -36,7 +36,7 @@ test: REPOSTORE_PATH: repostore LOCAL_CACHE: _cache/rack API_HOST: http://localhost:9090 - REDISCLOUD_URL: url-assigned-by-Redis-provider-on-Heroku + REDISCLOUD_URL: redis://localhost:6379 AWS_ACCESS_KEY_ID: AWS_SECRET_ACCESS_KEY: AWS_REGION: diff --git a/coverage/.resultset.json b/coverage/.resultset.json index 2563bcf..b0ee463 100644 --- a/coverage/.resultset.json +++ b/coverage/.resultset.json @@ -16,7 +16,7 @@ 1, null, 1, - 63, + 64, null, null ] @@ -128,7 +128,7 @@ null, null, 1, - 7555, + 4928, null, null, null, @@ -136,7 +136,7 @@ null, null, 1, - 6940, + 4513, null, null, null, @@ -159,29 +159,29 @@ 1, null, 1, - 190, - 190, + 124, + 124, null, null, 1, - 711, + 247, null, null, 1, - 199, + 0, null, null, 1, - 711, + 247, null, - 711, + 247, null, - 5360, + 3433, null, null, null, 1, - 66, + 0, null, null, 1, @@ -202,7 +202,7 @@ null, 1, 1, - 199, + 0, null, null, 1, @@ -246,7 +246,7 @@ null, null, 1, - 6496, + 3935, null, null, 1, @@ -261,7 +261,7 @@ null, null, 1, - 5360, + 3433, null, null, 1, @@ -269,7 +269,7 @@ null, null, 1, - 11870, + 3433, null, null, null, @@ -397,20 +397,20 @@ 1, null, 1, - 55, + 38, null, - 55, - 55, - 55, - 55, + 38, + 38, + 38, + 38, null, null, 1, - 17, + 0, null, null, 1, - 17, + 0, null, null, 1, @@ -418,51 +418,51 @@ null, null, 1, - 17, + 0, null, null, 1, - 17, + 0, null, null, 1, - 58, + 24, null, null, 1, - 17, + 0, null, null, 1, null, 1, - 584, + 384, null, null, 1, - 55, + 38, null, null, 1, - 55, - 387, - 190, + 38, + 254, + 124, null, null, 1, - 55, + 38, null, - 197, - 197, + 130, + 130, null, null, null, 1, - 55, - 43, + 38, + 28, null, null, - 98, + 66, null, null, null, @@ -488,14 +488,14 @@ 1, null, 1, - 5360, - 5360, - 5360, - 5360, + 3433, + 3433, + 3433, + 3433, null, null, 1, - 11870, + 3433, null, null, null, @@ -511,14 +511,14 @@ null, 1, 1, - 153, + 104, null, null, null, null, 1, 1, - 1845, + 1241, null, null, null, @@ -634,16 +634,16 @@ 1, null, 1, - 18, + 25, null, null, 1, - 10, + 13, null, null, 1, null, - 15, + 18, null, null, null, @@ -669,7 +669,7 @@ 1, null, 1, - 57, + 69, null, null, null, @@ -685,7 +685,7 @@ 1, null, 1, - 356, + 240, null, null, null, @@ -693,15 +693,15 @@ null, 1, null, - 356, - 356, + 240, + 240, null, - 356, + 240, null, - 1091, - 1091, + 742, + 742, null, - 1091, + 742, null, 0, 0, @@ -710,20 +710,20 @@ 0, 0, null, - 1091, + 742, null, - 86, - 86, - 86, + 58, + 58, + 58, null, null, - 1005, - 1005, - 1005, + 684, + 684, + 684, null, null, null, - 356, + 240, null, null, null, @@ -744,8 +744,8 @@ 1, null, 1, - 546, - 546, + 364, + 364, null, null, null, @@ -772,29 +772,29 @@ null, null, 1, - 199, + 0, null, null, 1, - 6365, - 6365, - 6365, - 6365, + 4117, + 4117, + 4117, + 4117, null, null, 1, - 356, + 240, null, null, null, - 356, - 446, + 240, + 310, null, null, - 356, - 1005, - 2096, - 1005, + 240, + 684, + 1426, + 684, null, null, null, @@ -810,7 +810,7 @@ 1, null, 1, - 13305, + 8622, null, null, null, @@ -844,38 +844,38 @@ 1, null, 1, - 760, - 760, + 476, + 476, null, null, 1, - 12887, + 7807, null, null, 1, - 6451, + 3908, null, - 6436, + 3899, null, null, 1, - 380, + 228, null, null, 1, null, - 197, + 130, null, - 197, - 197, + 130, + 130, null, null, 1, null, 1, - 760, - 760, - 760, + 476, + 476, + 476, null, null, null, @@ -925,9 +925,14 @@ 1, 1, null, + null, + null, + null, + null, 1, 1, - 14, + 48, + 48, null, null, null, @@ -935,14 +940,14 @@ null, null, 1, - 7, + 8, null, null, null, null, null, 1, - 3, + 11, null, null, null, @@ -953,11 +958,17 @@ null, null, 1, - 31, + 61, null, null, 1, - 36, + 70, + null, + null, + 1, + null, + 1, + 22, null, null, null, @@ -986,7 +997,7 @@ 1, null, 1, - 56, + 68, null, null, null, @@ -1034,11 +1045,11 @@ null, null, 1, - 28, + 32, null, null, 1, - 14, + 17, null, null, null, @@ -1062,9 +1073,9 @@ null, null, 1, - 104, + 128, null, - 104, + 128, null, null, null, @@ -1073,13 +1084,13 @@ null, null, 1, - 26, - 78, + 32, + 96, null, null, null, 1, - 56, + 68, null, null, null, @@ -1105,12 +1116,12 @@ null, null, null, - 27, + 31, null, null, null, null, - 27, + 31, null, null, 1, @@ -1121,7 +1132,7 @@ null, null, 1, - 14, + 17, null, null, 1, @@ -1130,21 +1141,21 @@ null, null, 1, - 14, - 14, + 17, + 17, null, null, 1, - 14, + 17, null, - 14, - 14, + 17, + 17, null, null, 1, - 55, + 65, null, - 26, + 32, null, null, null, @@ -1155,21 +1166,21 @@ null, 1, 1, - 14, + 17, null, null, 1, - 14, + 17, null, null, 1, - 14, + 17, null, - 14, - 14, + 17, + 17, null, - 14, - 42, + 17, + 51, null, null, null, @@ -1190,10 +1201,10 @@ 1, null, 1, - 194, - 194, - 194, - 194, + 128, + 128, + 128, + 128, null, null, 1, @@ -1203,14 +1214,14 @@ null, null, 1, - 191, - 191, - 191, + 125, + 125, + 125, null, null, 1, - 191, - 191, + 125, + 125, null, null, 1, @@ -1224,18 +1235,18 @@ null, null, 1, - 388, + 256, null, null, 1, - 194, + 128, null, null, null, null, null, 1, - 190, + 124, null, null, 1, @@ -1257,12 +1268,12 @@ null, 1, 1, - 5360, - 5360, + 3433, + 3433, null, null, 1, - 5360, + 3433, null, null, null, @@ -1272,7 +1283,7 @@ 1, null, 1, - 5360, + 3433, null, null, null, @@ -1288,13 +1299,13 @@ null, 1, 1, - 6, + 3, null, null, 1, - 6, + 3, null, - 5, + 3, null, null, null, @@ -1302,17 +1313,17 @@ null, 1, 1, - 5, - 190, + 3, + 124, null, null, null, null, 1, 1, - 190, - 190, - 190, + 124, + 124, + 124, null, null, null, @@ -1331,11 +1342,11 @@ null, 1, 1, - 190, + 124, null, null, 1, - 190, + 124, null, null, null, @@ -1344,20 +1355,20 @@ 1, null, 1, - 380, + 248, null, null, 1, - 5360, + 3433, null, null, 1, - 190, + 124, null, null, 1, - 190, - 5360, + 124, + 3433, null, null, null, @@ -1365,9 +1376,9 @@ null, 1, 1, - 5360, - 5360, - 5360, + 3433, + 3433, + 3433, null, null, 1, @@ -1377,14 +1388,14 @@ 1, null, 1, - 5360, + 3433, null, null, null, 1, null, 1, - 5360, + 3433, null, null, null, @@ -1405,20 +1416,20 @@ null, null, 1, - 5, - 5, + 3, + 3, null, null, 1, - 5, + 3, null, null, null, null, null, 1, - 5, - 190, + 3, + 124, null, null, null, @@ -1438,13 +1449,13 @@ 1, null, 1, - 190, - 5360, + 124, + 3433, null, null, 1, - 190, - 190, + 124, + 124, null, 0, 0, @@ -1452,28 +1463,28 @@ null, null, 1, - 5360, + 3433, null, - 5360, + 3433, null, null, null, - 5360, - 56896, - 56896, + 3433, + 36432, + 36432, null, null, - 5360, + 3433, null, null, 1, - 5360, - 5360, - 5360, + 3433, + 3433, + 3433, null, null, 1, - 56896, + 36432, null, null, null, @@ -1495,17 +1506,17 @@ 1, null, 1, - 6, - 6, - 6, + 3, + 3, + 3, null, null, 1, - 6, + 3, null, - 310, + 186, null, - 10, + 6, null, null, 1, @@ -1522,19 +1533,19 @@ null, null, 1, - 190, + 124, null, null, 1, null, 1, - 6, + 3, null, - 6, + 2, null, null, 1, - 1, + 0, null, null, null, @@ -1544,10 +1555,10 @@ null, null, 1, - 5, - 190, - 190, - 190, + 3, + 124, + 124, + 124, null, null, null, @@ -1566,11 +1577,11 @@ 1, null, 1, - 190, + 124, null, null, 1, - 190, + 124, null, null, null, @@ -1594,13 +1605,13 @@ null, null, 1, - 11, - 11, - 11, + 5, + 5, + 5, null, null, 1, - 6, + 3, null, null, 1, @@ -1608,7 +1619,7 @@ null, null, 1, - 19, + 10, null, null, 1, @@ -1642,8 +1653,8 @@ 1, null, 1, - 11, - 11, + 5, + 5, null, null, 1, @@ -1652,22 +1663,22 @@ null, null, 1, - 5, + 3, null, - 5, - 5, - 460, + 3, + 3, + 276, null, null, null, null, 1, - 13, - 26, + 7, + 14, null, null, 1, - 37, + 20, null, null, 1, @@ -1681,7 +1692,7 @@ 1, null, 1, - 18, + 10, null, null, null, @@ -1706,11 +1717,11 @@ 1, null, 1, - 11, + 5, null, null, 1, - 11, + 5, null, null, 1, @@ -1735,11 +1746,11 @@ 1, 1, 11, - 15, + 16, null, null, 1, - 15, + 16, null, null, null, @@ -1759,45 +1770,45 @@ 1, null, 1, - 51, + 57, null, null, 1, - 28, + 31, null, null, null, null, 1, - 23, + 26, null, null, null, 1, 1, - 28, - 28, + 31, + 31, null, null, 1, - 28, + 31, null, null, null, null, 1, 1, - 51, + 57, null, null, 1, - 51, + 57, null, null, null, null, - 50, - 50, + 56, + 56, null, null, null, @@ -1813,7 +1824,7 @@ null, null, 1, - 50, + 56, null, null, 1, @@ -1835,29 +1846,29 @@ null, 1, 1, - 23, - 23, - 23, + 26, + 26, + 26, null, null, 1, - 23, - 69, + 26, + 78, null, null, null, 1, - 92, + 104, null, null, null, 1, 1, - 92, + 104, null, null, 1, - 92, + 104, null, null, null, @@ -1868,15 +1879,15 @@ 1, null, 1, - 92, + 104, null, null, 1, - 92, + 104, null, null, 1, - 92, + 104, null, null, null, @@ -1895,31 +1906,31 @@ null, 1, 1, - 28, - 28, - 28, + 31, + 31, + 31, null, null, 1, - 28, - 23, + 31, + 26, null, null, 1, - 23, + 26, null, null, null, 1, 1, - 23, - 23, + 26, + 26, null, null, null, null, 1, - 23, + 26, null, null, null, @@ -1932,31 +1943,31 @@ null, null, 1, - 23, + 26, null, null, 1, - 23, + 26, null, null, 1, - 23, + 26, null, null, 1, - 23, + 26, null, null, 1, - 23, + 26, null, null, 1, - 23, + 26, null, null, 1, - 23, + 26, null, null, null, @@ -2079,17 +2090,17 @@ 1, null, 1, - 15, + 14, null, null, 1, null, 1, - 15, + 14, null, null, 1, - 15, + 14, null, null, null, @@ -2355,7 +2366,7 @@ null, null, 1, - 14, + 11, null, null, null, @@ -2547,16 +2558,21 @@ null, null, 8, - 6, - 6, + 5, + 5, null, null, - 2, - 2, null, - 2, + 3, + 3, + 3, + 3, + null, null, null, + 0, + 0, + null, null, null, null, @@ -2698,7 +2714,7 @@ 1, null, 1, - 3, + 9, null, null, 1, @@ -2803,35 +2819,35 @@ null, null, 1, - 10, - null, + 2, null, null, - 10, - 8, null, 2, + 1, + null, + 1, null, null, 0, null, null, 1, - 8, 1, + 0, null, - 7, - 7, + 1, + 1, null, null, null, 1, - 7, + 1, null, - 3, + 0, null, null, - 3, + 0, null, null, null, @@ -2841,14 +2857,14 @@ null, null, 1, - 4, + 1, null, null, - 3, - 3, + 1, + 1, null, null, - 1, + 0, null, null, null, @@ -2864,9 +2880,117 @@ null, null, 1, - 3, - 3, + 0, + 0, + null, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/fetch_or_request_appraisal.rb": { + "lines": [ + null, + null, + 1, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + 1, + 1, + 1, + null, + 1, + null, + 1, + 1, + 1, + 1, + 1, + null, + null, + 1, + 12, + null, + null, + null, + 12, + 10, + null, + 2, + null, + null, + 0, + null, + null, + 1, + 10, + 2, + null, + 8, + null, + null, + null, + 1, + 8, + 8, + null, + 8, + 8, + null, + null, + null, + 4, + 4, + 4, + null, + null, + 0, + 0, + null, + null, + 1, + null, + 8, + null, + null, + 4, + null, + null, + 4, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 1, + 8, + 8, + null, + null, + 1, + 4, + null, + null, + null, + 4, + null, + null, null, + 1, + 0, null, null, null, @@ -2930,23 +3054,23 @@ 1, null, 1, - 7, - 7, - 7, - 7, - 7, - 7, + 8, + 8, + 8, + 8, + 8, + 8, null, null, null, null, 1, - 30, - 1835, - 1835, + 34, + 2419, + 2419, null, null, - 30, + 34, null, null, null, @@ -2955,7 +3079,7 @@ null, null, 1, - 30, + 34, null, null ] @@ -2969,10 +3093,34 @@ null, 1, null, - 22, - 22, - 22, - 22, + 26, + 26, + 26, + 26, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/helpers/cache_helper.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + 1, + 40, + null, + null, + null, + 1, + 58, + 58, null, null ] @@ -3379,6 +3527,165 @@ null ] }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/integration/services/fetch_or_request_appraisal_spec.rb": { + "lines": [ + null, + null, + 1, + 1, + 1, + 1, + null, + 1, + null, + 1, + 1, + null, + 1, + 4, + 4, + 4, + 4, + null, + 4, + null, + null, + 1, + 4, + 4, + null, + null, + 1, + 1, + null, + 1, + null, + null, + 1, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + null, + 1, + null, + null, + 1, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + 1, + null, + 1, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null + ] + }, "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/integration/services/list_projects_spec.rb": { "lines": [ null, @@ -3842,6 +4149,7 @@ 1, null, 1, + null, 1, 1, 1, @@ -3862,29 +4170,6 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/helpers/cache_helper.rb": { - "lines": [ - null, - null, - 1, - null, - null, - null, - 1, - null, - null, - 1, - 14, - 14, - null, - null, - null, - 1, - 28, - null, - null - ] - }, "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/unit/result_spec.rb": { "lines": [ null, @@ -4223,6 +4508,6 @@ ] } }, - "timestamp": 1766299882 + "timestamp": 1766326644 } } diff --git a/spec/acceptance_tests b/spec/acceptance_tests index a574b07..d7d98a1 100755 --- a/spec/acceptance_tests +++ b/spec/acceptance_tests @@ -2,6 +2,10 @@ # see: https://stackoverflow.com/questions/360201/how-do-i-kill-background-processes-jobs-when-my-shell-script-exits trap "kill 0" EXIT +# Ensure Redis is running +echo "Checking Redis..." +bundle exec rake redis:ensure + # Create logs directory if needed (gitignored via spec/logs/) mkdir -p spec/logs diff --git a/spec/helpers/cache_helper.rb b/spec/helpers/cache_helper.rb index a220eeb..34c921a 100644 --- a/spec/helpers/cache_helper.rb +++ b/spec/helpers/cache_helper.rb @@ -1,19 +1,20 @@ # frozen_string_literal: true -require 'fakeredis' - # Helper for Redis cache testing -# Provides in-memory Redis mock via fakeredis gem +# Uses real Redis with database 2 (test database) to avoid conflicts with: +# - Database 0: Rack::Cache (reverse proxy) +# - Database 1: Appraisal cache (production/development) module CacheHelper - # Create a fake Redis-backed cache instance for testing - # Returns a Cache::Remote that uses fakeredis (in-memory) - def self.create_fake_cache - config = OpenStruct.new(REDISCLOUD_URL: 'redis://localhost:6379/15') - CodePraise::Cache::Remote.new(config) + # Create a cache instance for testing + # Uses App.config which points to real Redis, but Cache::Remote + # automatically uses database 2 when RACK_ENV=test + def self.create_test_cache + CodePraise::Cache::Remote.new(CodePraise::App.config) end - # Wipe all keys from fake Redis - def self.wipe_cache(cache) + # Wipe all keys from test cache + def self.wipe_cache(cache = nil) + cache ||= create_test_cache cache.wipe end end diff --git a/spec/tests/acceptance/api_spec.rb b/spec/tests/acceptance/api_spec.rb index b13555e..f4c84ae 100644 --- a/spec/tests/acceptance/api_spec.rb +++ b/spec/tests/acceptance/api_spec.rb @@ -3,6 +3,7 @@ require_relative '../../helpers/spec_helper' require_relative '../../helpers/vcr_helper' require_relative '../../helpers/database_helper' +require_relative '../../helpers/cache_helper' require 'rack/test' def app @@ -18,10 +19,12 @@ def app VcrHelper.configure_vcr_for_github DatabaseHelper.wipe_database CodePraise::Repository::RepoStore.wipe + CacheHelper.wipe_cache end after do VcrHelper.eject_vcr + CacheHelper.wipe_cache end describe 'Root route' do @@ -49,7 +52,8 @@ def app get "/api/v1/projects/#{USERNAME}/#{PROJECT_NAME}" _(last_response.status).must_equal 200 appraisal = JSON.parse last_response.body - _(appraisal.keys.sort).must_equal %w[folder project] + _(appraisal['status']).must_equal 'ok' + _(appraisal['folder_path']).must_equal '' _(appraisal['project']['name']).must_equal PROJECT_NAME _(appraisal['project']['owner']['username']).must_equal USERNAME _(appraisal['project']['contributors'].count).must_equal 3 @@ -72,7 +76,8 @@ def app get "/api/v1/projects/#{USERNAME}/#{PROJECT_NAME}/spec" _(last_response.status).must_equal 200 appraisal = JSON.parse last_response.body - _(appraisal.keys.sort).must_equal %w[folder project] + _(appraisal['status']).must_equal 'ok' + _(appraisal['folder_path']).must_equal 'spec' _(appraisal['project']['name']).must_equal PROJECT_NAME _(appraisal['project']['owner']['username']).must_equal USERNAME _(appraisal['project']['contributors'].count).must_equal 3 @@ -92,9 +97,12 @@ def app 5.times { sleep(1); print('_') } + # Error appraisals are cached with status 'error' get "/api/v1/projects/#{USERNAME}/#{PROJECT_NAME}/foobar" - _(last_response.status).must_equal 404 - _(JSON.parse(last_response.body)['status']).must_include 'not' + _(last_response.status).must_equal 200 + appraisal = JSON.parse last_response.body + _(appraisal['status']).must_equal 'error' + _(appraisal['error_type']).wont_be_nil end it 'should be report error for an invalid project' do diff --git a/spec/tests/integration/services/fetch_or_request_appraisal_spec.rb b/spec/tests/integration/services/fetch_or_request_appraisal_spec.rb new file mode 100644 index 0000000..ff29df5 --- /dev/null +++ b/spec/tests/integration/services/fetch_or_request_appraisal_spec.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +require_relative '../../../helpers/spec_helper' +require_relative '../../../helpers/vcr_helper' +require_relative '../../../helpers/database_helper' +require_relative '../../../helpers/cache_helper' + +require 'ostruct' + +describe 'FetchOrRequestAppraisal Service Integration Test' do + VcrHelper.setup_vcr + + before do + VcrHelper.configure_vcr_for_github(recording: :none) + DatabaseHelper.wipe_database + @cache = CacheHelper.create_test_cache + CacheHelper.wipe_cache(@cache) + # Use App.config so the service uses the same Redis database as our test cache + @config = CodePraise::App.config + end + + after do + VcrHelper.eject_vcr + CacheHelper.wipe_cache(@cache) + end + + describe 'Fetch or Request Appraisal' do + it 'HAPPY: should return cached JSON when cache hit' do + # GIVEN: a valid project in database and cached appraisal + gh_project = CodePraise::Github::ProjectMapper + .new(GITHUB_TOKEN) + .find(USERNAME, PROJECT_NAME) + CodePraise::Repository::For.entity(gh_project).create(gh_project) + + # Pre-populate cache with appraisal JSON + cache_key = "appraisal:#{USERNAME}/#{PROJECT_NAME}/" + cached_json = '{"status":"ok","data":{"path":"","subfolders":[]}}' + @cache.set(cache_key, cached_json, ttl: 86_400) + + # WHEN: we request appraisal + request = OpenStruct.new( + owner_name: USERNAME, + project_name: PROJECT_NAME, + project_fullname: "#{USERNAME}/#{PROJECT_NAME}", + folder_name: '' + ) + + result = CodePraise::Service::FetchOrRequestAppraisal.new.call( + requested: request, + request_id: 'test-123', + config: @config + ) + + # THEN: we should get success with cached JSON + _(result.success?).must_equal true + _(result.value![:cache_hit]).must_equal true + _(result.value![:cached_json]).must_equal cached_json + end + + it 'HAPPY: should return processing status when cache miss' do + # GIVEN: a valid project in database but NO cached appraisal + gh_project = CodePraise::Github::ProjectMapper + .new(GITHUB_TOKEN) + .find(USERNAME, PROJECT_NAME) + CodePraise::Repository::For.entity(gh_project).create(gh_project) + + # WHEN: we request appraisal (cache is empty) + request = OpenStruct.new( + owner_name: USERNAME, + project_name: PROJECT_NAME, + project_fullname: "#{USERNAME}/#{PROJECT_NAME}", + folder_name: '' + ) + + # Mock the queue to avoid actual SQS calls + mock_queue = Minitest::Mock.new + mock_queue.expect(:send, nil, [String]) + + CodePraise::Messaging::Queue.stub(:new, mock_queue) do + result = CodePraise::Service::FetchOrRequestAppraisal.new.call( + requested: request, + request_id: 'test-123', + config: @config + ) + + # THEN: we should get failure with processing status + _(result.failure?).must_equal true + _(result.failure.status).must_equal :processing + _(result.failure.message[:request_id]).must_equal 'test-123' + end + + mock_queue.verify + end + + it 'SAD: should not give appraisal for non-existent project' do + # GIVEN: no project exists in database + + # WHEN: we request appraisal + request = OpenStruct.new( + owner_name: USERNAME, + project_name: PROJECT_NAME, + project_fullname: "#{USERNAME}/#{PROJECT_NAME}", + folder_name: '' + ) + + result = CodePraise::Service::FetchOrRequestAppraisal.new.call( + requested: request, + request_id: 'test-123', + config: @config + ) + + # THEN: we should get failure with not_found status + _(result.failure?).must_equal true + _(result.failure.status).must_equal :not_found + end + + it 'SAD: should reject too large projects' do + # GIVEN: a project that is too large + gh_project = CodePraise::Github::ProjectMapper + .new(GITHUB_TOKEN) + .find(USERNAME, PROJECT_NAME) + + # Create a mock project that is too large + large_project = CodePraise::Entity::Project.new( + id: nil, + origin_id: gh_project.origin_id, + name: gh_project.name, + size: 100_001, # Over 100MB limit + ssh_url: gh_project.ssh_url, + http_url: gh_project.http_url, + owner: gh_project.owner, + contributors: gh_project.contributors + ) + CodePraise::Repository::For.entity(large_project).create(large_project) + + # WHEN: we request appraisal + request = OpenStruct.new( + owner_name: USERNAME, + project_name: PROJECT_NAME, + project_fullname: "#{USERNAME}/#{PROJECT_NAME}", + folder_name: '' + ) + + result = CodePraise::Service::FetchOrRequestAppraisal.new.call( + requested: request, + request_id: 'test-123', + config: @config + ) + + # THEN: we should get failure with forbidden status + _(result.failure?).must_equal true + _(result.failure.status).must_equal :forbidden + end + end +end diff --git a/spec/tests/unit/remote_cache_spec.rb b/spec/tests/unit/remote_cache_spec.rb index f98da4e..23b0fc6 100644 --- a/spec/tests/unit/remote_cache_spec.rb +++ b/spec/tests/unit/remote_cache_spec.rb @@ -3,9 +3,9 @@ require_relative '../../helpers/spec_helper' require_relative '../../helpers/cache_helper' -describe 'Unit test of Cache::Remote with fakeredis' do +describe 'Unit test of Cache::Remote' do before do - @cache = CacheHelper.create_fake_cache + @cache = CacheHelper.create_test_cache CacheHelper.wipe_cache(@cache) end @@ -47,13 +47,14 @@ end describe 'keys' do - it 'should list all keys' do + it 'should list all keys with test prefix' do @cache.set('key1', 'value1', ttl: 3600) @cache.set('key2', 'value2', ttl: 3600) keys = @cache.keys - _(keys).must_include 'key1' - _(keys).must_include 'key2' + # In test environment, keys are prefixed with 'test:' + _(keys).must_include 'test:key1' + _(keys).must_include 'test:key2' _(keys.length).must_equal 2 end end diff --git a/spec/tests/unit/worker_appraise_spec.rb b/spec/tests/unit/worker_appraise_spec.rb index b9e1779..3ce1628 100644 --- a/spec/tests/unit/worker_appraise_spec.rb +++ b/spec/tests/unit/worker_appraise_spec.rb @@ -6,7 +6,7 @@ describe 'Unit test of Worker::AppraiseProject' do before do - @cache = CacheHelper.create_fake_cache + @cache = CacheHelper.create_test_cache CacheHelper.wipe_cache(@cache) # Create test project OpenStruct (simulating deserialized JSON) diff --git a/workers/git_clone_worker.rb b/workers/git_clone_worker.rb index 28f71d1..11de166 100644 --- a/workers/git_clone_worker.rb +++ b/workers/git_clone_worker.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true require_relative '../require_app' +require_app + require_relative 'clone_monitor' require_relative 'job_reporter' require_relative 'services/appraise_project' -require_app require 'figaro' require 'shoryuken' @@ -55,7 +56,7 @@ def appraisal_request?(request) def perform_appraisal(job) gitrepo = CodePraise::GitRepo.new(job.project, Worker.config) - Worker::AppraiseProject.new.call( + ::Worker::AppraiseProject.new.call( project: job.project, folder_path: job.folder_path, config: Worker.config, From 04909efb394d2cdda278f5c23b67378ada809c53 Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Sun, 21 Dec 2025 22:31:23 +0800 Subject: [PATCH 08/18] refactor: Phase 4 cleanup - remove old service and unused representers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove deprecated code replaced by worker-based appraisal architecture: - Remove AppraiseProject service (replaced by FetchOrRequestAppraisal) - Remove ProjectFolderContributions response and representer (replaced by Value::Appraisal) - Remove Rack::Cache headers from appraisal endpoint (Redis is source of truth) - Remove old AppraiseProject integration test 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/application/controllers/app.rb | 6 +- app/application/services/appraise_project.rb | 96 --- ...roject_folder_contributions_representer.rb | 20 - .../responses/project_folder_contributions.rb | 8 - coverage/.resultset.json | 754 ++++++------------ .../services/appraise_project_spec.rb | 88 -- 6 files changed, 264 insertions(+), 708 deletions(-) delete mode 100644 app/application/services/appraise_project.rb delete mode 100644 app/presentation/representers/project_folder_contributions_representer.rb delete mode 100644 app/presentation/responses/project_folder_contributions.rb delete mode 100644 spec/tests/integration/services/appraise_project_spec.rb diff --git a/app/application/controllers/app.rb b/app/application/controllers/app.rb index 285b57e..0d3a9ad 100644 --- a/app/application/controllers/app.rb +++ b/app/application/controllers/app.rb @@ -32,10 +32,8 @@ class App < Roda routing.on String, String do |owner_name, project_name| # GET /projects/{owner_name}/{project_name}[/folder_namepath/] routing.get do - App.configure :production do - response.cache_control public: true, max_age: 300 - end - + # Appraisal results cached in Redis by worker (1-day TTL) + # No HTTP caching needed - Redis is the source of truth request_id = [request.env, request.path, Time.now.to_f].hash path_request = Request::ProjectPath.new( diff --git a/app/application/services/appraise_project.rb b/app/application/services/appraise_project.rb deleted file mode 100644 index 2582504..0000000 --- a/app/application/services/appraise_project.rb +++ /dev/null @@ -1,96 +0,0 @@ -# frozen_string_literal: true - -require 'dry/transaction' - -module CodePraise - module Service - # Analyzes contributions to a project - class AppraiseProject - include Dry::Transaction - - step :find_project_details - step :check_project_eligibility - step :request_cloning_worker - step :appraise_contributions - - private - - # rubocop:disable Lint/UselessConstantScoping - NO_PROJ_ERR = 'Project not found' - DB_ERR = 'Having trouble accessing the database' - CLONE_ERR = 'Could not clone this project' - TOO_LARGE_ERR = 'Project is too large to analyze' - NO_FOLDER_ERR = 'Could not find that folder' - PROCESSING_MSG = 'Processing the appraisal request' - # rubocop:enable Lint/UselessConstantScoping - - # input hash keys expected: :project, :requested, :config - def find_project_details(input) - input[:project] = Repository::For.klass(Entity::Project).find_full_name( - input[:requested].owner_name, input[:requested].project_name - ) - - if input[:project] - Success(input) - else - Failure(Response::ApiResult.new(status: :not_found, message: NO_PROJ_ERR)) - end - rescue StandardError - Failure(Response::ApiResult.new(status: :internal_error, message: DB_ERR)) - end - - def check_project_eligibility(input) - if input[:project].too_large? - Failure(Response::ApiResult.new(status: :forbidden, message: TOO_LARGE_ERR)) - else - input[:gitrepo] = GitRepo.new(input[:project], input[:config]) - Success(input) - end - end - - def request_cloning_worker(input) - return Success(input) if input[:gitrepo].exists_locally? - - Messaging::Queue.new(App.config.CLONE_QUEUE_URL, App.config) - .send(clone_request_json(input)) - - Failure(Response::ApiResult.new( - status: :processing, - message: { request_id: input[:request_id], msg: PROCESSING_MSG } - )) - rescue StandardError => e - log_error(e) - Failure(Response::ApiResult.new(status: :internal_error, message: CLONE_ERR)) - end - - def appraise_contributions(input) - input[:folder] = Mapper::Contributions - .new(input[:gitrepo]).for_folder(input[:requested].folder_name) - - appraisal = Response::ProjectFolderContributions.new(input[:project], input[:folder]) - Success(Response::ApiResult.new(status: :ok, message: appraisal)) - rescue StandardError - # App.logger.error "Could not find: #{full_request_path(input)}" - Failure(Response::ApiResult.new(status: :not_found, message: NO_FOLDER_ERR)) - end - - # Helper methods for steps - - def full_request_path(input) - [input[:requested].owner_name, - input[:requested].project_name, - input[:requested].folder_name].join('/') - end - - def log_error(error) - App.logger.error [error.inspect, error.backtrace].flatten.join("\n") - end - - def clone_request_json(input) - Response::CloneRequest.new(input[:project], input[:request_id]) - .then { Representer::CloneRequest.new(_1) } - .then(&:to_json) - end - end - end -end diff --git a/app/presentation/representers/project_folder_contributions_representer.rb b/app/presentation/representers/project_folder_contributions_representer.rb deleted file mode 100644 index cbf599a..0000000 --- a/app/presentation/representers/project_folder_contributions_representer.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -require 'ostruct' -require 'roar/decorator' -require 'roar/json' - -require_relative 'folder_contributions_representer' -require_relative 'project_representer' - -module CodePraise - module Representer - # Represents folder summary about repo's folder - class ProjectFolderContributions < Roar::Decorator - include Roar::JSON - - property :project, extend: Representer::Project, class: OpenStruct - property :folder, extend: Representer::FolderContributions, class: OpenStruct - end - end -end diff --git a/app/presentation/responses/project_folder_contributions.rb b/app/presentation/responses/project_folder_contributions.rb deleted file mode 100644 index 3e97bde..0000000 --- a/app/presentation/responses/project_folder_contributions.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -module CodePraise - module Response - # Contributions for a folder of a project - ProjectFolderContributions = Struct.new(:project, :folder) - end -end diff --git a/coverage/.resultset.json b/coverage/.resultset.json index b0ee463..ad4af6f 100644 --- a/coverage/.resultset.json +++ b/coverage/.resultset.json @@ -16,7 +16,7 @@ 1, null, 1, - 64, + 61, null, null ] @@ -128,7 +128,7 @@ null, null, 1, - 4928, + 2473, null, null, null, @@ -136,7 +136,7 @@ null, null, 1, - 4513, + 2264, null, null, null, @@ -159,12 +159,12 @@ 1, null, 1, - 124, - 124, + 63, + 63, null, null, 1, - 247, + 125, null, null, 1, @@ -172,11 +172,11 @@ null, null, 1, - 247, + 125, null, - 247, + 125, null, - 3433, + 1724, null, null, null, @@ -246,7 +246,7 @@ null, null, 1, - 3935, + 2019, null, null, 1, @@ -261,7 +261,7 @@ null, null, 1, - 3433, + 1724, null, null, 1, @@ -269,7 +269,7 @@ null, null, 1, - 3433, + 1724, null, null, null, @@ -397,12 +397,12 @@ 1, null, 1, - 38, + 23, null, - 38, - 38, - 38, - 38, + 23, + 23, + 23, + 23, null, null, 1, @@ -426,7 +426,7 @@ null, null, 1, - 24, + 12, null, null, 1, @@ -436,33 +436,33 @@ 1, null, 1, - 384, + 193, null, null, 1, - 38, + 23, null, null, 1, - 38, - 254, - 124, + 23, + 128, + 63, null, null, 1, - 38, + 23, null, - 130, - 130, + 65, + 65, null, null, null, 1, - 38, - 28, + 23, + 14, null, null, - 66, + 37, null, null, null, @@ -488,14 +488,14 @@ 1, null, 1, - 3433, - 3433, - 3433, - 3433, + 1724, + 1724, + 1724, + 1724, null, null, 1, - 3433, + 1724, null, null, null, @@ -511,14 +511,14 @@ null, 1, 1, - 104, + 60, null, null, null, null, 1, 1, - 1241, + 623, null, null, null, @@ -638,12 +638,12 @@ null, null, 1, - 13, + 11, null, null, 1, null, - 18, + 17, null, null, null, @@ -669,7 +669,7 @@ 1, null, 1, - 69, + 65, null, null, null, @@ -685,7 +685,7 @@ 1, null, 1, - 240, + 120, null, null, null, @@ -693,15 +693,15 @@ null, 1, null, - 240, - 240, + 120, + 120, null, - 240, + 120, null, - 742, - 742, + 371, + 371, null, - 742, + 371, null, 0, 0, @@ -710,20 +710,20 @@ 0, 0, null, - 742, + 371, null, - 58, - 58, - 58, + 29, + 29, + 29, null, null, - 684, - 684, - 684, + 342, + 342, + 342, null, null, null, - 240, + 120, null, null, null, @@ -744,8 +744,8 @@ 1, null, 1, - 364, - 364, + 183, + 183, null, null, null, @@ -776,25 +776,25 @@ null, null, 1, - 4117, - 4117, - 4117, - 4117, + 2066, + 2066, + 2066, + 2066, null, null, 1, - 240, + 120, null, null, null, - 240, - 310, + 120, + 155, null, null, - 240, - 684, - 1426, - 684, + 120, + 342, + 713, + 342, null, null, null, @@ -810,7 +810,7 @@ 1, null, 1, - 8622, + 4329, null, null, null, @@ -844,38 +844,38 @@ 1, null, 1, - 476, - 476, + 278, + 278, null, null, 1, - 7807, + 3996, null, null, 1, - 3908, + 2001, null, - 3899, + 1995, null, null, 1, - 228, + 152, null, null, 1, null, - 130, + 65, null, - 130, - 130, + 65, + 65, null, null, 1, null, 1, - 476, - 476, - 476, + 278, + 278, + 278, null, null, null, @@ -997,7 +997,7 @@ 1, null, 1, - 68, + 64, null, null, null, @@ -1045,11 +1045,11 @@ null, null, 1, - 32, + 30, null, null, 1, - 17, + 16, null, null, null, @@ -1073,9 +1073,9 @@ null, null, 1, - 128, + 120, null, - 128, + 120, null, null, null, @@ -1084,13 +1084,13 @@ null, null, 1, - 32, - 96, + 30, + 90, null, null, null, 1, - 68, + 64, null, null, null, @@ -1116,12 +1116,12 @@ null, null, null, - 31, + 29, null, null, null, null, - 31, + 29, null, null, 1, @@ -1132,7 +1132,7 @@ null, null, 1, - 17, + 16, null, null, 1, @@ -1141,21 +1141,21 @@ null, null, 1, - 17, - 17, + 16, + 16, null, null, 1, - 17, + 16, null, - 17, - 17, + 16, + 16, null, null, 1, - 65, + 61, null, - 32, + 30, null, null, null, @@ -1166,21 +1166,21 @@ null, 1, 1, - 17, + 16, null, null, 1, - 17, + 16, null, null, 1, - 17, + 16, null, - 17, - 17, + 16, + 16, null, - 17, - 51, + 16, + 48, null, null, null, @@ -1201,57 +1201,57 @@ 1, null, 1, - 128, - 128, - 128, - 128, + 66, + 66, + 66, + 66, null, null, 1, - 3, - 3, - 3, + 2, + 2, + 2, null, null, 1, - 125, - 125, - 125, + 64, + 64, + 64, null, null, 1, - 125, - 125, + 64, + 64, null, null, 1, - 3, - 3, + 2, + 2, null, null, 1, - 3, - 3, + 2, + 2, null, null, 1, - 256, + 132, null, null, 1, - 128, + 66, null, null, null, null, null, 1, - 124, + 63, null, null, 1, - 2, - 10, + 1, + 5, null, null, null, @@ -1268,12 +1268,12 @@ null, 1, 1, - 3433, - 3433, + 1724, + 1724, null, null, 1, - 3433, + 1724, null, null, null, @@ -1283,7 +1283,7 @@ 1, null, 1, - 3433, + 1724, null, null, null, @@ -1299,13 +1299,13 @@ null, 1, 1, - 3, + 2, null, null, 1, - 3, + 2, null, - 3, + 2, null, null, null, @@ -1313,17 +1313,17 @@ null, 1, 1, - 3, - 124, + 2, + 63, null, null, null, null, 1, 1, - 124, - 124, - 124, + 63, + 63, + 63, null, null, null, @@ -1342,11 +1342,11 @@ null, 1, 1, - 124, + 63, null, null, 1, - 124, + 63, null, null, null, @@ -1355,20 +1355,20 @@ 1, null, 1, - 248, + 126, null, null, 1, - 3433, + 1724, null, null, 1, - 124, + 63, null, null, 1, - 124, - 3433, + 63, + 1724, null, null, null, @@ -1376,9 +1376,9 @@ null, 1, 1, - 3433, - 3433, - 3433, + 1724, + 1724, + 1724, null, null, 1, @@ -1388,14 +1388,14 @@ 1, null, 1, - 3433, + 1724, null, null, null, 1, null, 1, - 3433, + 1724, null, null, null, @@ -1416,20 +1416,20 @@ null, null, 1, - 3, - 3, + 2, + 2, null, null, 1, - 3, + 2, null, null, null, null, null, 1, - 3, - 124, + 2, + 63, null, null, null, @@ -1449,13 +1449,13 @@ 1, null, 1, - 124, - 3433, + 63, + 1724, null, null, 1, - 124, - 124, + 63, + 63, null, 0, 0, @@ -1463,28 +1463,28 @@ null, null, 1, - 3433, + 1724, null, - 3433, + 1724, null, null, null, - 3433, - 36432, - 36432, + 1724, + 18292, + 18292, null, null, - 3433, + 1724, null, null, 1, - 3433, - 3433, - 3433, + 1724, + 1724, + 1724, null, null, 1, - 36432, + 18292, null, null, null, @@ -1506,17 +1506,17 @@ 1, null, 1, - 3, - 3, - 3, + 2, + 2, + 2, null, null, 1, - 3, + 2, null, - 186, + 124, null, - 6, + 4, null, null, 1, @@ -1533,13 +1533,13 @@ null, null, 1, - 124, + 63, null, null, 1, null, 1, - 3, + 2, null, 2, null, @@ -1555,10 +1555,10 @@ null, null, 1, - 3, - 124, - 124, - 124, + 2, + 63, + 63, + 63, null, null, null, @@ -1577,11 +1577,11 @@ 1, null, 1, - 124, + 63, null, null, 1, - 124, + 63, null, null, null, @@ -1605,13 +1605,13 @@ null, null, 1, - 5, - 5, - 5, + 3, + 3, + 3, null, null, 1, - 3, + 2, null, null, 1, @@ -1619,14 +1619,14 @@ null, null, 1, - 10, + 6, null, null, 1, - 2, - 2, + 1, + 1, null, - 12, + 6, null, null, null @@ -1653,32 +1653,32 @@ 1, null, 1, - 5, - 5, + 3, + 3, null, null, 1, - 12, - 2, + 6, + 1, null, null, 1, - 3, + 2, null, - 3, - 3, - 276, + 2, + 2, + 184, null, null, null, null, 1, - 7, - 14, + 5, + 10, null, null, 1, - 20, + 13, null, null, 1, @@ -1692,7 +1692,7 @@ 1, null, 1, - 10, + 7, null, null, null, @@ -1717,15 +1717,15 @@ 1, null, 1, - 5, + 3, null, null, 1, - 5, + 3, null, null, 1, - 2, + 1, null, null, null, @@ -1770,45 +1770,45 @@ 1, null, 1, - 57, + 55, null, null, 1, - 31, + 30, null, null, null, null, 1, - 26, + 25, null, null, null, 1, 1, - 31, - 31, + 30, + 30, null, null, 1, - 31, + 30, null, null, null, null, 1, 1, - 57, + 55, null, null, 1, - 57, + 55, null, null, null, null, - 56, - 56, + 54, + 54, null, null, null, @@ -1824,7 +1824,7 @@ null, null, 1, - 56, + 54, null, null, 1, @@ -1846,29 +1846,29 @@ null, 1, 1, - 26, - 26, - 26, + 25, + 25, + 25, null, null, 1, - 26, - 78, + 25, + 75, null, null, null, 1, - 104, + 100, null, null, null, 1, 1, - 104, + 100, null, null, 1, - 104, + 100, null, null, null, @@ -1879,15 +1879,15 @@ 1, null, 1, - 104, + 100, null, null, 1, - 104, + 100, null, null, 1, - 104, + 100, null, null, null, @@ -1906,31 +1906,31 @@ null, 1, 1, - 31, - 31, - 31, + 30, + 30, + 30, null, null, 1, - 31, - 26, + 30, + 25, null, null, 1, - 26, + 25, null, null, null, 1, 1, - 26, - 26, + 25, + 25, null, null, null, null, 1, - 26, + 25, null, null, null, @@ -1943,31 +1943,31 @@ null, null, 1, - 26, + 25, null, null, 1, - 26, + 25, null, null, 1, - 26, + 25, null, null, 1, - 26, + 25, null, null, 1, - 26, + 25, null, null, 1, - 26, + 25, null, null, 1, - 26, + 25, null, null, null, @@ -2389,30 +2389,6 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/project_folder_contributions_representer.rb": { - "lines": [ - null, - null, - 1, - 1, - 1, - null, - 1, - 1, - null, - 1, - 1, - null, - 1, - 1, - null, - 1, - 1, - null, - null, - null - ] - }, "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/projects_representer.rb": { "lines": [ null, @@ -2456,9 +2432,9 @@ null, 1, 1, - 32, + 30, null, - 31, + 29, null, null, null, @@ -2481,18 +2457,6 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/project_folder_contributions.rb": { - "lines": [ - null, - null, - 1, - 1, - null, - 1, - null, - null - ] - }, "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/projects_list.rb": { "lines": [ null, @@ -2541,8 +2505,6 @@ 13, null, 10, - 8, - 0, null, null, 8, @@ -2789,106 +2751,6 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/appraise_project.rb": { - "lines": [ - null, - null, - 1, - null, - 1, - 1, - null, - 1, - 1, - null, - 1, - 1, - 1, - 1, - null, - 1, - null, - null, - 1, - 1, - 1, - 1, - 1, - 1, - null, - null, - null, - 1, - 2, - null, - null, - null, - 2, - 1, - null, - 1, - null, - null, - 0, - null, - null, - 1, - 1, - 0, - null, - 1, - 1, - null, - null, - null, - 1, - 1, - null, - 0, - null, - null, - 0, - null, - null, - null, - null, - 0, - 0, - null, - null, - 1, - 1, - null, - null, - 1, - 1, - null, - null, - 0, - null, - null, - null, - null, - 1, - 0, - null, - null, - null, - null, - 1, - 0, - null, - null, - 1, - 0, - 0, - null, - null, - null, - null, - null - ] - }, "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/fetch_or_request_appraisal.rb": { "lines": [ null, @@ -3054,23 +2916,23 @@ 1, null, 1, - 8, - 8, - 8, - 8, - 8, - 8, + 7, + 7, + 7, + 7, + 7, + 7, null, null, null, null, 1, - 34, - 2419, - 2419, + 32, + 2297, + 2297, null, null, - 34, + 32, null, null, null, @@ -3079,7 +2941,7 @@ null, null, 1, - 34, + 32, null, null ] @@ -3093,10 +2955,10 @@ null, 1, null, - 26, - 26, - 26, - 26, + 24, + 24, + 24, + 24, null, null ] @@ -3435,98 +3297,6 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/integration/services/appraise_project_spec.rb": { - "lines": [ - null, - null, - 1, - 1, - 1, - null, - 1, - null, - 1, - 1, - null, - 1, - 2, - null, - null, - 1, - 2, - null, - null, - 1, - 1, - 2, - null, - null, - 1, - null, - 1, - null, - null, - 1, - 1, - 1, - null, - null, - 1, - null, - null, - null, - null, - null, - null, - 1, - null, - null, - null, - null, - null, - 1, - 1, - 1, - 1, - null, - 1, - 1, - 1, - null, - null, - 1, - null, - null, - 1, - null, - null, - 1, - null, - null, - null, - 1, - null, - null, - null, - 1, - null, - null, - null, - null, - null, - null, - 1, - null, - null, - null, - null, - null, - 1, - null, - null, - null - ] - }, "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/integration/services/fetch_or_request_appraisal_spec.rb": { "lines": [ null, @@ -4508,6 +4278,6 @@ ] } }, - "timestamp": 1766326644 + "timestamp": 1766327327 } } diff --git a/spec/tests/integration/services/appraise_project_spec.rb b/spec/tests/integration/services/appraise_project_spec.rb deleted file mode 100644 index e9bd6a2..0000000 --- a/spec/tests/integration/services/appraise_project_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -require_relative '../../../helpers/spec_helper' -require_relative '../../../helpers/vcr_helper' -require_relative '../../../helpers/database_helper' - -require 'ostruct' - -describe 'AppraiseProject Service Integration Test' do - VcrHelper.setup_vcr - - before do - VcrHelper.configure_vcr_for_github(recording: :none) - end - - after do - VcrHelper.eject_vcr - end - - describe 'Appraise a Project' do - before do - DatabaseHelper.wipe_database - end - - it 'HAPPY: should give contributions for a folder of an existing project' do - # GIVEN: a valid project that exists locally - gh_project = CodePraise::Github::ProjectMapper - .new(GITHUB_TOKEN) - .find(USERNAME, PROJECT_NAME) - CodePraise::Repository::For.entity(gh_project).create(gh_project) - gitrepo = CodePraise::GitRepo.new(gh_project, CodePraise::App.config) - gitrepo.clone_locally unless gitrepo.exists_locally? - - # WHEN: we request to appraise the project - request = OpenStruct.new( - owner_name: USERNAME, - project_name: PROJECT_NAME, - project_fullname: "#{USERNAME}/#{PROJECT_NAME}", - folder_name: '' - ) - - appraisal = CodePraise::Service::AppraiseProject.new.call( - requested: request, - config: CodePraise::App.config - ).value!.message - - # THEN: we should get an appraisal - folder = appraisal[:folder] - _(folder).must_be_kind_of CodePraise::Entity::FolderContributions - _(folder.subfolders.count).must_equal 10 - _(folder.base_files.count).must_equal 2 - - first_file = folder.base_files.first - _(%w[init.rb README.md]).must_include first_file.file_path.filename - _(folder.subfolders.first.path.size).must_be :>, 0 - - subfolders_plus_basefiles = - folder.subfolders.map(&:credit_share).reduce(&:+) + - folder.base_files.map(&:credit_share).reduce(&:+) - - _(subfolders_plus_basefiles.share.values.sort) - .must_equal(folder.credit_share.share.values.sort) - - _(subfolders_plus_basefiles.contributors.map(&:email).sort) - .must_equal(folder.credit_share.contributors.map(&:email).sort) - end - - it 'SAD: should not give contributions for non-existent project' do - # GIVEN: no project exists locally - - # WHEN: we request to appraise the project - request = OpenStruct.new( - owner_name: USERNAME, - project_name: PROJECT_NAME, - project_fullname: "#{USERNAME}/#{PROJECT_NAME}", - folder_name: '' - ) - - result = CodePraise::Service::AppraiseProject.new.call( - requested: request, - config: CodePraise::App.config - ) - - # THEN: we should get failure - _(result.failure?).must_equal true - end - end -end From b39262978a59a1b818315d28aeee8e304a613c17 Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Sun, 21 Dec 2025 23:42:46 +0800 Subject: [PATCH 09/18] refactor: move contributions domain to worker-only location MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move git blame analysis domain objects to workers/domain/ since they're only used by the worker, not the API: - Create require_worker.rb parallel to require_app.rb for worker code loading - Move entities: FolderContributions, FileContributions, LineContribution, Contributor - Move values: FilePath, CodeLanguage, CreditShare, Contributors - Move lib: ContributionsCalculator, Types - Update Value::Appraisal to use Nominal(Object) type for folder attribute - Update spec_helper to load worker domain for tests The API only needs Value::Appraisal (for cache key format). All heavy contribution entities are now worker-only concerns. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/domain/contributions/values/appraisal.rb | 9 +- coverage/.resultset.json | 2012 +++++++++-------- require_worker.rb | 17 + spec/helpers/spec_helper.rb | 5 +- .../contributions/entities/contributor.rb | 0 .../entities/file_contributions.rb | 0 .../entities/folder_contributions.rb | 0 .../entities/line_contribution.rb | 0 .../lib/contributions_calculator.rb | 0 .../domain/contributions/lib/types.rb | 0 .../contributions/values/code_language.rb | 0 .../contributions/values/contributors.rb | 0 .../contributions/values/credit_share.rb | 0 .../domain/contributions/values/file_path.rb | 0 workers/git_clone_worker.rb | 6 +- 15 files changed, 1049 insertions(+), 1000 deletions(-) create mode 100644 require_worker.rb rename {app => workers}/domain/contributions/entities/contributor.rb (100%) rename {app => workers}/domain/contributions/entities/file_contributions.rb (100%) rename {app => workers}/domain/contributions/entities/folder_contributions.rb (100%) rename {app => workers}/domain/contributions/entities/line_contribution.rb (100%) rename {app => workers}/domain/contributions/lib/contributions_calculator.rb (100%) rename {app => workers}/domain/contributions/lib/types.rb (100%) rename {app => workers}/domain/contributions/values/code_language.rb (100%) rename {app => workers}/domain/contributions/values/contributors.rb (100%) rename {app => workers}/domain/contributions/values/credit_share.rb (100%) rename {app => workers}/domain/contributions/values/file_path.rb (100%) diff --git a/app/domain/contributions/values/appraisal.rb b/app/domain/contributions/values/appraisal.rb index ebf729d..5137f53 100644 --- a/app/domain/contributions/values/appraisal.rb +++ b/app/domain/contributions/values/appraisal.rb @@ -3,9 +3,11 @@ require 'dry-types' require 'dry-struct' -# Require dependencies that load after this file alphabetically +# Require project entity (always available in API) require_relative '../../projects/entities/project' -require_relative '../entities/folder_contributions' + +# Note: FolderContributions is loaded by the worker via workers/domain/contributions/ +# The folder attribute uses duck typing - any object responding to expected methods module CodePraise module Value @@ -28,7 +30,8 @@ class Appraisal < Dry::Struct attribute :folder_path, Strict::String # Folder contributions (present on success, nil on error) - attribute :folder, Instance(Entity::FolderContributions).optional + # Uses Nominal type to accept any FolderContributions-like object + attribute :folder, Nominal(Object).optional # Error details (present on error, nil on success) attribute :error_type, Strict::String.optional diff --git a/coverage/.resultset.json b/coverage/.resultset.json index ad4af6f..937aabf 100644 --- a/coverage/.resultset.json +++ b/coverage/.resultset.json @@ -16,7 +16,28 @@ 1, null, 1, - 61, + 51, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/require_worker.rb": { + "lines": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 2, + null, + 1, + 10, null, null ] @@ -110,480 +131,433 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/entities/contributor.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/values/appraisal.rb": { "lines": [ null, null, 1, 1, null, - 1, + null, 1, null, + null, + null, + null, 1, 1, null, + null, 1, 1, null, null, 1, - 2473, - null, + 1, null, null, 1, null, null, 1, - 2264, null, null, + 1, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/entities/file_contributions.rb": { - "lines": [ null, null, 1, - 1, null, - 1, - 1, null, 1, 1, null, 1, - null, - 1, - 63, - 63, + 12, null, null, 1, - 125, + 14, null, null, 1, - 0, + 2, null, null, 1, - 125, - null, - 125, - null, - 1724, + 4, null, null, null, 1, - 0, + 7, null, null, - 1, null, - 1, - 1, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/lib/contributions_calculator.rb": { - "lines": [ null, null, - 1, - 1, null, - 1, - 1, - 0, null, null, 1, - 0, + 14, null, null, - 1, - 0, null, null, - 1, - 0, null, null, - 1, - 0, null, - 0, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/values/code_language.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/projects/entities/project.rb": { "lines": [ - null, null, null, 1, 1, - 1, - 1, - 1, - null, null, 1, null, 1, - 8, - null, - null, - 1, - 2019, - null, - null, 1, - 0, null, - null, - 1, 1, - null, 1, - 9, - null, null, 1, - 1724, - null, null, 1, - 0, - null, - null, 1, - 1724, - null, - null, - null, 1, 1, - null, 1, 1, - 8, - 8, - 8, - null, - 8, - null, - null, - null, 1, 1, null, - null, - null, - 1, 1, + 25, null, - 1, null, 1, - 1, - 1, + 11, null, null, 1, - 1, null, - 1, + 17, null, - 1, - 1, - 1, null, null, - 1, - 1, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/projects/entities/member.rb": { + "lines": [ null, - 1, null, 1, 1, - 1, null, - null, - 1, 1, - null, 1, null, 1, 1, null, - null, 1, 1, - null, - 1, - null, 1, 1, null, - null, - 1, 1, + 65, null, - 1, null, - 1, - 1, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/cache/local_cache.rb": { + "lines": [ null, - 1, - 1, null, 1, null, 1, 1, null, - null, 1, 1, + 5, + 5, null, - 1, null, 1, - 1, - null, + 13, null, - 1, - 1, null, 1, + 2, null, - 1, - 1, null, 1, null, - null, 1, + 5, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/entities/folder_contributions.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/cache/redis_cache.rb": { "lines": [ null, null, 1, - 1, null, 1, 1, null, - 1, null, - 1, - 23, null, - 23, - 23, - 23, - 23, null, null, 1, - 0, - null, - null, 1, - 0, + 48, + 48, null, null, - 1, - 0, null, null, - 1, - 0, null, null, 1, - 0, - null, + 8, null, - 1, - 12, null, null, - 1, - 0, null, null, 1, + 11, null, - 1, - 193, null, null, - 1, - 23, null, null, 1, - 23, - 128, - 63, + 3, null, null, 1, - 23, + 61, null, - 65, - 65, null, + 1, + 70, null, null, 1, - 23, - 14, - null, null, - 37, + 1, + 22, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/entities/line_contribution.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/orm/member_orm.rb": { "lines": [ null, null, 1, - 1, - null, - 1, null, 1, 1, null, 1, 1, - 1, null, - 1, + null, null, 1, - 1724, - 1724, - 1724, - 1724, + null, + null, null, null, 1, - 1724, + null, + 1, + 64, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/lib/types.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/orm/project_orm.rb": { "lines": [ null, null, 1, + null, + 1, 1, null, 1, 1, - 60, null, null, + 1, null, null, - 1, - 1, - 623, null, null, + 1, + null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/values/appraisal.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/repositories/for.rb": { "lines": [ null, null, 1, 1, null, - null, 1, 1, null, 1, - 1, null, - null, - 1, 1, null, null, + null, 1, - 1, + 30, null, null, 1, + 16, null, null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/repositories/members.rb": { + "lines": [ + null, + null, + 1, 1, null, + 1, + 1, + 0, + null, null, 1, + 0, null, null, 1, + 120, + null, + 120, + null, + null, + null, + null, + null, null, null, 1, + 30, + 90, + null, + null, + null, 1, + 64, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/repositories/projects.rb": { + "lines": [ + null, null, 1, - 12, null, + 1, + 1, null, 1, - 14, + 1, + 0, null, null, 1, - 2, + null, + null, + null, + null, + 29, + null, + null, + null, + null, + 29, null, null, 1, + 5, + 4, 4, null, null, null, 1, - 7, + 16, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 16, + 16, + null, + null, + 1, + 16, null, + 16, + 16, + null, + null, + 1, + 61, null, + 30, null, null, null, @@ -593,11 +567,22 @@ null, null, 1, - 14, + 1, + 16, + null, + null, + 1, + 16, null, null, + 1, + 16, null, + 16, + 16, null, + 16, + 48, null, null, null, @@ -607,7 +592,7 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/projects/entities/project.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/gateway/git_command.rb": { "lines": [ null, null, @@ -615,42 +600,68 @@ 1, null, 1, - null, - 1, 1, null, 1, - 1, + 66, + 66, + 66, + 66, null, - 1, null, 1, + 2, + 2, + 2, + null, + null, 1, + 64, + 64, + 64, + null, + null, 1, + 64, + 64, + null, + null, 1, + 2, + 2, + null, + null, 1, + 2, + 2, + null, + null, 1, - 1, - 1, + 132, + null, null, 1, - 25, + 66, + null, + null, + null, null, null, 1, - 11, + 63, null, null, 1, + 1, + 5, null, - 17, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/projects/entities/member.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/blame_contributor.rb": { "lines": [ null, null, @@ -659,71 +670,62 @@ null, 1, 1, + 1724, + 1724, null, - 1, - 1, null, 1, - 1, - 1, + 1724, + null, + null, + null, + null, + null, + null, 1, null, 1, - 65, + 1724, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/values/contributors.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/contributions_mapper.rb": { "lines": [ - 1, - 1, + null, null, 1, 1, null, 1, - 120, - null, - null, - null, - null, - null, 1, + 2, null, - 120, - 120, null, - 120, + 1, + 2, null, - 371, - 371, + 2, null, - 371, null, - 0, - 0, - 0, - 0, - 0, - 0, null, - 371, null, - 29, - 29, - 29, null, + 1, + 1, + 2, + 63, null, - 342, - 342, - 342, null, null, null, - 120, + 1, + 1, + 63, + 63, + 63, null, null, null, @@ -731,251 +733,267 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/values/credit_share.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/file_contributions_mapper.rb": { "lines": [ null, null, 1, - 1, null, 1, + 1, null, 1, 1, + 63, + null, null, 1, - 183, - 183, + 63, null, null, null, - 1, - 0, null, null, 1, - 0, - null, null, 1, - 0, + 126, null, null, 1, - 0, + 1724, null, null, 1, + 63, + null, null, 1, - 0, + 63, + 1724, + null, null, null, - 1, - 0, null, null, 1, - 2066, - 2066, - 2066, - 2066, + 1, + 1724, + 1724, + 1724, null, null, 1, - 120, + 1, + 1, + 1, + 1, null, + 1, + 1724, null, null, - 120, - 155, null, + 1, null, - 120, - 342, - 713, - 342, + 1, + 1724, null, null, null, - 1, - 7, - 3, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/folder_contributions_mapper.rb": { + "lines": [ null, null, 1, - 3, - null, null, 1, + 1, null, 1, - 4329, + 1, null, null, + 1, + 2, + 2, null, null, 1, - 0, - 0, + 2, null, - 0, null, null, null, null, + 1, + 2, + 63, + null, + null, + null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/values/file_path.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/porcelain_parser.rb": { "lines": [ null, null, 1, + 1, null, 1, 1, - null, 1, null, 1, + 63, + 1724, null, - 1, null, 1, - 278, - 278, + 63, + 63, null, + 0, + 0, null, - 1, - 3996, null, null, 1, - 2001, + 1724, null, - 1995, + 1724, null, null, - 1, - 152, - null, null, - 1, + 1724, + 18292, + 18292, null, - 65, null, - 65, - 65, + 1724, null, null, 1, + 1724, + 1724, + 1724, null, - 1, - 278, - 278, - 278, null, + 1, + 18292, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/cache/local_cache.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/blame_reporter.rb": { "lines": [ null, null, 1, null, 1, - 1, null, 1, 1, - 5, - 5, - null, null, 1, - 13, - null, + 1, null, 1, 2, + 2, + 2, null, null, 1, + 2, null, - 1, - 5, - null, - null, + 124, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/cache/redis_cache.rb": { - "lines": [ + 4, null, null, 1, + 0, + 0, + 0, null, - 1, - 1, + 0, null, null, null, + 1, + 0, null, null, 1, - 1, - 48, - 48, + 63, null, null, + 1, null, + 1, + 2, null, + 2, null, null, 1, - 8, + 0, + null, null, null, + 1, + 0, null, null, null, 1, - 11, + 2, + 63, + 63, + 63, null, null, null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/repo_file.rb": { + "lines": [ null, null, 1, - 3, + 1, null, + 1, + 1, null, 1, - 61, + 63, null, null, 1, - 70, + 63, null, null, - 1, null, - 1, - 22, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/orm/member_orm.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/git_repo.rb": { "lines": [ null, null, @@ -983,28 +1001,40 @@ null, 1, 1, - null, + 1, 1, 1, null, null, + 1, + 3, + 3, + 3, + null, null, 1, + 2, null, null, + 1, + 0, null, null, 1, + 6, + null, null, 1, - 64, + 1, + 1, null, + 6, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/orm/project_orm.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/local_repo.rb": { "lines": [ null, null, @@ -1012,92 +1042,124 @@ null, 1, 1, + 1, + null, + 1, + null, + null, null, 1, 1, + 1, null, + 1, null, 1, + 3, + 3, null, null, + 1, + 6, + 1, null, null, 1, + 2, null, + 2, + 2, + 184, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/repositories/for.rb": { - "lines": [ null, null, - 1, - 1, null, 1, - 1, + 5, + 10, null, - 1, null, 1, + 13, null, null, + 1, + 0, + null, null, 1, - 30, + 0, null, null, 1, - 16, + null, + 1, + 7, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/repositories/members.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/remote_repo.rb": { "lines": [ null, null, 1, + null, + 1, 1, null, + null, + null, + null, + null, + null, 1, 1, - 0, + null, + 1, + 3, null, null, 1, - 0, + 3, null, null, 1, - 120, + 1, + null, null, - 120, null, null, null, null, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/repo_store.rb": { + "lines": [ null, null, 1, - 30, - 90, + 1, null, + 1, + 1, + 11, + 14, null, null, 1, - 64, + 14, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/repositories/projects.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/github/gateways/github_api.rb": { "lines": [ null, null, @@ -1108,57 +1170,47 @@ null, 1, 1, - 0, - null, null, 1, + 55, null, null, + 1, + 30, null, null, - 29, - null, - null, - null, - null, - 29, null, null, 1, - 5, - 4, - 4, + 25, null, null, null, 1, - 16, + 1, + 30, + 30, null, null, 1, - 0, - 0, + 30, null, null, - 1, - 16, - 16, null, null, 1, - 16, - null, - 16, - 16, + 1, + 55, null, null, 1, - 61, + 55, null, - 30, null, null, null, + 54, + 54, null, null, null, @@ -1166,23 +1218,19 @@ null, 1, 1, - 16, - null, - null, 1, - 16, null, null, 1, - 16, null, - 16, - 16, null, - 16, - 48, + null, + 1, + 54, null, null, + 1, + 4, null, null, null, @@ -1190,68 +1238,58 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/gateway/git_command.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/github/mappers/member_mapper.rb": { "lines": [ null, null, 1, - 1, null, 1, - 1, null, 1, - 66, - 66, - 66, - 66, - null, - null, 1, - 2, - 2, - 2, + 25, + 25, + 25, null, null, 1, - 64, - 64, - 64, - null, + 25, + 75, null, - 1, - 64, - 64, null, null, 1, - 2, - 2, + 100, + null, null, null, 1, - 2, - 2, + 1, + 100, null, null, 1, - 132, + 100, null, null, - 1, - 66, null, null, null, null, null, 1, - 63, + null, + 1, + 100, null, null, 1, + 100, + null, + null, 1, - 5, + 100, null, null, null, @@ -1259,71 +1297,79 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/blame_contributor.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/github/mappers/project_mapper.rb": { "lines": [ null, null, 1, + null, + 1, 1, null, 1, 1, - 1724, - 1724, + 30, + 30, + 30, null, null, 1, - 1724, - null, + 30, + 25, null, null, + 1, + 25, null, null, null, 1, + 1, + 25, + 25, + null, + null, + null, null, 1, - 1724, + 25, null, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/contributions_mapper.rb": { - "lines": [ null, null, - 1, - 1, null, - 1, - 1, - 2, null, null, - 1, - 2, null, - 2, null, null, + 1, + 25, null, null, + 1, + 25, + null, null, 1, + 25, + null, + null, 1, - 2, - 63, + 25, null, null, + 1, + 25, null, null, 1, + 25, + null, + null, 1, - 63, - 63, - 63, + 25, null, null, null, @@ -1331,7 +1377,7 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/file_contributions_mapper.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/messaging/queue.rb": { "lines": [ null, null, @@ -1340,73 +1386,50 @@ 1, 1, null, - 1, - 1, - 63, - null, null, 1, - 63, - null, - null, - null, - null, - null, 1, null, 1, - 126, - null, + 3, + 3, null, - 1, - 1724, null, null, - 1, - 63, null, + 3, null, - 1, - 63, - 1724, null, null, null, null, null, 1, - 1, - 1724, - 1724, - 1724, + 3, null, null, - 1, - 1, - 1, - 1, - 1, null, - 1, - 1724, null, null, null, 1, + 0, + 0, + 0, null, - 1, - 1724, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/folder_contributions_mapper.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/appraisal_representer.rb": { "lines": [ null, null, 1, + 1, + 1, null, 1, 1, @@ -1415,235 +1438,231 @@ 1, null, null, - 1, - 2, - 2, null, + 1, + 1, null, 1, - 2, + 1, + 1, null, null, + 1, + 6, null, null, + 7, + 7, null, - 1, - 2, - 63, null, + 1, + 6, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/porcelain_parser.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/project_representer.rb": { "lines": [ null, null, 1, 1, + 1, null, 1, + null, + null, 1, 1, null, 1, - 63, - 1724, - null, + 1, + 1, + 1, null, 1, - 63, - 63, + 1, + 1, + 1, + 1, + 1, + 1, + 1, null, - 0, - 0, + 1, + 14, null, null, + 1, null, 1, - 1724, + 14, null, - 1724, null, + 1, + 14, null, null, - 1724, - 18292, - 18292, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/member_representer.rb": { + "lines": [ null, - 1724, null, + 1, + 1, null, 1, - 1724, - 1724, - 1724, + 1, + null, + null, null, null, 1, - 18292, + 1, null, + 1, + 1, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/blame_reporter.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/folder_contributions_representer.rb": { "lines": [ null, null, 1, - null, + 1, 1, null, 1, 1, - null, 1, 1, null, 1, - 2, - 2, - 2, - null, - null, 1, - 2, - null, - 124, - null, - 4, null, + 1, + 1, null, 1, - 0, - 0, - 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, null, - 0, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/contributor_representer.rb": { + "lines": [ null, null, 1, - 0, - null, - null, 1, - 63, - null, null, 1, - null, 1, - 2, - null, - 2, - null, null, 1, - 0, - null, - null, - null, 1, - 0, - null, - null, null, 1, - 2, - 63, - 63, - 63, - null, - null, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/repo_file.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/credit_share_representer.rb": { "lines": [ null, null, 1, 1, - null, - 1, 1, null, 1, - 63, - null, null, 1, - 63, - null, + 1, null, + 1, + 1, null, + 1, + 1, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/git_repo.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/file_contributions_representer.rb": { "lines": [ null, null, 1, - null, 1, 1, + null, 1, 1, 1, - null, - null, 1, - 3, - 3, - 3, - null, null, 1, - 2, - null, - null, 1, - 0, - null, null, 1, - 6, - null, + 1, null, 1, 1, 1, - null, - 6, + 1, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/local_repo.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/file_path_representer.rb": { "lines": [ null, null, 1, + 1, + null, + 1, null, 1, 1, + null, + 1, 1, null, 1, + 1, + null, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/line_contribution_representer.rb": { + "lines": [ null, null, 1, @@ -1653,57 +1672,73 @@ 1, null, 1, - 3, - 3, - null, + 1, null, 1, - 6, 1, null, - null, 1, - 2, + 1, + 1, + 1, + 1, + 1, null, - 2, - 2, - 184, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/appraisal_request_representer.rb": { + "lines": [ null, null, + 1, + 1, + 1, null, 1, - 5, - 10, + 1, null, null, 1, - 13, - null, + 1, null, 1, - 0, + 1, + 1, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/clone_request_representer.rb": { + "lines": [ null, null, 1, - 0, + 1, + 1, null, null, 1, + 1, null, 1, - 7, + 1, null, + 1, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/remote_repo.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/http_response_representer.rb": { "lines": [ null, null, 1, + 1, null, 1, 1, @@ -1712,20 +1747,17 @@ null, null, null, + 1, + 1, null, 1, 1, null, 1, - 3, null, null, - 1, - 3, null, null, - 1, - 1, null, null, null, @@ -1733,35 +1765,38 @@ null, null, null, + null, + null, + 1, + 11, + null, + null, + null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/repo_store.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/openstruct_with_links.rb": { "lines": [ null, null, 1, - 1, null, 1, 1, - 11, - 16, - null, null, 1, - 16, - null, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/github/gateways/github_api.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/projects_representer.rb": { "lines": [ null, null, 1, + 1, null, 1, 1, @@ -1770,481 +1805,467 @@ 1, null, 1, - 55, - null, - null, 1, - 30, - null, null, + 1, null, null, - 1, - 25, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/api_result.rb": { + "lines": [ null, null, 1, 1, - 30, - 30, + 1, + null, null, null, 1, - 30, null, null, null, null, 1, - 1, - 55, null, null, 1, - 55, + 1, + 30, null, + 29, null, null, null, - 54, - 54, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/clone_request.rb": { + "lines": [ null, null, + 1, + 1, + null, + 1, null, null, null, 1, - 1, - 1, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/projects_list.rb": { + "lines": [ null, null, 1, - null, - null, - null, 1, - 54, - null, null, 1, - 4, - null, - null, - null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/github/mappers/member_mapper.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/controllers/app.rb": { "lines": [ null, null, 1, + 1, null, 1, null, 1, 1, - 25, - 25, - 25, + 1, + null, + null, null, null, 1, - 25, - 75, + 14, null, null, + 14, + 1, null, 1, - 100, null, null, null, 1, 1, - 100, null, null, - 1, - 100, + 13, + 13, + 13, null, + 10, null, null, + 8, null, + 8, null, null, null, - 1, + 8, null, - 1, - 100, null, null, - 1, - 100, null, null, - 1, - 100, + 8, + 5, + 5, null, null, null, + 3, + 3, + 3, + 3, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/github/mappers/project_mapper.rb": { - "lines": [ null, null, - 1, + 0, + 0, null, - 1, - 1, null, - 1, - 1, - 30, - 30, - 30, null, null, - 1, - 30, - 25, + 2, + 2, null, null, - 1, - 25, null, + 2, + 1, + 1, null, null, 1, 1, - 25, - 25, + 1, null, null, null, + 3, null, - 1, - 25, + 3, + 3, + 3, null, + 3, + 1, + 1, null, null, + 2, + 2, + 2, null, null, null, null, null, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/controllers/helpers.rb": { + "lines": [ null, null, 1, - 25, - null, - null, 1, - 25, - null, null, 1, - 25, - null, - null, 1, - 25, + 0, + 0, + 0, + 0, null, null, 1, - 25, - null, null, 1, - 25, + 0, null, null, 1, - 25, - null, + 0, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/messaging/queue.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/requests/project_list.rb": { "lines": [ null, null, 1, - null, 1, 1, null, + 1, + 1, null, 1, 1, null, 1, - 3, - 3, + 6, null, null, null, + 1, + 6, null, - 3, null, null, + 1, + null, null, null, null, null, - 1, - 3, null, null, null, + 1, + 6, + null, null, null, null, 1, - 0, - 0, - 0, + 5, + null, null, null, + 1, + 3, + null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/appraisal_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/requests/project_request_path.rb": { "lines": [ null, null, 1, 1, - 1, - null, - 1, - 1, null, 1, 1, + 8, + 8, + 8, + 8, null, null, - null, - 1, - 1, - null, - 1, 1, - 1, - null, null, 1, - 6, - null, - null, - 7, - 7, + 9, null, null, 1, - 6, + 0, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/project_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/add_project.rb": { "lines": [ null, null, 1, - 1, - 1, - null, - 1, - null, null, 1, 1, null, 1, 1, - 1, - 1, null, 1, 1, + null, 1, + null, 1, 1, + null, + null, 1, - 1, + 12, 1, null, - 1, - 14, + 11, null, + 9, + null, + 3, null, - 1, null, 1, - 14, null, + 9, + 8, null, 1, - 14, null, + 9, + null, + 0, + 0, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/member_representer.rb": { - "lines": [ null, null, 1, - 1, + 11, null, - 1, - 1, null, null, + 3, null, null, 1, - 1, + 12, + null, null, - 1, - 1, - 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/folder_contributions_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/fetch_or_request_appraisal.rb": { "lines": [ null, null, 1, - 1, - 1, null, 1, 1, + null, + null, 1, 1, null, 1, 1, - null, 1, 1, null, 1, + null, 1, 1, 1, 1, 1, + null, + null, 1, + 12, + null, + null, + null, + 12, + 10, + null, + 2, + null, + null, + 0, + null, + null, 1, + 10, + 2, + null, + 8, + null, + null, + null, 1, + 8, + 8, null, + 8, + 8, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/contributor_representer.rb": { - "lines": [ + null, + null, + 4, + 4, + 4, + null, + null, + 0, + 0, null, null, 1, - 1, null, - 1, - 1, + 8, null, - 1, - 1, null, - 1, - 1, + 4, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/credit_share_representer.rb": { - "lines": [ + 4, null, null, - 1, - 1, - 1, null, - 1, null, - 1, - 1, + 0, + 0, + null, + null, null, - 1, - 1, null, 1, - 1, + 8, + 8, null, null, + 1, + 4, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/file_contributions_representer.rb": { - "lines": [ null, null, - 1, - 1, - 1, + 4, null, - 1, - 1, - 1, - 1, null, - 1, - 1, null, 1, - 1, + 0, null, - 1, - 1, - 1, - 1, - 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/file_path_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/list_projects.rb": { "lines": [ null, null, 1, - 1, - null, - 1, null, 1, 1, @@ -2255,83 +2276,69 @@ 1, 1, null, + 1, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/line_contribution_representer.rb": { - "lines": [ + 1, null, null, 1, - 1, - 1, + 6, + 6, + 5, null, 1, null, - 1, - 1, null, - 1, - 1, null, 1, - 1, - 1, - 1, - 1, - 1, + 5, + 5, + 5, + 5, + null, + 0, + null, + null, + null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/appraisal_request_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/entities/contributor.rb": { "lines": [ null, null, 1, 1, - 1, null, 1, 1, null, - null, 1, 1, null, 1, 1, - 1, - null, - null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/clone_request_representer.rb": { - "lines": [ null, null, 1, - 1, - 1, + 2473, null, null, - 1, - 1, null, 1, - 1, + null, null, 1, - 1, + 2264, + null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/http_response_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/entities/file_contributions.rb": { "lines": [ null, null, @@ -2341,136 +2348,142 @@ 1, 1, null, - null, - null, - null, - null, 1, 1, null, 1, - 1, null, 1, + 63, + 63, null, null, + 1, + 125, null, null, + 1, + 0, null, null, + 1, + 125, null, + 125, null, + 1724, null, null, null, + 1, + 0, null, null, 1, - 11, null, + 1, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/openstruct_with_links.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/lib/contributions_calculator.rb": { "lines": [ null, null, 1, - null, - 1, 1, null, 1, 1, + 0, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/projects_representer.rb": { - "lines": [ + 1, + 0, null, null, 1, - 1, + 0, null, - 1, - 1, null, 1, - 1, + 0, null, - 1, - 1, null, 1, + 0, + null, + 0, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/api_result.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/values/code_language.rb": { "lines": [ null, null, + null, + 1, + 1, 1, 1, 1, null, null, + 1, null, 1, + 8, null, null, + 1, + 2019, null, null, 1, + 0, null, null, 1, 1, - 30, - null, - 29, null, + 1, + 9, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/clone_request.rb": { - "lines": [ + 1, + 1724, null, null, 1, - 1, + 0, + null, null, 1, + 1724, null, null, null, 1, - null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/projects_list.rb": { - "lines": [ - null, + 1, null, 1, 1, + 8, + 8, + 8, + null, + 8, + null, null, + null, + 1, 1, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/controllers/app.rb": { - "lines": [ null, null, 1, @@ -2483,101 +2496,90 @@ 1, null, null, - null, - null, 1, - 14, - null, - null, - 14, 1, null, 1, null, - null, - null, + 1, 1, 1, null, null, - 13, - 13, - 13, - null, - 10, - null, - null, - 8, - null, - 8, - null, - null, - null, - 8, - null, - null, + 1, + 1, null, + 1, null, + 1, + 1, + 1, null, - 8, - 5, - 5, null, + 1, + 1, null, + 1, null, - 3, - 3, - 3, - 3, + 1, + 1, null, null, + 1, + 1, null, - 0, - 0, + 1, null, + 1, + 1, null, null, + 1, + 1, null, - 2, - 2, + 1, null, + 1, + 1, null, null, - 2, 1, 1, null, - null, 1, + null, 1, 1, null, null, + 1, + 1, null, - 3, + 1, + null, + 1, + 1, null, - 3, - 3, - 3, null, - 3, 1, 1, null, + 1, null, - 2, - 2, - 2, + 1, + 1, null, + 1, null, null, + 1, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/controllers/helpers.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/entities/folder_contributions.rb": { "lines": [ null, null, @@ -2586,13 +2588,17 @@ null, 1, 1, - 0, - 0, - 0, - 0, null, + 1, null, 1, + 23, + null, + 23, + 23, + 23, + 23, + null, null, 1, 0, @@ -2602,63 +2608,63 @@ 0, null, null, - null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/requests/project_list.rb": { - "lines": [ + 1, + 0, null, null, 1, - 1, - 1, + 0, null, - 1, - 1, null, 1, - 1, + 0, null, - 1, - 6, null, + 1, + 12, null, null, 1, - 6, - null, + 0, null, null, 1, null, + 1, + 193, null, null, + 1, + 23, null, null, - null, + 1, + 23, + 128, + 63, null, null, 1, - 6, + 23, null, + 65, + 65, null, null, null, 1, - 5, - null, + 23, + 14, null, null, - 1, - 3, + 37, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/requests/project_request_path.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/entities/line_contribution.rb": { "lines": [ null, null, @@ -2666,84 +2672,102 @@ 1, null, 1, + null, + 1, 1, - 8, - 8, - 8, - 8, null, + 1, + 1, + 1, null, 1, null, 1, - 9, + 1724, + 1724, + 1724, + 1724, null, null, 1, - 0, + 1724, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/add_project.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/lib/types.rb": { "lines": [ null, null, 1, - null, - 1, 1, null, 1, 1, + 60, + null, + null, + null, null, 1, 1, + 623, + null, null, - 1, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/values/contributors.rb": { + "lines": [ 1, 1, null, - null, 1, - 12, 1, null, - 11, + 1, + 120, + null, null, - 9, null, - 3, null, null, 1, null, - 9, - 8, + 120, + 120, null, - 1, + 120, null, - 9, + 371, + 371, + null, + 371, null, 0, 0, + 0, + 0, + 0, + 0, null, + 371, null, + 29, + 29, + 29, null, null, - 1, - 11, - null, - null, + 342, + 342, + 342, null, - 3, null, null, - 1, - 12, + 120, null, null, null, @@ -2751,115 +2775,104 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/fetch_or_request_appraisal.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/values/credit_share.rb": { "lines": [ null, null, 1, - null, - 1, 1, null, - null, - 1, 1, null, 1, 1, - 1, - 1, null, 1, + 183, + 183, null, - 1, - 1, - 1, - 1, - 1, null, null, 1, - 12, - null, - null, - null, - 12, - 10, - null, - 2, - null, - null, 0, null, null, 1, - 10, - 2, + 0, null, - 8, null, + 1, + 0, null, null, 1, - 8, - 8, - null, - 8, - 8, + 0, null, null, + 1, null, - 4, - 4, - 4, + 1, + 0, null, null, - 0, + 1, 0, null, null, 1, - null, - 8, + 2066, + 2066, + 2066, + 2066, null, null, - 4, + 1, + 120, null, null, - 4, null, + 120, + 155, null, null, + 120, + 342, + 713, + 342, null, - 0, - 0, null, null, + 1, + 7, + 3, null, null, 1, - 8, - 8, + 3, null, null, 1, - 4, - null, null, + 1, + 4329, null, - 4, null, null, null, 1, 0, + 0, + null, + 0, + null, + null, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/list_projects.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/values/file_path.rb": { "lines": [ null, null, @@ -2869,33 +2882,44 @@ 1, null, 1, + null, 1, null, 1, + null, 1, + 278, + 278, + null, null, 1, + 3996, + null, null, 1, + 2001, + null, + 1995, null, null, 1, - 6, - 6, - 5, + 152, + null, null, 1, null, + 65, null, + 65, + 65, null, - 1, - 5, - 5, - 5, - 5, null, - 0, + 1, null, + 1, + 278, + 278, + 278, null, null, null, @@ -2928,8 +2952,8 @@ null, 1, 32, - 2297, - 2297, + 2227, + 2227, null, null, 32, @@ -4278,6 +4302,6 @@ ] } }, - "timestamp": 1766327327 + "timestamp": 1766331706 } } diff --git a/require_worker.rb b/require_worker.rb new file mode 100644 index 0000000..482ecaf --- /dev/null +++ b/require_worker.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# Requires all ruby files in specified worker folders +# Worker has its own domain (contributions) and services separate from the API +# Params: +# - (opt) folders: Array of folder names within workers/, or String of single folder name +# Usage: +# require_worker +# require_worker(%w[domain]) +# require_worker('services') +def require_worker(folders = %w[domain services]) + worker_list = Array(folders).map { |folder| "workers/#{folder}" } + + Dir.glob("./{#{worker_list.join(',')}}/**/*.rb").each do |file| + require file + end +end diff --git a/spec/helpers/spec_helper.rb b/spec/helpers/spec_helper.rb index 5770107..f293bbc 100644 --- a/spec/helpers/spec_helper.rb +++ b/spec/helpers/spec_helper.rb @@ -14,7 +14,10 @@ require 'webmock' require_relative '../../require_app' -require_app +require_relative '../../require_worker' + +require_app # Load API layers +require_worker('domain') # Load worker domain for tests using git infrastructure USERNAME = 'soumyaray' PROJECT_NAME = 'YPBT-app' diff --git a/app/domain/contributions/entities/contributor.rb b/workers/domain/contributions/entities/contributor.rb similarity index 100% rename from app/domain/contributions/entities/contributor.rb rename to workers/domain/contributions/entities/contributor.rb diff --git a/app/domain/contributions/entities/file_contributions.rb b/workers/domain/contributions/entities/file_contributions.rb similarity index 100% rename from app/domain/contributions/entities/file_contributions.rb rename to workers/domain/contributions/entities/file_contributions.rb diff --git a/app/domain/contributions/entities/folder_contributions.rb b/workers/domain/contributions/entities/folder_contributions.rb similarity index 100% rename from app/domain/contributions/entities/folder_contributions.rb rename to workers/domain/contributions/entities/folder_contributions.rb diff --git a/app/domain/contributions/entities/line_contribution.rb b/workers/domain/contributions/entities/line_contribution.rb similarity index 100% rename from app/domain/contributions/entities/line_contribution.rb rename to workers/domain/contributions/entities/line_contribution.rb diff --git a/app/domain/contributions/lib/contributions_calculator.rb b/workers/domain/contributions/lib/contributions_calculator.rb similarity index 100% rename from app/domain/contributions/lib/contributions_calculator.rb rename to workers/domain/contributions/lib/contributions_calculator.rb diff --git a/app/domain/contributions/lib/types.rb b/workers/domain/contributions/lib/types.rb similarity index 100% rename from app/domain/contributions/lib/types.rb rename to workers/domain/contributions/lib/types.rb diff --git a/app/domain/contributions/values/code_language.rb b/workers/domain/contributions/values/code_language.rb similarity index 100% rename from app/domain/contributions/values/code_language.rb rename to workers/domain/contributions/values/code_language.rb diff --git a/app/domain/contributions/values/contributors.rb b/workers/domain/contributions/values/contributors.rb similarity index 100% rename from app/domain/contributions/values/contributors.rb rename to workers/domain/contributions/values/contributors.rb diff --git a/app/domain/contributions/values/credit_share.rb b/workers/domain/contributions/values/credit_share.rb similarity index 100% rename from app/domain/contributions/values/credit_share.rb rename to workers/domain/contributions/values/credit_share.rb diff --git a/app/domain/contributions/values/file_path.rb b/workers/domain/contributions/values/file_path.rb similarity index 100% rename from app/domain/contributions/values/file_path.rb rename to workers/domain/contributions/values/file_path.rb diff --git a/workers/git_clone_worker.rb b/workers/git_clone_worker.rb index 11de166..af1571d 100644 --- a/workers/git_clone_worker.rb +++ b/workers/git_clone_worker.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true require_relative '../require_app' -require_app +require_relative '../require_worker' + +require_app # Load API layers (domain, infrastructure, presentation, application) +require_worker # Load worker-only layers (domain, services) require_relative 'clone_monitor' require_relative 'job_reporter' -require_relative 'services/appraise_project' require 'figaro' require 'shoryuken' From d170ac405debc9df09035200ba0a6b2b3ec04688 Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Mon, 22 Dec 2025 00:01:17 +0800 Subject: [PATCH 10/18] refactor: move git infrastructure to worker-only location MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move git-related infrastructure to workers/infrastructure/ since it's only used by the worker for clone and blame operations: - Move gateway: git_command.rb (git CLI wrapper) - Move repositories: git_repo.rb, local_repo.rb, remote_repo.rb, blame_reporter.rb, repo_file.rb, repo_store.rb - Move mappers: contributions_mapper.rb, folder_contributions_mapper.rb, file_contributions_mapper.rb, blame_contributor.rb, porcelain_parser.rb - Update require_worker.rb to load infrastructure folder by default - Update spec_helper to load worker infrastructure for tests API infrastructure now contains only: cache, database, github, messaging 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- coverage/.resultset.json | 1678 ++++++++--------- require_worker.rb | 6 +- spec/helpers/spec_helper.rb | 4 +- .../infrastructure/git/gateway/git_command.rb | 0 .../git/mappers/blame_contributor.rb | 0 .../git/mappers/contributions_mapper.rb | 0 .../git/mappers/file_contributions_mapper.rb | 0 .../mappers/folder_contributions_mapper.rb | 0 .../git/mappers/porcelain_parser.rb | 0 .../git/repositories/blame_reporter.rb | 0 .../git/repositories/git_repo.rb | 0 .../git/repositories/local_repo.rb | 0 .../git/repositories/remote_repo.rb | 0 .../git/repositories/repo_file.rb | 0 .../git/repositories/repo_store.rb | 0 15 files changed, 844 insertions(+), 844 deletions(-) rename {app => workers}/infrastructure/git/gateway/git_command.rb (100%) rename {app => workers}/infrastructure/git/mappers/blame_contributor.rb (100%) rename {app => workers}/infrastructure/git/mappers/contributions_mapper.rb (100%) rename {app => workers}/infrastructure/git/mappers/file_contributions_mapper.rb (100%) rename {app => workers}/infrastructure/git/mappers/folder_contributions_mapper.rb (100%) rename {app => workers}/infrastructure/git/mappers/porcelain_parser.rb (100%) rename {app => workers}/infrastructure/git/repositories/blame_reporter.rb (100%) rename {app => workers}/infrastructure/git/repositories/git_repo.rb (100%) rename {app => workers}/infrastructure/git/repositories/local_repo.rb (100%) rename {app => workers}/infrastructure/git/repositories/remote_repo.rb (100%) rename {app => workers}/infrastructure/git/repositories/repo_file.rb (100%) rename {app => workers}/infrastructure/git/repositories/repo_store.rb (100%) diff --git a/coverage/.resultset.json b/coverage/.resultset.json index 937aabf..f476012 100644 --- a/coverage/.resultset.json +++ b/coverage/.resultset.json @@ -16,7 +16,7 @@ 1, null, 1, - 51, + 39, null, null ] @@ -34,10 +34,10 @@ null, null, 1, - 2, + 3, null, 1, - 10, + 22, null, null ] @@ -247,7 +247,7 @@ null, null, 1, - 11, + 10, null, null, 1, @@ -592,246 +592,217 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/gateway/git_command.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/github/gateways/github_api.rb": { "lines": [ null, null, 1, - 1, null, 1, 1, null, 1, - 66, - 66, - 66, - 66, - null, - null, 1, - 2, - 2, - 2, - null, null, 1, - 64, - 64, - 64, + 55, null, null, 1, - 64, - 64, + 30, null, null, - 1, - 2, - 2, null, null, 1, - 2, - 2, + 25, + null, null, null, 1, - 132, + 1, + 30, + 30, null, null, 1, - 66, - null, + 30, null, null, null, null, 1, - 63, + 1, + 55, null, null, 1, - 1, - 5, + 55, null, null, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/blame_contributor.rb": { - "lines": [ + 54, + 54, + null, + null, null, null, - 1, - 1, null, 1, 1, - 1724, - 1724, + 1, null, null, 1, - 1724, - null, null, null, null, + 1, + 54, null, null, 1, + 4, null, - 1, - 1724, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/contributions_mapper.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/github/mappers/member_mapper.rb": { "lines": [ null, null, 1, + null, 1, null, 1, 1, - 2, + 25, + 25, + 25, null, null, 1, - 2, + 25, + 75, null, - 2, null, null, + 1, + 100, null, null, null, 1, 1, - 2, - 63, - null, - null, + 100, null, null, 1, - 1, - 63, - 63, - 63, + 100, null, null, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/file_contributions_mapper.rb": { - "lines": [ null, null, - 1, null, 1, - 1, null, 1, - 1, - 63, + 100, null, null, 1, - 63, + 100, null, null, + 1, + 100, null, null, null, - 1, null, - 1, - 126, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/github/mappers/project_mapper.rb": { + "lines": [ null, null, 1, - 1724, null, + 1, + 1, null, 1, - 63, + 1, + 30, + 30, + 30, null, null, 1, - 63, - 1724, + 30, + 25, null, null, + 1, + 25, null, null, null, 1, 1, - 1724, - 1724, - 1724, + 25, + 25, + null, null, null, - 1, - 1, - 1, - 1, - 1, null, 1, - 1724, + 25, null, null, null, - 1, null, - 1, - 1724, null, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/folder_contributions_mapper.rb": { - "lines": [ null, null, - 1, null, - 1, - 1, null, 1, + 25, + null, + null, 1, + 25, null, null, 1, - 2, - 2, + 25, null, null, 1, - 2, + 25, null, null, + 1, + 25, null, null, + 1, + 25, + null, null, 1, - 2, - 63, + 25, null, null, null, @@ -839,136 +810,139 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/porcelain_parser.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/messaging/queue.rb": { "lines": [ null, null, 1, - 1, null, 1, 1, + null, + null, + 1, 1, null, 1, - 63, - 1724, + 3, + 3, null, null, - 1, - 63, - 63, null, - 0, - 0, null, + 3, null, null, - 1, - 1724, null, - 1724, null, null, null, - 1724, - 18292, - 18292, + 1, + 3, null, null, - 1724, null, null, - 1, - 1724, - 1724, - 1724, null, null, 1, - 18292, + 0, + 0, + 0, + null, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/blame_reporter.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/appraisal_representer.rb": { "lines": [ null, null, 1, + 1, + 1, null, 1, + 1, null, 1, 1, null, + null, + null, 1, 1, null, 1, - 2, - 2, - 2, + 1, + 1, null, null, 1, - 2, + 6, null, - 124, null, - 4, + 7, + 7, null, null, 1, - 0, - 0, - 0, + 6, + null, null, - 0, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/project_representer.rb": { + "lines": [ null, null, 1, - 0, - null, + 1, + 1, null, 1, - 63, null, null, 1, - null, 1, - 2, - null, - 2, null, + 1, + 1, + 1, + 1, null, 1, - 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, null, + 1, + 14, null, null, 1, - 0, null, + 1, + 14, null, null, 1, - 2, - 63, - 63, - 63, - null, + 14, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/repo_file.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/member_representer.rb": { "lines": [ null, null, @@ -978,74 +952,75 @@ 1, 1, null, - 1, - 63, - null, - null, - 1, - 63, null, null, null, + 1, + 1, null, + 1, + 1, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/git_repo.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/folder_contributions_representer.rb": { "lines": [ null, null, 1, - null, 1, 1, + null, 1, 1, 1, - null, - null, 1, - 3, - 3, - 3, - null, null, 1, - 2, - null, - null, 1, - 0, - null, null, 1, - 6, - null, + 1, null, 1, 1, 1, - null, - 6, + 1, + 1, + 1, + 1, + 1, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/local_repo.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/contributor_representer.rb": { "lines": [ null, null, 1, + 1, null, 1, 1, + null, + 1, 1, null, 1, + 1, + null, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/credit_share_representer.rb": { + "lines": [ null, null, 1, @@ -1055,375 +1030,418 @@ 1, null, 1, - 3, - 3, - null, + 1, null, 1, - 6, 1, null, - null, 1, - 2, + 1, null, - 2, - 2, - 184, null, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/file_contributions_representer.rb": { + "lines": [ null, null, 1, - 5, - 10, - null, - null, 1, - 13, - null, - null, 1, - 0, - null, null, 1, - 0, - null, + 1, + 1, + 1, null, 1, + 1, null, 1, - 7, + 1, null, + 1, + 1, + 1, + 1, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/remote_repo.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/file_path_representer.rb": { "lines": [ null, null, 1, + 1, + null, + 1, null, 1, 1, null, + 1, + 1, null, + 1, + 1, null, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/line_contribution_representer.rb": { + "lines": [ null, null, 1, 1, - null, 1, - 3, - null, null, 1, - 3, - null, null, 1, 1, null, + 1, + 1, null, - null, - null, - null, + 1, + 1, + 1, + 1, + 1, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/repo_store.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/appraisal_request_representer.rb": { "lines": [ null, null, 1, 1, + 1, null, 1, 1, - 11, - 14, null, null, 1, - 14, + 1, null, + 1, + 1, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/github/gateways/github_api.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/clone_request_representer.rb": { "lines": [ null, null, 1, - null, 1, 1, null, + null, 1, 1, null, 1, - 55, - null, + 1, null, 1, - 30, + 1, null, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/http_response_representer.rb": { + "lines": [ null, null, 1, - 25, - null, - null, + 1, null, 1, 1, - 30, - 30, - null, null, - 1, - 30, null, null, null, null, 1, 1, - 55, null, + 1, + 1, null, 1, - 55, null, null, null, null, - 54, - 54, null, null, null, null, null, - 1, - 1, - 1, + null, + null, null, null, 1, + 11, null, null, null, - 1, - 54, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/openstruct_with_links.rb": { + "lines": [ null, null, 1, - 4, null, + 1, + 1, null, + 1, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/github/mappers/member_mapper.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/projects_representer.rb": { "lines": [ null, null, 1, - null, 1, null, 1, 1, - 25, - 25, - 25, null, + 1, + 1, null, 1, - 25, - 75, + 1, null, + 1, null, null, - 1, - 100, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/api_result.rb": { + "lines": [ null, null, 1, 1, - 100, - null, - null, 1, - 100, - null, null, null, null, + 1, null, null, null, - 1, null, 1, - 100, null, null, 1, - 100, - null, - null, 1, - 100, + 30, null, + 29, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/github/mappers/project_mapper.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/clone_request.rb": { "lines": [ null, null, 1, - null, - 1, 1, null, 1, - 1, - 30, - 30, - 30, - null, null, - 1, - 30, - 25, null, null, 1, - 25, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/projects_list.rb": { + "lines": [ null, null, 1, 1, - 25, - 25, null, + 1, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/controllers/app.rb": { + "lines": [ null, null, 1, - 25, - null, - null, + 1, null, + 1, null, + 1, + 1, + 1, null, null, null, null, + 1, + 14, null, null, + 14, + 1, null, 1, - 25, + null, null, null, 1, - 25, + 1, null, null, - 1, - 25, + 13, + 13, + 13, null, + 10, null, - 1, - 25, null, + 8, null, - 1, - 25, + 8, null, null, - 1, - 25, null, + 8, null, - 1, - 25, null, null, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/messaging/queue.rb": { - "lines": [ + 8, + 5, + 5, null, null, - 1, null, + 3, + 3, + 3, + 3, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 2, + 2, + null, + null, + null, + 2, 1, 1, null, null, 1, 1, - null, 1, - 3, - 3, null, null, null, + 3, null, 3, + 3, + 3, null, + 3, + 1, + 1, null, null, + 2, + 2, + 2, null, null, null, - 1, - 3, null, null, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/controllers/helpers.rb": { + "lines": [ null, null, + 1, + 1, null, 1, + 1, + 0, 0, 0, 0, null, null, + 1, + null, + 1, + 0, + null, + null, + 1, + 0, + null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/appraisal_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/requests/project_list.rb": { "lines": [ null, null, @@ -1437,79 +1455,45 @@ 1, 1, null, - null, - null, - 1, 1, + 6, null, - 1, - 1, - 1, null, null, 1, 6, null, null, - 7, - 7, - null, null, 1, - 6, - null, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/project_representer.rb": { - "lines": [ null, null, - 1, - 1, - 1, null, - 1, null, null, - 1, - 1, null, 1, - 1, - 1, - 1, + 6, null, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, null, - 1, - 14, null, null, 1, + 5, null, - 1, - 14, null, null, 1, - 14, + 3, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/member_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/requests/project_request_path.rb": { "lines": [ null, null, @@ -1518,33 +1502,34 @@ null, 1, 1, + 8, + 8, + 8, + 8, null, null, - null, - null, - 1, 1, null, 1, + 9, + null, + null, 1, - 1, + 0, + null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/folder_contributions_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/add_project.rb": { "lines": [ null, null, 1, - 1, - 1, null, 1, 1, - 1, - 1, null, 1, 1, @@ -1553,81 +1538,73 @@ 1, null, 1, + null, 1, 1, + null, + null, 1, - 1, - 1, - 1, - 1, + 12, 1, null, + 11, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/contributor_representer.rb": { - "lines": [ + 9, null, + 3, null, - 1, - 1, null, 1, - 1, null, - 1, - 1, + 9, + 8, null, 1, - 1, null, + 9, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/credit_share_representer.rb": { - "lines": [ + 0, + 0, null, null, - 1, - 1, - 1, null, - 1, null, 1, - 1, + 11, + null, + null, + null, + 3, null, - 1, - 1, null, 1, - 1, + 12, + null, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/file_contributions_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/fetch_or_request_appraisal.rb": { "lines": [ null, null, 1, + null, 1, 1, null, + null, 1, 1, + null, 1, 1, - null, 1, 1, null, 1, - 1, null, 1, 1, @@ -1636,117 +1613,96 @@ 1, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/file_path_representer.rb": { - "lines": [ + 1, + 12, null, null, - 1, - 1, null, - 1, + 12, + 10, null, - 1, - 1, + 2, + null, + null, + 0, null, - 1, - 1, null, 1, - 1, + 10, + 2, null, + 8, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/line_contribution_representer.rb": { - "lines": [ null, null, 1, - 1, - 1, + 8, + 8, null, - 1, + 8, + 8, null, - 1, - 1, null, - 1, - 1, null, - 1, - 1, - 1, - 1, - 1, - 1, + 4, + 4, + 4, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/appraisal_request_representer.rb": { - "lines": [ + 0, + 0, null, null, 1, - 1, - 1, null, - 1, - 1, + 8, null, null, - 1, - 1, + 4, null, - 1, - 1, - 1, null, + 4, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/clone_request_representer.rb": { - "lines": [ null, null, - 1, - 1, - 1, + null, + 0, + 0, + null, null, null, - 1, - 1, null, 1, - 1, + 8, + 8, + null, null, 1, + 4, + null, + null, + null, + 4, + null, + null, + null, 1, + 0, + null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/http_response_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/list_projects.rb": { "lines": [ null, null, 1, - 1, null, 1, 1, null, - null, - null, - null, - null, 1, 1, null, @@ -1755,95 +1711,117 @@ null, 1, null, + 1, null, null, + 1, + 6, + 6, + 5, null, + 1, null, null, null, + 1, + 5, + 5, + 5, + 5, null, + 0, null, null, null, null, null, - 1, - 11, - null, - null, - null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/openstruct_with_links.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/entities/contributor.rb": { "lines": [ null, null, 1, - null, - 1, 1, null, 1, 1, null, - null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/projects_representer.rb": { - "lines": [ - null, - null, 1, 1, null, 1, 1, null, - 1, - 1, null, 1, + 2473, + null, + null, + null, 1, null, + null, 1, + 2264, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/api_result.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/entities/file_contributions.rb": { "lines": [ null, null, 1, 1, + null, + 1, 1, null, + 1, + 1, null, + 1, null, 1, + 63, + 63, null, null, + 1, + 125, null, null, 1, + 0, null, null, 1, + 125, + null, + 125, + null, + 1724, + null, + null, + null, 1, - 30, + 0, null, - 29, null, + 1, + null, + 1, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/clone_request.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/lib/contributions_calculator.rb": { "lines": [ null, null, @@ -1851,216 +1829,275 @@ 1, null, 1, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, null, + 0, null, null, - 1, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/projects_list.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/values/code_language.rb": { "lines": [ + null, null, null, 1, 1, - null, 1, - null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/controllers/app.rb": { - "lines": [ - null, - null, 1, 1, null, - 1, null, 1, + null, 1, - 1, + 8, null, null, + 1, + 2019, null, null, 1, - 14, + 0, null, null, - 14, 1, - null, 1, null, + 1, + 9, null, null, 1, - 1, + 1724, null, null, - 13, - 13, - 13, + 1, + 0, null, - 10, null, + 1, + 1724, null, - 8, null, - 8, null, + 1, + 1, null, + 1, + 1, + 8, + 8, + 8, null, 8, null, null, null, + 1, + 1, null, null, - 8, - 5, - 5, null, + 1, + 1, null, + 1, null, - 3, - 3, - 3, - 3, + 1, + 1, + 1, null, null, + 1, + 1, null, - 0, - 0, + 1, null, + 1, + 1, + 1, null, null, + 1, + 1, null, - 2, - 2, + 1, null, + 1, + 1, + 1, null, null, - 2, 1, 1, null, - null, 1, + null, 1, 1, null, null, + 1, + 1, null, - 3, + 1, + null, + 1, + 1, null, - 3, - 3, - 3, null, - 3, 1, 1, null, + 1, null, - 2, - 2, - 2, + 1, + 1, null, null, + 1, + 1, null, + 1, null, + 1, + 1, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/controllers/helpers.rb": { - "lines": [ + 1, + 1, null, + 1, null, 1, 1, null, + null, 1, 1, - 0, - 0, - 0, - 0, null, + 1, null, 1, + 1, null, 1, - 0, null, null, 1, - 0, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/requests/project_list.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/entities/folder_contributions.rb": { "lines": [ null, null, 1, 1, - 1, null, 1, 1, null, 1, + null, 1, + 23, + null, + 23, + 23, + 23, + 23, + null, null, 1, - 6, + 0, null, null, + 1, + 0, + null, null, 1, - 6, + 0, + null, null, + 1, + 0, null, null, 1, + 0, + null, null, + 1, + 12, null, null, + 1, + 0, null, null, + 1, null, + 1, + 193, null, null, 1, - 6, + 23, null, null, + 1, + 23, + 128, + 63, null, null, 1, - 5, + 23, + null, + 65, + 65, null, null, null, 1, - 3, + 23, + 14, + null, + null, + 37, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/requests/project_request_path.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/entities/line_contribution.rb": { "lines": [ null, null, @@ -2068,84 +2105,102 @@ 1, null, 1, + null, + 1, 1, - 8, - 8, - 8, - 8, null, + 1, + 1, + 1, null, 1, null, 1, - 9, + 1724, + 1724, + 1724, + 1724, null, null, 1, - 0, + 1724, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/add_project.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/lib/types.rb": { "lines": [ null, null, 1, - null, - 1, 1, null, 1, 1, + 60, + null, + null, + null, null, 1, 1, + 623, null, - 1, null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/values/contributors.rb": { + "lines": [ 1, 1, null, - null, 1, - 12, 1, null, - 11, + 1, + 120, + null, null, - 9, null, - 3, null, null, 1, null, - 9, - 8, + 120, + 120, null, - 1, + 120, null, - 9, + 371, + 371, + null, + 371, null, 0, 0, + 0, + 0, + 0, + 0, null, + 371, null, + 29, + 29, + 29, null, null, - 1, - 11, - null, - null, + 342, + 342, + 342, null, - 3, null, null, - 1, - 12, + 120, null, null, null, @@ -2153,21 +2208,10 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/fetch_or_request_appraisal.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/values/credit_share.rb": { "lines": [ null, - null, - 1, - null, - 1, - 1, - null, - null, - 1, - 1, - null, - 1, - 1, + null, 1, 1, null, @@ -2175,93 +2219,93 @@ null, 1, 1, - 1, - 1, - 1, - null, null, 1, - 12, - null, - null, - null, - 12, - 10, + 183, + 183, null, - 2, null, null, + 1, 0, null, null, 1, - 10, - 2, + 0, null, - 8, null, + 1, + 0, null, null, 1, - 8, - 8, - null, - 8, - 8, + 0, null, null, + 1, null, - 4, - 4, - 4, + 1, + 0, null, null, - 0, + 1, 0, null, null, 1, - null, - 8, + 2066, + 2066, + 2066, + 2066, null, null, - 4, + 1, + 120, null, null, - 4, null, + 120, + 155, null, null, + 120, + 342, + 713, + 342, null, - 0, - 0, null, null, + 1, + 7, + 3, null, null, 1, - 8, - 8, + 3, null, null, 1, - 4, - null, null, + 1, + 4329, null, - 4, null, null, null, 1, 0, + 0, + null, + 0, + null, + null, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/list_projects.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/values/file_path.rb": { "lines": [ null, null, @@ -2271,33 +2315,44 @@ 1, null, 1, + null, 1, null, 1, + null, 1, + 278, + 278, + null, null, 1, + 3996, + null, null, 1, + 2001, + null, + 1995, null, null, 1, - 6, - 6, - 5, + 152, + null, null, 1, null, + 65, null, + 65, + 65, null, - 1, - 5, - 5, - 5, - 5, null, - 0, + 1, null, + 1, + 278, + 278, + 278, null, null, null, @@ -2305,7 +2360,7 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/entities/contributor.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/gateway/git_command.rb": { "lines": [ null, null, @@ -2316,79 +2371,96 @@ 1, null, 1, - 1, + 65, + 65, + 65, + 65, + null, null, 1, 1, - null, - null, 1, - 2473, - null, - null, - null, 1, null, null, 1, - 2264, - null, + 64, + 64, + 64, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/entities/file_contributions.rb": { - "lines": [ + 1, + 64, + 64, null, null, 1, 1, - null, - 1, 1, null, + null, + 1, 1, 1, null, + null, 1, + 130, + null, null, 1, - 63, - 63, + 65, + null, + null, + null, null, null, 1, - 125, + 63, null, null, 1, 0, + 0, null, null, - 1, - 125, null, - 125, null, - 1724, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/mappers/blame_contributor.rb": { + "lines": [ null, null, + 1, + 1, null, 1, - 0, + 1, + 1724, + 1724, null, null, 1, + 1724, + null, + null, + null, + null, + null, null, 1, + null, 1, + 1724, + null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/lib/contributions_calculator.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/mappers/contributions_mapper.rb": { "lines": [ null, null, @@ -2397,62 +2469,63 @@ null, 1, 1, - 0, + 2, null, null, 1, - 0, + 2, + null, + 2, + null, null, null, - 1, - 0, null, null, 1, - 0, + 1, + 2, + 63, + null, + null, null, null, 1, - 0, + 1, + 63, + 63, + 63, null, - 0, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/values/code_language.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/mappers/file_contributions_mapper.rb": { "lines": [ null, null, - null, - 1, - 1, 1, + null, 1, 1, null, - null, 1, - null, 1, - 8, + 63, null, null, 1, - 2019, + 63, + null, null, null, - 1, - 0, null, null, - 1, 1, null, 1, - 9, + 126, null, null, 1, @@ -2460,211 +2533,210 @@ null, null, 1, - 0, + 63, null, null, 1, + 63, 1724, null, null, null, + null, + null, 1, 1, + 1724, + 1724, + 1724, + null, null, 1, 1, - 8, - 8, - 8, + 1, + 1, + 1, null, - 8, + 1, + 1724, null, null, null, 1, + null, 1, + 1724, null, null, null, - 1, - 1, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/mappers/folder_contributions_mapper.rb": { + "lines": [ null, - 1, null, 1, + null, 1, 1, null, - null, 1, 1, null, - 1, null, 1, - 1, - 1, + 2, + 2, null, null, 1, - 1, + 2, null, - 1, null, - 1, - 1, - 1, null, null, - 1, - 1, null, 1, + 2, + 63, null, - 1, - 1, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/mappers/porcelain_parser.rb": { + "lines": [ null, null, 1, 1, null, 1, - null, 1, 1, null, - null, - 1, 1, + 63, + 1724, null, - 1, null, 1, - 1, + 63, + 63, null, + 0, + 0, null, - 1, - 1, null, - 1, null, 1, - 1, - null, + 1724, null, - 1, - 1, + 1724, null, - 1, null, - 1, - 1, null, + 1724, + 18292, + 18292, null, - 1, - 1, null, - 1, + 1724, null, - 1, - 1, null, 1, + 1724, + 1724, + 1724, null, null, 1, + 18292, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/entities/folder_contributions.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/repositories/blame_reporter.rb": { "lines": [ null, null, 1, + null, 1, null, 1, 1, null, 1, - null, 1, - 23, null, - 23, - 23, - 23, - 23, + 1, + 2, + 2, + 2, null, null, 1, - 0, + 2, null, + 124, null, - 1, - 0, + 4, null, null, 1, 0, + 0, + 0, null, - null, - 1, 0, null, null, + null, 1, 0, null, null, 1, - 12, + 63, null, null, 1, - 0, - null, null, 1, + 2, null, - 1, - 193, + 2, null, null, 1, - 23, - null, + 0, null, - 1, - 23, - 128, - 63, null, null, 1, - 23, - null, - 65, - 65, + 0, null, null, null, 1, - 23, - 14, - null, + 2, + 63, + 63, + 63, null, - 37, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/entities/line_contribution.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/repositories/repo_file.rb": { "lines": [ null, null, @@ -2672,207 +2744,132 @@ 1, null, 1, - null, - 1, 1, null, 1, - 1, - 1, + 63, null, - 1, null, 1, - 1724, - 1724, - 1724, - 1724, + 63, + null, null, null, - 1, - 1724, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/lib/types.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/repositories/git_repo.rb": { "lines": [ null, null, 1, - 1, null, 1, 1, - 60, - null, - null, - null, - null, - 1, 1, - 623, - null, - null, - null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/values/contributors.rb": { - "lines": [ 1, 1, null, - 1, - 1, null, 1, - 120, - null, + 3, + 3, + 3, null, null, + 1, + 2, null, null, 1, + 0, null, - 120, - 120, - null, - 120, null, - 371, - 371, + 1, + 5, null, - 371, null, + 1, 0, 0, - 0, - 0, - 0, - 0, - null, - 371, - null, - 29, - 29, - 29, - null, - null, - 342, - 342, - 342, - null, - null, - null, - 120, - null, null, + 0, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/values/credit_share.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/repositories/local_repo.rb": { "lines": [ null, null, 1, - 1, null, 1, - null, 1, 1, null, 1, - 183, - 183, - null, null, null, - 1, - 0, - null, null, 1, - 0, - null, - null, 1, - 0, - null, - null, 1, - 0, - null, null, 1, null, 1, - 0, + 3, + 3, null, null, 1, 0, + 0, null, null, 1, - 2066, - 2066, - 2066, - 2066, - null, - null, - 1, - 120, - null, - null, - null, - 120, - 155, + 2, null, + 2, + 2, + 184, null, - 120, - 342, - 713, - 342, null, null, null, 1, - 7, - 3, + 5, + 10, null, null, 1, - 3, - null, + 12, null, - 1, null, 1, - 4329, - null, - null, + 0, null, null, 1, 0, - 0, null, - 0, null, + 1, null, + 1, + 7, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/values/file_path.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/repositories/remote_repo.rb": { "lines": [ null, null, @@ -2881,46 +2878,49 @@ 1, 1, null, - 1, null, - 1, null, + null, + null, + null, + 1, 1, null, 1, - 278, - 278, + 3, null, null, 1, - 3996, + 3, null, null, 1, - 2001, + 0, null, - 1995, null, null, - 1, - 152, null, null, - 1, null, - 65, null, - 65, - 65, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/repositories/repo_store.rb": { + "lines": [ null, null, 1, + 1, null, 1, - 278, - 278, - 278, + 1, + 11, + 15, + null, null, + 1, + 15, null, null, null, @@ -2952,8 +2952,8 @@ null, 1, 32, - 2227, - 2227, + 2022, + 2022, null, null, 32, @@ -4302,6 +4302,6 @@ ] } }, - "timestamp": 1766331706 + "timestamp": 1766332625 } } diff --git a/require_worker.rb b/require_worker.rb index 482ecaf..09efa56 100644 --- a/require_worker.rb +++ b/require_worker.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true # Requires all ruby files in specified worker folders -# Worker has its own domain (contributions) and services separate from the API +# Worker has its own domain (contributions), infrastructure (git), and services separate from the API # Params: # - (opt) folders: Array of folder names within workers/, or String of single folder name # Usage: # require_worker -# require_worker(%w[domain]) +# require_worker(%w[domain infrastructure]) # require_worker('services') -def require_worker(folders = %w[domain services]) +def require_worker(folders = %w[domain infrastructure services]) worker_list = Array(folders).map { |folder| "workers/#{folder}" } Dir.glob("./{#{worker_list.join(',')}}/**/*.rb").each do |file| diff --git a/spec/helpers/spec_helper.rb b/spec/helpers/spec_helper.rb index f293bbc..761b402 100644 --- a/spec/helpers/spec_helper.rb +++ b/spec/helpers/spec_helper.rb @@ -16,8 +16,8 @@ require_relative '../../require_app' require_relative '../../require_worker' -require_app # Load API layers -require_worker('domain') # Load worker domain for tests using git infrastructure +require_app # Load API layers +require_worker(%w[domain infrastructure]) # Load worker domain + git infrastructure for tests USERNAME = 'soumyaray' PROJECT_NAME = 'YPBT-app' diff --git a/app/infrastructure/git/gateway/git_command.rb b/workers/infrastructure/git/gateway/git_command.rb similarity index 100% rename from app/infrastructure/git/gateway/git_command.rb rename to workers/infrastructure/git/gateway/git_command.rb diff --git a/app/infrastructure/git/mappers/blame_contributor.rb b/workers/infrastructure/git/mappers/blame_contributor.rb similarity index 100% rename from app/infrastructure/git/mappers/blame_contributor.rb rename to workers/infrastructure/git/mappers/blame_contributor.rb diff --git a/app/infrastructure/git/mappers/contributions_mapper.rb b/workers/infrastructure/git/mappers/contributions_mapper.rb similarity index 100% rename from app/infrastructure/git/mappers/contributions_mapper.rb rename to workers/infrastructure/git/mappers/contributions_mapper.rb diff --git a/app/infrastructure/git/mappers/file_contributions_mapper.rb b/workers/infrastructure/git/mappers/file_contributions_mapper.rb similarity index 100% rename from app/infrastructure/git/mappers/file_contributions_mapper.rb rename to workers/infrastructure/git/mappers/file_contributions_mapper.rb diff --git a/app/infrastructure/git/mappers/folder_contributions_mapper.rb b/workers/infrastructure/git/mappers/folder_contributions_mapper.rb similarity index 100% rename from app/infrastructure/git/mappers/folder_contributions_mapper.rb rename to workers/infrastructure/git/mappers/folder_contributions_mapper.rb diff --git a/app/infrastructure/git/mappers/porcelain_parser.rb b/workers/infrastructure/git/mappers/porcelain_parser.rb similarity index 100% rename from app/infrastructure/git/mappers/porcelain_parser.rb rename to workers/infrastructure/git/mappers/porcelain_parser.rb diff --git a/app/infrastructure/git/repositories/blame_reporter.rb b/workers/infrastructure/git/repositories/blame_reporter.rb similarity index 100% rename from app/infrastructure/git/repositories/blame_reporter.rb rename to workers/infrastructure/git/repositories/blame_reporter.rb diff --git a/app/infrastructure/git/repositories/git_repo.rb b/workers/infrastructure/git/repositories/git_repo.rb similarity index 100% rename from app/infrastructure/git/repositories/git_repo.rb rename to workers/infrastructure/git/repositories/git_repo.rb diff --git a/app/infrastructure/git/repositories/local_repo.rb b/workers/infrastructure/git/repositories/local_repo.rb similarity index 100% rename from app/infrastructure/git/repositories/local_repo.rb rename to workers/infrastructure/git/repositories/local_repo.rb diff --git a/app/infrastructure/git/repositories/remote_repo.rb b/workers/infrastructure/git/repositories/remote_repo.rb similarity index 100% rename from app/infrastructure/git/repositories/remote_repo.rb rename to workers/infrastructure/git/repositories/remote_repo.rb diff --git a/app/infrastructure/git/repositories/repo_file.rb b/workers/infrastructure/git/repositories/repo_file.rb similarity index 100% rename from app/infrastructure/git/repositories/repo_file.rb rename to workers/infrastructure/git/repositories/repo_file.rb diff --git a/app/infrastructure/git/repositories/repo_store.rb b/workers/infrastructure/git/repositories/repo_store.rb similarity index 100% rename from app/infrastructure/git/repositories/repo_store.rb rename to workers/infrastructure/git/repositories/repo_store.rb From 02583527b31f0ef263ad7456584dd464a927e75e Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Mon, 22 Dec 2025 00:13:57 +0800 Subject: [PATCH 11/18] refactor: restructure worker with DDD layers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorganize worker files into DDD-style layers parallel to the API: - application/controllers/worker.rb - Shoryuken entry point - application/services/appraise_project.rb - Business workflow - application/requests/job_reporter.rb - Request parsing + progress - infrastructure/messaging/progress_publisher.rb - Faye HTTP gateway - presentation/values/progress_monitor.rb - Progress calculation Update require_worker.rb to load: domain, infrastructure, presentation, application Update Rakefile and Procfile with new worker path 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Procfile | 2 +- Rakefile | 6 +- coverage/.resultset.json | 169 ++++++++++++++---- require_worker.rb | 10 +- spec/helpers/spec_helper.rb | 4 +- spec/tests/unit/worker_appraise_spec.rb | 2 +- .../controllers/worker.rb} | 9 +- .../requests}/job_reporter.rb | 2 +- .../services/appraise_project.rb | 0 .../messaging}/progress_publisher.rb | 0 .../values/progress_monitor.rb} | 0 11 files changed, 157 insertions(+), 47 deletions(-) rename workers/{git_clone_worker.rb => application/controllers/worker.rb} (92%) rename workers/{ => application/requests}/job_reporter.rb (94%) rename workers/{ => application}/services/appraise_project.rb (100%) rename workers/{ => infrastructure/messaging}/progress_publisher.rb (100%) rename workers/{clone_monitor.rb => presentation/values/progress_monitor.rb} (100%) diff --git a/Procfile b/Procfile index 7b12f85..6437772 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ release: rake db:migrate; rake queues:create web: bundle exec puma -t 5:5 -p ${PORT:-3000} -e ${RACK_ENV:-development} -worker: bundle exec shoryuken -r ./workers/git_clone_worker.rb -C ./workers/shoryuken.yml \ No newline at end of file +worker: bundle exec shoryuken -r ./workers/application/controllers/worker.rb -C ./workers/shoryuken.yml \ No newline at end of file diff --git a/Rakefile b/Rakefile index a2e0284..98e35fe 100644 --- a/Rakefile +++ b/Rakefile @@ -300,17 +300,17 @@ namespace :worker do namespace :run do desc 'Run the background cloning worker in development mode' task :dev => :config do - sh 'RACK_ENV=development bundle exec shoryuken -r ./workers/git_clone_worker.rb -C ./workers/shoryuken_dev.yml' + sh 'RACK_ENV=development bundle exec shoryuken -r ./workers/application/controllers/worker.rb -C ./workers/shoryuken_dev.yml' end desc 'Run the background cloning worker in testing mode' task :test => :config do - sh 'RACK_ENV=test bundle exec shoryuken -r ./workers/git_clone_worker.rb -C ./workers/shoryuken_test.yml' + sh 'RACK_ENV=test bundle exec shoryuken -r ./workers/application/controllers/worker.rb -C ./workers/shoryuken_test.yml' end desc 'Run the background cloning worker in production mode' task :production => :config do - sh 'RACK_ENV=production bundle exec shoryuken -r ./workers/git_clone_worker.rb -C ./workers/shoryuken.yml' + sh 'RACK_ENV=production bundle exec shoryuken -r ./workers/application/controllers/worker.rb -C ./workers/shoryuken.yml' end end end diff --git a/coverage/.resultset.json b/coverage/.resultset.json index f476012..5615a35 100644 --- a/coverage/.resultset.json +++ b/coverage/.resultset.json @@ -33,11 +33,15 @@ null, null, null, + null, + null, + null, + null, 1, - 3, + 4, null, 1, - 22, + 24, null, null ] @@ -247,7 +251,7 @@ null, null, 1, - 10, + 11, null, null, 1, @@ -2371,16 +2375,16 @@ 1, null, 1, - 65, - 65, - 65, - 65, + 66, + 66, + 66, + 66, null, null, 1, - 1, - 1, - 1, + 2, + 2, + 2, null, null, 1, @@ -2395,21 +2399,21 @@ null, null, 1, - 1, - 1, + 2, + 2, null, null, 1, - 1, - 1, + 2, + 2, null, null, 1, - 130, + 132, null, null, 1, - 65, + 66, null, null, null, @@ -2420,8 +2424,8 @@ null, null, 1, - 0, - 0, + 1, + 5, null, null, null, @@ -2789,14 +2793,14 @@ null, null, 1, - 5, + 6, null, null, 1, - 0, - 0, + 1, + 1, null, - 0, + 6, null, null, null @@ -2828,8 +2832,8 @@ null, null, 1, - 0, - 0, + 6, + 1, null, null, 1, @@ -2848,7 +2852,7 @@ null, null, 1, - 12, + 13, null, null, 1, @@ -2895,7 +2899,7 @@ null, null, 1, - 0, + 1, null, null, null, @@ -2927,6 +2931,111 @@ null ] }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/messaging/progress_publisher.rb": { + "lines": [ + null, + null, + 1, + null, + 1, + null, + 1, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + null, + 0, + null, + null, + 1, + null, + 1, + 0, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/presentation/values/progress_monitor.rb": { + "lines": [ + null, + null, + 1, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + null + ] + }, "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/helpers/vcr_helper.rb": { "lines": [ null, @@ -2952,8 +3061,8 @@ null, 1, 32, - 2022, - 2022, + 2247, + 2247, null, null, 32, @@ -4144,7 +4253,7 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/services/appraise_project.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/application/services/appraise_project.rb": { "lines": [ null, null, @@ -4302,6 +4411,6 @@ ] } }, - "timestamp": 1766332625 + "timestamp": 1766333574 } } diff --git a/require_worker.rb b/require_worker.rb index 09efa56..7ae1c41 100644 --- a/require_worker.rb +++ b/require_worker.rb @@ -1,14 +1,18 @@ # frozen_string_literal: true # Requires all ruby files in specified worker folders -# Worker has its own domain (contributions), infrastructure (git), and services separate from the API +# Worker has its own DDD layers parallel to the API: +# - domain: contributions entities/values/lib +# - infrastructure: git gateway/repositories/mappers, messaging +# - presentation: progress monitor values +# - application: controllers, services, requests # Params: # - (opt) folders: Array of folder names within workers/, or String of single folder name # Usage: # require_worker # require_worker(%w[domain infrastructure]) -# require_worker('services') -def require_worker(folders = %w[domain infrastructure services]) +# require_worker('application') +def require_worker(folders = %w[domain infrastructure presentation application]) worker_list = Array(folders).map { |folder| "workers/#{folder}" } Dir.glob("./{#{worker_list.join(',')}}/**/*.rb").each do |file| diff --git a/spec/helpers/spec_helper.rb b/spec/helpers/spec_helper.rb index 761b402..31d2d81 100644 --- a/spec/helpers/spec_helper.rb +++ b/spec/helpers/spec_helper.rb @@ -16,8 +16,8 @@ require_relative '../../require_app' require_relative '../../require_worker' -require_app # Load API layers -require_worker(%w[domain infrastructure]) # Load worker domain + git infrastructure for tests +require_app # Load API layers +require_worker(%w[domain infrastructure presentation]) # Load worker layers for tests (no application) USERNAME = 'soumyaray' PROJECT_NAME = 'YPBT-app' diff --git a/spec/tests/unit/worker_appraise_spec.rb b/spec/tests/unit/worker_appraise_spec.rb index 3ce1628..7bba4f3 100644 --- a/spec/tests/unit/worker_appraise_spec.rb +++ b/spec/tests/unit/worker_appraise_spec.rb @@ -2,7 +2,7 @@ require_relative '../../helpers/spec_helper' require_relative '../../helpers/cache_helper' -require_relative '../../../workers/services/appraise_project' +require_relative '../../../workers/application/services/appraise_project' describe 'Unit test of Worker::AppraiseProject' do before do diff --git a/workers/git_clone_worker.rb b/workers/application/controllers/worker.rb similarity index 92% rename from workers/git_clone_worker.rb rename to workers/application/controllers/worker.rb index af1571d..d60abe0 100644 --- a/workers/git_clone_worker.rb +++ b/workers/application/controllers/worker.rb @@ -1,13 +1,10 @@ # frozen_string_literal: true -require_relative '../require_app' -require_relative '../require_worker' +require_relative '../../../require_app' +require_relative '../../../require_worker' require_app # Load API layers (domain, infrastructure, presentation, application) -require_worker # Load worker-only layers (domain, services) - -require_relative 'clone_monitor' -require_relative 'job_reporter' +require_worker # Load worker-only layers (domain, infrastructure, presentation, application) require 'figaro' require 'shoryuken' diff --git a/workers/job_reporter.rb b/workers/application/requests/job_reporter.rb similarity index 94% rename from workers/job_reporter.rb rename to workers/application/requests/job_reporter.rb index c9883b5..994f6d4 100644 --- a/workers/job_reporter.rb +++ b/workers/application/requests/job_reporter.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'progress_publisher' +# Note: ProgressPublisher is loaded via require_worker (infrastructure/messaging) module GitClone # Reports job progress to client diff --git a/workers/services/appraise_project.rb b/workers/application/services/appraise_project.rb similarity index 100% rename from workers/services/appraise_project.rb rename to workers/application/services/appraise_project.rb diff --git a/workers/progress_publisher.rb b/workers/infrastructure/messaging/progress_publisher.rb similarity index 100% rename from workers/progress_publisher.rb rename to workers/infrastructure/messaging/progress_publisher.rb diff --git a/workers/clone_monitor.rb b/workers/presentation/values/progress_monitor.rb similarity index 100% rename from workers/clone_monitor.rb rename to workers/presentation/values/progress_monitor.rb From f56b47b2e86afd996cb13f49046b6d3db89a861b Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Mon, 22 Dec 2025 00:19:59 +0800 Subject: [PATCH 12/18] docs: add architecture section to README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add brief architecture overview explaining API/Worker separation and parallel DDD layer structures. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 5793b6d..448ae24 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,17 @@ rake spec # Run unit and integration tests bash spec/acceptance_tests # Run full acceptance tests (starts worker automatically) ``` +## Architecture + +This project uses **Clean Architecture** with separate DDD-style layers for the API and background worker: + +- **API (`app/`)**: Handles HTTP requests, database, GitHub integration +- **Worker (`workers/`)**: Handles git clone/blame operations, Redis caching + +Both use parallel layer structures: `domain/`, `infrastructure/`, `presentation/`, `application/` + +See `CLAUDE.md` for detailed architecture documentation. + ## Routes ### Root check From d9f51cb57c4317c74a6e397e2cad8f4a4b427e1e Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Mon, 22 Dec 2025 01:54:02 +0800 Subject: [PATCH 13/18] chore: renamed worker to Appraiser --- coverage/.resultset.json | 6 +++--- workers/application/controllers/worker.rb | 2 +- workers/application/requests/job_reporter.rb | 2 +- workers/infrastructure/messaging/progress_publisher.rb | 2 +- workers/presentation/values/progress_monitor.rb | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/coverage/.resultset.json b/coverage/.resultset.json index 5615a35..e179e51 100644 --- a/coverage/.resultset.json +++ b/coverage/.resultset.json @@ -3061,8 +3061,8 @@ null, 1, 32, - 2247, - 2247, + 2397, + 2397, null, null, 32, @@ -4411,6 +4411,6 @@ ] } }, - "timestamp": 1766333574 + "timestamp": 1766339428 } } diff --git a/workers/application/controllers/worker.rb b/workers/application/controllers/worker.rb index d60abe0..48ef94d 100644 --- a/workers/application/controllers/worker.rb +++ b/workers/application/controllers/worker.rb @@ -9,7 +9,7 @@ require 'figaro' require 'shoryuken' -module GitClone +module Appraiser # Shoryuken worker class to clone repos and appraise contributions class Worker # Environment variables setup diff --git a/workers/application/requests/job_reporter.rb b/workers/application/requests/job_reporter.rb index 994f6d4..686ecd2 100644 --- a/workers/application/requests/job_reporter.rb +++ b/workers/application/requests/job_reporter.rb @@ -2,7 +2,7 @@ # Note: ProgressPublisher is loaded via require_worker (infrastructure/messaging) -module GitClone +module Appraiser # Reports job progress to client class JobReporter attr_reader :project, :folder_path diff --git a/workers/infrastructure/messaging/progress_publisher.rb b/workers/infrastructure/messaging/progress_publisher.rb index 5dd2d5a..beef60b 100644 --- a/workers/infrastructure/messaging/progress_publisher.rb +++ b/workers/infrastructure/messaging/progress_publisher.rb @@ -2,7 +2,7 @@ require 'http' -module GitClone +module Appraiser # Publishes progress as percent to Faye endpoint class ProgressPublisher def initialize(config, channel_id) diff --git a/workers/presentation/values/progress_monitor.rb b/workers/presentation/values/progress_monitor.rb index 1697f90..cfd7ffd 100644 --- a/workers/presentation/values/progress_monitor.rb +++ b/workers/presentation/values/progress_monitor.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module GitClone +module Appraiser # Infrastructure to clone while yielding progress # Legacy module for backwards compatibility module CloneMonitor From a5ea58d106d97e22e1dfac8d0e39f036015b1609 Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Mon, 22 Dec 2025 08:37:36 +0800 Subject: [PATCH 14/18] refactor: remove Rack::Cache reverse-proxy caching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove rack-cache and redis-rack-cache gems - Delete local_cache.rb file-based cache (was for Rack::Cache) - Remove Rack::Cache middleware configuration from environment.rb - Simplify cache rake tasks (remove Rack::Cache specific tasks) - Update redis_cache.rb and cache_helper.rb comments - Remove LOCAL_CACHE config from secrets_example Redis is now used directly for appraisal caching via Cache::Remote, eliminating the need for HTTP reverse-proxy caching layer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .gitignore | 1 - Gemfile | 2 - Gemfile.lock | 42 +++++------- Rakefile | 47 ++++--------- app/infrastructure/cache/local_cache.rb | 29 --------- app/infrastructure/cache/redis_cache.rb | 7 +- config/environment.rb | 20 ------ config/secrets_example.yml | 3 - spec/helpers/cache_helper.rb | 6 +- spec/tests/unit/local_cache_spec.rb | 87 ------------------------- 10 files changed, 33 insertions(+), 211 deletions(-) delete mode 100644 app/infrastructure/cache/local_cache.rb delete mode 100644 spec/tests/unit/local_cache_spec.rb diff --git a/.gitignore b/.gitignore index 30e6906..9115917 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,4 @@ coverage/* !coverage/.resultset.json *.db repostore/**/ -_cache/ spec/logs/ \ No newline at end of file diff --git a/Gemfile b/Gemfile index bc60558..959f262 100644 --- a/Gemfile +++ b/Gemfile @@ -28,9 +28,7 @@ gem 'dry-transaction', '~> 0' gem 'dry-validation', '~> 1.0' # Caching -gem 'rack-cache', '~> 1.13' gem 'redis', '~> 4.8' -gem 'redis-rack-cache', '~> 2.2' # DOMAIN LAYER # Validation diff --git a/Gemfile.lock b/Gemfile.lock index 55494e4..86cb1e6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,8 +5,8 @@ GEM public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) aws-eventstream (1.4.0) - aws-partitions (1.1190.0) - aws-sdk-core (3.239.2) + aws-partitions (1.1198.0) + aws-sdk-core (3.240.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -22,7 +22,7 @@ GEM base64 (0.3.0) bigdecimal (3.3.1) coderay (1.1.3) - concurrent-ruby (1.3.5) + concurrent-ruby (1.3.6) cookiejar (0.3.4) crack (1.0.1) bigdecimal @@ -113,9 +113,9 @@ GEM rake figaro (1.3.0) thor (>= 0.14.0, < 2) - flog (4.8.0) - path_expander (~> 1.0) - ruby_parser (~> 3.1, > 3.1.0) + flog (4.9.1) + path_expander (~> 2.0) + prism (~> 1.7) sexp_processor (~> 4.8) hashdiff (1.2.1) hirb (0.7.3) @@ -130,7 +130,7 @@ GEM http_parser.rb (0.8.0) ice_nine (0.11.2) jmespath (1.6.2) - json (2.17.1) + json (2.18.0) language_server-protocol (3.17.0.5) lint_roller (1.1.0) listen (3.9.0) @@ -142,7 +142,7 @@ GEM logger (1.7.0) method_source (1.1.0) mini_portile2 (2.8.9) - minitest (5.26.2) + minitest (5.27.0) minitest-rg (5.3.0) minitest (~> 5.0) multi_json (1.18.0) @@ -153,11 +153,11 @@ GEM parser (3.3.10.0) ast (~> 2.4.1) racc - path_expander (1.1.3) + path_expander (2.0.0) pg (1.6.2) pg (1.6.2-arm64-darwin) pg (1.6.2-x86_64-linux) - prism (1.6.0) + prism (1.7.0) pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) @@ -166,8 +166,6 @@ GEM nio4r (~> 2.0) racc (1.8.1) rack (3.2.4) - rack-cache (1.17.0) - rack (>= 0.4) rack-session (0.3.0) rack (>= 3.0.0.beta1) rack-test (2.2.0) @@ -178,11 +176,6 @@ GEM rb-inotify (0.11.1) ffi (~> 1.0) redis (4.8.1) - redis-rack-cache (2.2.1) - rack-cache (>= 1.10, < 2) - redis-store (>= 1.6, < 2) - redis-store (1.11.0) - redis (>= 4, < 6) reek (6.5.0) dry-schema (~> 1.13) logger (~> 1.6) @@ -199,9 +192,9 @@ GEM rexml (3.4.4) roar (1.2.0) representable (~> 3.1) - roda (3.98.0) + roda (3.99.0) rack - rubocop (1.81.7) + rubocop (1.82.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -209,7 +202,7 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.47.1, < 2.0) + rubocop-ast (>= 1.48.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.48.0) @@ -226,9 +219,6 @@ GEM lint_roller (~> 1.1) rubocop (>= 1.72.1, < 2) ruby-progressbar (1.13.0) - ruby_parser (3.21.1) - racc (~> 1.5) - sexp_processor (~> 4.16) sequel (5.99.0) bigdecimal sexp_processor (4.17.4) @@ -249,7 +239,7 @@ GEM uber (0.1.0) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) - unicode-emoji (4.1.0) + unicode-emoji (4.2.0) vcr (6.3.1) base64 webmock (3.26.1) @@ -260,7 +250,7 @@ GEM base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - zeitwerk (2.7.3) + zeitwerk (2.7.4) PLATFORMS arm64-darwin-24 @@ -290,12 +280,10 @@ DEPENDENCIES pg (~> 1.0) pry puma (~> 6.0) - rack-cache (~> 1.13) rack-session (~> 0) rack-test rake (~> 13.0) redis (~> 4.8) - redis-rack-cache (~> 2.2) reek rerun roar (~> 1.1) diff --git a/Rakefile b/Rakefile index 98e35fe..4f63bc0 100644 --- a/Rakefile +++ b/Rakefile @@ -38,7 +38,7 @@ end desc 'Keep restarting web app in dev mode upon changes' task :rerun do - sh "rerun -c --ignore 'coverage/*' --ignore 'repostore/*' --ignore '_cache/*' -- bundle exec puma -p 9090" + sh "rerun -c --ignore 'coverage/*' --ignore 'repostore/*'' -- bundle exec puma -p 9090" end namespace :db do @@ -204,47 +204,26 @@ end namespace :cache do task :config do # rubocop:disable Rake/Desc - require_relative 'app/infrastructure/cache/local_cache' require_relative 'app/infrastructure/cache/redis_cache' require_relative 'config/environment' # load config info @api = CodePraise::App end desc 'Directory listing of local dev cache' - namespace :list do - desc 'Lists development cache' - task :dev => :config do - puts 'Lists development cache' - keys = CodePraise::Cache::Local.new(@api.config).keys - puts 'No local cache found' if keys.none? - keys.each { |key| puts "Key: #{key}" } - end - - desc 'Lists production cache' - task :production => :config do - puts 'Finding production cache' - keys = CodePraise::Cache::Remote.new(@api.config).keys - puts 'No keys found' if keys.none? - keys.each { |key| puts "Key: #{key}" } - end + task :list => :config do + puts 'Finding production cache' + keys = CodePraise::Cache::Remote.new(@api.config).keys + puts 'No keys found' if keys.none? + keys.each { |key| puts "Key: #{key}" } end - namespace :wipe do - desc 'Delete development cache' - task :dev => :config do - puts 'Deleting development cache' - CodePraise::Cache::Local.new(@api.config).wipe - puts 'Development cache wiped' - end - - desc 'Delete production cache' - task :production => :config do - print 'Are you sure you wish to wipe the production cache? (y/n) ' - if $stdin.gets.chomp.downcase == 'y' - puts 'Deleting production cache' - wiped = CodePraise::Cache::Remote.new(@api.config).wipe - wiped.each { |key| puts "Wiped: #{key}" } - end + desc 'Wipe cache' + task :wipe => :config do + print 'Are you sure you wish to wipe the production cache? (y/n) ' + if $stdin.gets.chomp.downcase == 'y' + puts 'Deleting production cache' + wiped = CodePraise::Cache::Remote.new(@api.config).wipe + wiped.each { |key| puts "Wiped: #{key}" } end end end diff --git a/app/infrastructure/cache/local_cache.rb b/app/infrastructure/cache/local_cache.rb deleted file mode 100644 index a9f6d8b..0000000 --- a/app/infrastructure/cache/local_cache.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -require 'fileutils' - -module CodePraise - module Cache - # Local disk cache utility - class Local - def initialize(config) - @cache_dir = config.LOCAL_CACHE - ensure_cache_directory - end - - def keys - Dir.glob("#{@cache_dir}/**/*").select { |f| File.file?(f) } - end - - def wipe - FileUtils.rm_rf(Dir.glob("#{@cache_dir}/*")) - end - - private - - def ensure_cache_directory - FileUtils.mkdir_p(@cache_dir) - end - end - end -end diff --git a/app/infrastructure/cache/redis_cache.rb b/app/infrastructure/cache/redis_cache.rb index 9367d4f..4d323ed 100644 --- a/app/infrastructure/cache/redis_cache.rb +++ b/app/infrastructure/cache/redis_cache.rb @@ -4,11 +4,10 @@ module CodePraise module Cache - # Redis client utility for caching with TTL support + # Redis client utility for caching appraisal results with TTL support # Uses key prefixes to separate namespaces: - # - 'appraisal:' prefix for appraisal cache - # - 'test:appraisal:' prefix for test environment - # This avoids conflicts with Rack::Cache which uses its own key patterns + # - 'appraisal:' prefix for appraisal cache (production/development) + # - 'test:appraisal:' prefix for test environment isolation class Remote def initialize(config) @redis = Redis.new(url: config.REDISCLOUD_URL) diff --git a/config/environment.rb b/config/environment.rb index 1a3ee2b..4f1cc69 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -5,8 +5,6 @@ require 'rack/session' require 'roda' require 'sequel' -require 'rack/cache' -require 'redis-rack-cache' module CodePraise # Environment-specific configuration @@ -25,24 +23,6 @@ def self.config = Figaro.env plugin :common_logger, $stderr end - # Setup Cacheing mechanism - configure :development do - use Rack::Cache, - verbose: true, - metastore: "#{config.LOCAL_CACHE}/meta", - entitystore: "#{config.LOCAL_CACHE}/body" - end - - configure :production do - puts 'RUNNING IN PRODUCTION MODE' - # Set DATABASE_URL environment variable on production platform - - use Rack::Cache, - verbose: true, - metastore: "#{config.REDISCLOUD_URL}/0/metastore", - entitystore: "#{config.REDISCLOUD_URL}/0/entitystore" - end - # Automated HTTP stubbing for testing only configure :app_test do require_relative '../spec/helpers/vcr_helper' diff --git a/config/secrets_example.yml b/config/secrets_example.yml index 333a6c6..3ec7c9e 100644 --- a/config/secrets_example.yml +++ b/config/secrets_example.yml @@ -5,7 +5,6 @@ development: DB_FILENAME: db/local/dev.db GITHUB_TOKEN: create_github_token REPOSTORE_PATH: repostore - LOCAL_CACHE: _cache/rack API_HOST: http://localhost:9090 REDISCLOUD_URL: redis://localhost:6379 AWS_ACCESS_KEY_ID: @@ -19,7 +18,6 @@ development: app_test: DB_FILENAME: db/local/test.db REPOSTORE_PATH: repostore - LOCAL_CACHE: _cache/rack API_HOST: http://localhost:9090 REDISCLOUD_URL: redis://localhost:6379 AWS_ACCESS_KEY_ID: @@ -34,7 +32,6 @@ test: DB_FILENAME: db/local/test.db GITHUB_TOKEN: create_github_token REPOSTORE_PATH: repostore - LOCAL_CACHE: _cache/rack API_HOST: http://localhost:9090 REDISCLOUD_URL: redis://localhost:6379 AWS_ACCESS_KEY_ID: diff --git a/spec/helpers/cache_helper.rb b/spec/helpers/cache_helper.rb index 34c921a..f253de8 100644 --- a/spec/helpers/cache_helper.rb +++ b/spec/helpers/cache_helper.rb @@ -1,13 +1,11 @@ # frozen_string_literal: true # Helper for Redis cache testing -# Uses real Redis with database 2 (test database) to avoid conflicts with: -# - Database 0: Rack::Cache (reverse proxy) -# - Database 1: Appraisal cache (production/development) +# Uses real Redis with 'test:' key prefix for isolation from production data module CacheHelper # Create a cache instance for testing # Uses App.config which points to real Redis, but Cache::Remote - # automatically uses database 2 when RACK_ENV=test + # automatically uses 'test:' key prefix when RACK_ENV=test def self.create_test_cache CodePraise::Cache::Remote.new(CodePraise::App.config) end diff --git a/spec/tests/unit/local_cache_spec.rb b/spec/tests/unit/local_cache_spec.rb deleted file mode 100644 index 3be3f3f..0000000 --- a/spec/tests/unit/local_cache_spec.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true - -require_relative '../../helpers/spec_helper' -require 'fileutils' - -describe 'Unit test of LocalCache' do - before do - # SAFETY: Create a mock config that returns a TEST cache directory - # This prevents tests from touching the real cache at _cache/rack - @test_cache_dir = 'spec/fixtures/test_cache' - @config = Minitest::Mock.new - @config.expect(:LOCAL_CACHE, @test_cache_dir) - - # When LocalCache.new(@config) is called, it will use @test_cache_dir - # instead of the real LOCAL_CACHE value from secrets.yml - end - - after do - # Clean up the temporary test cache directory after each test - # The real cache directory (_cache/rack) is never touched - FileUtils.rm_rf(@test_cache_dir) - end - - it 'should create cache directory if it does not exist' do - CodePraise::Cache::Local.new(@config) - - _(Dir.exist?(@test_cache_dir)).must_equal true - end - - it 'should list keys when cache has files' do - cache = CodePraise::Cache::Local.new(@config) - - # Create some test files in the TEST directory - meta_dir = "#{@test_cache_dir}/meta" - body_dir = "#{@test_cache_dir}/body" - FileUtils.mkdir_p(meta_dir) - FileUtils.mkdir_p(body_dir) - FileUtils.touch("#{meta_dir}/test1") - FileUtils.touch("#{body_dir}/test2") - - keys = cache.keys - - _(keys.length).must_equal 2 - _(keys).must_include "#{meta_dir}/test1" - _(keys).must_include "#{body_dir}/test2" - end - - it 'should return empty array when no cache files exist' do - cache = CodePraise::Cache::Local.new(@config) - - keys = cache.keys - - _(keys).must_be_empty - end - - it 'should wipe all cache files' do - cache = CodePraise::Cache::Local.new(@config) - - # Create some test files in the TEST directory - meta_dir = "#{@test_cache_dir}/meta" - body_dir = "#{@test_cache_dir}/body" - FileUtils.mkdir_p(meta_dir) - FileUtils.mkdir_p(body_dir) - FileUtils.touch("#{meta_dir}/test1") - FileUtils.touch("#{body_dir}/test2") - - # Verify files exist before wipe - _(cache.keys.length).must_equal 2 - - # Wipe cache - only affects TEST directory, not real cache - cache.wipe - - # Verify files are gone - _(cache.keys).must_be_empty - # But directory should still exist (wipe removes contents, not directory) - _(Dir.exist?(@test_cache_dir)).must_equal true - end - - it 'should handle wipe when cache is already empty' do - cache = CodePraise::Cache::Local.new(@config) - - # Should not raise error when wiping empty cache - cache.wipe - - _(cache.keys).must_be_empty - end -end From c4cecc2c7aef3efae7776061577c6310bfb0314d Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Mon, 22 Dec 2025 16:05:28 +0800 Subject: [PATCH 15/18] refactor: use Redis database indexes and rename queue env vars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cache infrastructure: - Use Redis database indexes for environment isolation (dev=/0, test=/1) - Rename REDISCLOUD_URL → REDIS_URL for consistency - Remove key prefix logic from Cache::Remote (simpler implementation) - Consolidate rake tasks under 'cache' namespace (technology-agnostic) - cache:status, cache:ensure, cache:list, cache:wipe - cache:redis:start, cache:redis:stop, cache:redis:remove Queue infrastructure: - Rename CLONE_QUEUE → WORKER_QUEUE (reflects broader responsibility) - Rename CLONE_QUEUE_URL → WORKER_QUEUE_URL - Update shoryuken config files with new queue names - Fix queues:create task to work before queue exists Testing: - Use `bash spec/acceptance_tests` for full test suite with worker - 75 runs, 201 assertions, 0 failures, 93.21% coverage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/api.yml | 4 +- Rakefile | 195 +++++++++--------- .../services/fetch_or_request_appraisal.rb | 2 +- .../cache/{redis_cache.rb => remote_cache.rb} | 24 +-- app/infrastructure/messaging/queue.rb | 4 +- config/secrets_example.yml | 35 ++-- coverage/.resultset.json | 163 +-------------- spec/acceptance_tests | 6 +- spec/helpers/cache_helper.rb | 6 +- spec/tests/unit/remote_cache_spec.rb | 8 +- workers/application/controllers/worker.rb | 2 +- workers/shoryuken.yml | 2 +- workers/shoryuken_dev.yml | 2 +- workers/shoryuken_test.yml | 2 +- 14 files changed, 152 insertions(+), 303 deletions(-) rename app/infrastructure/cache/{redis_cache.rb => remote_cache.rb} (62%) diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index ab19be0..cbbeac0 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -74,8 +74,8 @@ jobs: DB_FILENAME: ${{ secrets.DB_FILENAME }} REPOSTORE_PATH: ${{ secrets.REPOSTORE_PATH }} API_HOST: ${{ secrets.API_HOST }} - REDISCLOUD_URL: redis://localhost:6379 - CLONE_QUEUE_URL: ${{ secrets.CLONE_QUEUE_URL }} + REDIS_URL: redis://localhost:6379/1 + WORKER_QUEUE_URL: ${{ secrets.WORKER_QUEUE_URL }} AWS_REGION: ${{ secrets.AWS_REGION }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/Rakefile b/Rakefile index 4f63bc0..9003c6a 100644 --- a/Rakefile +++ b/Rakefile @@ -13,8 +13,8 @@ Rake::TestTask.new(:spec_only) do |t| t.warning = false end -# Run specs with Redis check -task spec: ['redis:ensure', :spec_only] +# Run specs with cache check +task spec: ['cache:ensure', :spec_only] desc 'Keep rerunning unit/integration tests upon changes' task :respec do @@ -111,125 +111,132 @@ namespace :repos do end end -namespace :redis do - CONTAINER_NAME = 'redis-codepraise' +namespace :cache do + REDIS_CONTAINER = 'redis-codepraise' task :config do # rubocop:disable Rake/Desc require 'redis' require_relative 'config/environment' + require_relative 'app/infrastructure/cache/remote_cache' @api = CodePraise::App end - desc 'Check Redis connectivity' + desc 'Check cache server connectivity' task status: :config do - redis_url = @api.config.REDISCLOUD_URL - puts "Checking Redis at: #{redis_url}" + redis_url = @api.config.REDIS_URL + puts "Environment: #{@api.environment}" + puts "Checking cache at: #{redis_url}" redis = Redis.new(url: redis_url) response = redis.ping - puts "Redis responded: #{response}" - puts 'Redis connection successful!' + puts "Cache responded: #{response}" + puts 'Cache connection successful!' rescue Redis::CannotConnectError => e - puts "Redis connection FAILED: #{e.message}" + puts "Cache connection FAILED: #{e.message}" puts '' puts 'To start Redis locally:' - puts ' rake redis:start' + puts ' rake cache:redis:start' exit 1 end - desc 'Start Redis Docker container' - task :start do - # Check if container exists - container_exists = system("docker ps -a --format '{{.Names}}' | grep -q '^#{CONTAINER_NAME}$'") - - if container_exists - # Container exists, check if running - container_running = system("docker ps --format '{{.Names}}' | grep -q '^#{CONTAINER_NAME}$'") - if container_running - puts "Redis container '#{CONTAINER_NAME}' is already running" - else - puts "Starting existing Redis container '#{CONTAINER_NAME}'..." - sh "docker start #{CONTAINER_NAME}" - end - else - # Create and start new container - puts "Creating and starting Redis container '#{CONTAINER_NAME}'..." - sh "docker run -d --name #{CONTAINER_NAME} -p 6379:6379 redis:latest" - end + desc 'Ensure cache is running (start if needed)' + task :ensure do + require 'redis' + require_relative 'config/environment' + redis_url = CodePraise::App.config.REDIS_URL - # Wait for Redis to be ready - puts 'Waiting for Redis to be ready...' - sleep 2 - Rake::Task['redis:status'].invoke + redis = Redis.new(url: redis_url) + redis.ping + puts 'Cache is running' + rescue Redis::CannotConnectError + puts 'Cache not running, starting Redis container...' + Rake::Task['cache:redis:start'].invoke end - desc 'Stop Redis Docker container' - task :stop do - container_running = system("docker ps --format '{{.Names}}' | grep -q '^#{CONTAINER_NAME}$'") - if container_running - puts "Stopping Redis container '#{CONTAINER_NAME}'..." - sh "docker stop #{CONTAINER_NAME}" - puts 'Redis container stopped' + desc 'List all cached keys' + task list: :config do + puts "Environment: #{@api.environment}" + keys = CodePraise::Cache::Remote.new(@api.config).keys + if keys.none? + puts 'No keys found' else - puts "Redis container '#{CONTAINER_NAME}' is not running" + keys.each { |key| puts " #{key}" } end end - desc 'Remove Redis Docker container' - task :remove do - Rake::Task['redis:stop'].invoke - container_exists = system("docker ps -a --format '{{.Names}}' | grep -q '^#{CONTAINER_NAME}$'") - if container_exists - puts "Removing Redis container '#{CONTAINER_NAME}'..." - sh "docker rm #{CONTAINER_NAME}" - puts 'Redis container removed' + desc 'Wipe all cached keys' + task wipe: :config do + env = @api.environment + if env == :production + print 'Are you sure you wish to wipe the PRODUCTION cache? (y/n) ' + return unless $stdin.gets.chomp.downcase == 'y' + end + + puts "Wiping #{env} cache..." + wiped = CodePraise::Cache::Remote.new(@api.config).wipe + if wiped.empty? + puts 'No keys to wipe' else - puts "Redis container '#{CONTAINER_NAME}' does not exist" + wiped.each { |key| puts " Wiped: #{key}" } end end - desc 'Ensure Redis is running (start if needed)' - task :ensure do - require 'redis' - require_relative 'config/environment' - redis_url = CodePraise::App.config.REDISCLOUD_URL - - redis = Redis.new(url: redis_url) - redis.ping - puts 'Redis is running' - rescue Redis::CannotConnectError - puts 'Redis not running, starting Docker container...' - Rake::Task['redis:start'].invoke - end -end + # Redis-specific container management (for local development) + namespace :redis do + desc 'Start Redis Docker container' + task :start do + # Check if container exists + container_exists = system("docker ps -a --format '{{.Names}}' | grep -q '^#{REDIS_CONTAINER}$'") + + if container_exists + # Container exists, check if running + container_running = system("docker ps --format '{{.Names}}' | grep -q '^#{REDIS_CONTAINER}$'") + if container_running + puts "Redis container '#{REDIS_CONTAINER}' is already running" + else + puts "Starting existing Redis container '#{REDIS_CONTAINER}'..." + sh "docker start #{REDIS_CONTAINER}" + end + else + # Create and start new container + puts "Creating and starting Redis container '#{REDIS_CONTAINER}'..." + sh "docker run -d --name #{REDIS_CONTAINER} -p 6379:6379 redis:latest" + end -namespace :cache do - task :config do # rubocop:disable Rake/Desc - require_relative 'app/infrastructure/cache/redis_cache' - require_relative 'config/environment' # load config info - @api = CodePraise::App - end + # Wait for Redis to be ready + puts 'Waiting for Redis to be ready...' + sleep 2 + Rake::Task['cache:status'].invoke + end - desc 'Directory listing of local dev cache' - task :list => :config do - puts 'Finding production cache' - keys = CodePraise::Cache::Remote.new(@api.config).keys - puts 'No keys found' if keys.none? - keys.each { |key| puts "Key: #{key}" } - end + desc 'Stop Redis Docker container' + task :stop do + container_running = system("docker ps --format '{{.Names}}' | grep -q '^#{REDIS_CONTAINER}$'") + if container_running + puts "Stopping Redis container '#{REDIS_CONTAINER}'..." + sh "docker stop #{REDIS_CONTAINER}" + puts 'Redis container stopped' + else + puts "Redis container '#{REDIS_CONTAINER}' is not running" + end + end - desc 'Wipe cache' - task :wipe => :config do - print 'Are you sure you wish to wipe the production cache? (y/n) ' - if $stdin.gets.chomp.downcase == 'y' - puts 'Deleting production cache' - wiped = CodePraise::Cache::Remote.new(@api.config).wipe - wiped.each { |key| puts "Wiped: #{key}" } + desc 'Remove Redis Docker container' + task :remove do + Rake::Task['cache:redis:stop'].invoke + container_exists = system("docker ps -a --format '{{.Names}}' | grep -q '^#{REDIS_CONTAINER}$'") + if container_exists + puts "Removing Redis container '#{REDIS_CONTAINER}'..." + sh "docker rm #{REDIS_CONTAINER}" + puts 'Redis container removed' + else + puts "Redis container '#{REDIS_CONTAINER}' does not exist" + end end end end namespace :queues do - task :config do + task :config do # rubocop:disable Rake/Desc require 'aws-sdk-sqs' require_relative 'config/environment' # load config info @api = CodePraise::App @@ -238,26 +245,28 @@ namespace :queues do secret_access_key: @api.config.AWS_SECRET_ACCESS_KEY, region: @api.config.AWS_REGION ) - @q_name = @api.config.CLONE_QUEUE - @q_url = @sqs.get_queue_url(queue_name: @q_name).queue_url - + @q_name = @api.config.WORKER_QUEUE puts "Environment: #{@api.environment}" end + task :get_url => :config do # rubocop:disable Rake/Desc + @q_url = @sqs.get_queue_url(queue_name: @q_name).queue_url + end + desc 'Create SQS queue for worker' task :create => :config do - @sqs.create_queue(queue_name: @q_name) + result = @sqs.create_queue(queue_name: @q_name) puts 'Queue created:' puts " Name: #{@q_name}" puts " Region: #{@api.config.AWS_REGION}" - puts " URL: #{@q_url}" + puts " URL: #{result.queue_url}" rescue StandardError => e puts "Error creating queue: #{e}" end desc 'Report status of queue for worker' - task :status => :config do + task :status => :get_url do puts 'Queue info:' puts " Name: #{@q_name}" puts " Region: #{@api.config.AWS_REGION}" @@ -267,7 +276,7 @@ namespace :queues do end desc 'Purge messages in SQS queue for worker' - task :purge => :config do + task :purge => :get_url do @sqs.purge_queue(queue_url: @q_url) puts "Queue #{@q_name} purged" rescue StandardError => e diff --git a/app/application/services/fetch_or_request_appraisal.rb b/app/application/services/fetch_or_request_appraisal.rb index a3ff910..5f31763 100644 --- a/app/application/services/fetch_or_request_appraisal.rb +++ b/app/application/services/fetch_or_request_appraisal.rb @@ -68,7 +68,7 @@ def request_appraisal_worker(input) return Success(input) if input[:cache_hit] # Cache miss - send request to worker - Messaging::Queue.new(App.config.CLONE_QUEUE_URL, App.config) + Messaging::Queue.new(App.config.WORKER_QUEUE_URL, App.config) .send(appraisal_request_json(input)) Failure(Response::ApiResult.new( diff --git a/app/infrastructure/cache/redis_cache.rb b/app/infrastructure/cache/remote_cache.rb similarity index 62% rename from app/infrastructure/cache/redis_cache.rb rename to app/infrastructure/cache/remote_cache.rb index 4d323ed..83000de 100644 --- a/app/infrastructure/cache/redis_cache.rb +++ b/app/infrastructure/cache/remote_cache.rb @@ -5,13 +5,13 @@ module CodePraise module Cache # Redis client utility for caching appraisal results with TTL support - # Uses key prefixes to separate namespaces: - # - 'appraisal:' prefix for appraisal cache (production/development) - # - 'test:appraisal:' prefix for test environment isolation + # Environment isolation via separate Redis databases (configured in secrets.yml): + # - Development: redis://localhost:6379/0 + # - Test: redis://localhost:6379/1 + # - Production: as assigned by provider class Remote def initialize(config) - @redis = Redis.new(url: config.REDISCLOUD_URL) - @key_prefix = ENV['RACK_ENV'] == 'test' ? 'test:' : '' + @redis = Redis.new(url: config.REDIS_URL) end # Store a value with expiration @@ -19,36 +19,30 @@ def initialize(config) # @param value [String] value to store (caller handles serialization) # @param ttl [Integer] time-to-live in seconds def set(key, value, ttl:) - @redis.setex(prefixed(key), ttl, value) + @redis.setex(key, ttl, value) end # Retrieve a cached value # @param key [String] cache key # @return [String, nil] cached value or nil if not found/expired def get(key) - @redis.get(prefixed(key)) + @redis.get(key) end # Check if a key exists # @param key [String] cache key # @return [Boolean] true if key exists def exists?(key) - @redis.exists?(prefixed(key)) + @redis.exists?(key) end def keys - @redis.keys("#{@key_prefix}*") + @redis.keys('*') end def wipe keys.each { |key| @redis.del(key) } end - - private - - def prefixed(key) - "#{@key_prefix}#{key}" - end end end end diff --git a/app/infrastructure/messaging/queue.rb b/app/infrastructure/messaging/queue.rb index 8980d1b..8194dea 100644 --- a/app/infrastructure/messaging/queue.rb +++ b/app/infrastructure/messaging/queue.rb @@ -21,7 +21,7 @@ def initialize(queue_url, config) ## Sends message to queue # Usage: - # q = Messaging::Queue.new(App.config.CLONE_QUEUE_URL) + # q = Messaging::Queue.new(App.config.WORKER_QUEUE_URL) # q.send({data: "hello"}.to_json) def send(message) @queue.send_message(message_body: message) @@ -29,7 +29,7 @@ def send(message) ## Polls queue, yielding each messge # Usage: - # q = Messaging::Queue.new(App.config.CLONE_QUEUE_URL) + # q = Messaging::Queue.new(App.config.WORKER_QUEUE_URL) # q.poll { |msg| print msg.body.to_s } def poll poller = Aws::SQS::QueuePoller.new(@queue_url) diff --git a/config/secrets_example.yml b/config/secrets_example.yml index 3ec7c9e..350d2db 100644 --- a/config/secrets_example.yml +++ b/config/secrets_example.yml @@ -1,55 +1,54 @@ --- # Name your file secrets.yml +# +# Redis database indexes for environment isolation: +# - Development: /0 (default) +# - Test: /1 +# - Production: as assigned by provider (usually /0) development: DB_FILENAME: db/local/dev.db GITHUB_TOKEN: create_github_token REPOSTORE_PATH: repostore API_HOST: http://localhost:9090 - REDISCLOUD_URL: redis://localhost:6379 + REDIS_URL: redis://localhost:6379/0 AWS_ACCESS_KEY_ID: AWS_SECRET_ACCESS_KEY: AWS_REGION: - CLONE_QUEUE: soa-codepraise-clone-dev - CLONE_QUEUE_URL: - REPORT_QUEUE: codepraise-report-development - REPORT_QUEUE_URL: app_test: DB_FILENAME: db/local/test.db REPOSTORE_PATH: repostore API_HOST: http://localhost:9090 - REDISCLOUD_URL: redis://localhost:6379 + REDIS_URL: redis://localhost:6379/1 AWS_ACCESS_KEY_ID: AWS_SECRET_ACCESS_KEY: AWS_REGION: - CLONE_QUEUE: codepraise-clone-test - CLONE_QUEUE_URL: - REPORT_QUEUE: codepraise-report-test - REPORT_QUEUE_URL: test: DB_FILENAME: db/local/test.db GITHUB_TOKEN: create_github_token REPOSTORE_PATH: repostore API_HOST: http://localhost:9090 - REDISCLOUD_URL: redis://localhost:6379 + REDIS_URL: redis://localhost:6379/1 AWS_ACCESS_KEY_ID: AWS_SECRET_ACCESS_KEY: AWS_REGION: - CLONE_QUEUE: codepraise-clone-test - CLONE_QUEUE_URL: - REPORT_QUEUE: codepraise-report-test - REPORT_QUEUE_URL: production: # - assign DATABASE_URL in production GITHUB_TOKEN: create_github_token REPOSTORE_PATH: repostore API_HOST: https://codepraise2022-api - REDISCLOUD_URL: url-assigned-by-Redis-provider-on-Heroku + REDIS_URL: AWS_ACCESS_KEY_ID: AWS_SECRET_ACCESS_KEY: AWS_REGION: - CLONE_QUEUE: codepraise-clone-production - CLONE_QUEUE_URL: + WORKER_QUEUE: codepraise-worker-production + WORKER_QUEUE_URL: diff --git a/coverage/.resultset.json b/coverage/.resultset.json index e179e51..bde9e12 100644 --- a/coverage/.resultset.json +++ b/coverage/.resultset.json @@ -16,7 +16,7 @@ 1, null, 1, - 39, + 38, null, null ] @@ -55,8 +55,6 @@ 1, 1, 1, - 1, - 1, null, 1, null, @@ -78,24 +76,6 @@ null, 1, 0, - null, - null, - null, - null, - null, - 1, - 0, - null, - null, - 0, - null, - null, - null, - null, - null, - null, - 1, - 0, 0, 0, null, @@ -289,7 +269,7 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/cache/local_cache.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/cache/remote_cache.rb": { "lines": [ null, null, @@ -298,46 +278,12 @@ 1, 1, null, - 1, - 1, - 5, - 5, null, null, - 1, - 13, null, null, 1, - 2, - null, - null, 1, - null, - 1, - 5, - null, - null, - null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/cache/redis_cache.rb": { - "lines": [ - null, - null, - 1, - null, - 1, - 1, - null, - null, - null, - null, - null, - 1, - 1, - 48, 48, null, null, @@ -371,12 +317,6 @@ 70, null, null, - 1, - null, - 1, - 22, - null, - null, null, null ] @@ -3061,8 +3001,8 @@ null, 1, 32, - 2397, - 2397, + 2462, + 2462, null, null, 32, @@ -3106,8 +3046,6 @@ null, 1, null, - null, - null, 1, 40, null, @@ -3905,97 +3843,6 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/unit/local_cache_spec.rb": { - "lines": [ - null, - null, - 1, - 1, - null, - 1, - 1, - null, - null, - 5, - 5, - 5, - null, - null, - null, - null, - null, - 1, - null, - null, - 5, - null, - null, - 1, - 1, - null, - 1, - null, - null, - 1, - 1, - null, - null, - 1, - 1, - 1, - 1, - 1, - 1, - null, - 1, - null, - 1, - 1, - 1, - null, - null, - 1, - 1, - null, - 1, - null, - 1, - null, - null, - 1, - 1, - null, - null, - 1, - 1, - 1, - 1, - 1, - 1, - null, - null, - 1, - null, - null, - 1, - null, - null, - 1, - null, - 1, - null, - null, - 1, - 1, - null, - null, - 1, - null, - 1, - null, - null - ] - }, "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/unit/remote_cache_spec.rb": { "lines": [ null, @@ -4411,6 +4258,6 @@ ] } }, - "timestamp": 1766339428 + "timestamp": 1766390257 } } diff --git a/spec/acceptance_tests b/spec/acceptance_tests index d7d98a1..3e8ddf0 100755 --- a/spec/acceptance_tests +++ b/spec/acceptance_tests @@ -2,9 +2,9 @@ # see: https://stackoverflow.com/questions/360201/how-do-i-kill-background-processes-jobs-when-my-shell-script-exits trap "kill 0" EXIT -# Ensure Redis is running -echo "Checking Redis..." -bundle exec rake redis:ensure +# Ensure cache (Redis) is running +echo "Checking cache..." +bundle exec rake cache:ensure # Create logs directory if needed (gitignored via spec/logs/) mkdir -p spec/logs diff --git a/spec/helpers/cache_helper.rb b/spec/helpers/cache_helper.rb index f253de8..311e25e 100644 --- a/spec/helpers/cache_helper.rb +++ b/spec/helpers/cache_helper.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true # Helper for Redis cache testing -# Uses real Redis with 'test:' key prefix for isolation from production data +# Environment isolation via separate Redis databases: +# - Test uses redis://localhost:6379/1 (configured in secrets.yml) +# - Development uses redis://localhost:6379/0 module CacheHelper # Create a cache instance for testing - # Uses App.config which points to real Redis, but Cache::Remote - # automatically uses 'test:' key prefix when RACK_ENV=test def self.create_test_cache CodePraise::Cache::Remote.new(CodePraise::App.config) end diff --git a/spec/tests/unit/remote_cache_spec.rb b/spec/tests/unit/remote_cache_spec.rb index 23b0fc6..c5f8121 100644 --- a/spec/tests/unit/remote_cache_spec.rb +++ b/spec/tests/unit/remote_cache_spec.rb @@ -47,14 +47,14 @@ end describe 'keys' do - it 'should list all keys with test prefix' do + it 'should list all keys in the database' do @cache.set('key1', 'value1', ttl: 3600) @cache.set('key2', 'value2', ttl: 3600) keys = @cache.keys - # In test environment, keys are prefixed with 'test:' - _(keys).must_include 'test:key1' - _(keys).must_include 'test:key2' + # Environment isolation via separate Redis databases (no key prefixes) + _(keys).must_include 'key1' + _(keys).must_include 'key2' _(keys.length).must_equal 2 end end diff --git a/workers/application/controllers/worker.rb b/workers/application/controllers/worker.rb index 48ef94d..aa16254 100644 --- a/workers/application/controllers/worker.rb +++ b/workers/application/controllers/worker.rb @@ -29,7 +29,7 @@ def self.config = Figaro.env include Shoryuken::Worker Shoryuken.sqs_client_receive_message_opts = { wait_time_seconds: 20 } - shoryuken_options queue: config.CLONE_QUEUE_URL, auto_delete: true + shoryuken_options queue: config.WORKER_QUEUE_URL, auto_delete: true def perform(_sqs_msg, request) job = JobReporter.new(request, Worker.config) diff --git a/workers/shoryuken.yml b/workers/shoryuken.yml index 2a460f0..ecc8704 100644 --- a/workers/shoryuken.yml +++ b/workers/shoryuken.yml @@ -1,2 +1,2 @@ queues: - - https://sqs.us-east-1.amazonaws.com/503315808870/soa-codepraise-clone-production + - https://sqs.us-east-1.amazonaws.com/503315808870/soa-codepraise-worker-production diff --git a/workers/shoryuken_dev.yml b/workers/shoryuken_dev.yml index 35848dd..e994f08 100644 --- a/workers/shoryuken_dev.yml +++ b/workers/shoryuken_dev.yml @@ -1,2 +1,2 @@ queues: - - https://sqs.ap-northeast-1.amazonaws.com/503315808870/soa-codepraise-clone-dev + - https://sqs.ap-northeast-1.amazonaws.com/503315808870/soa-codepraise-worker-dev diff --git a/workers/shoryuken_test.yml b/workers/shoryuken_test.yml index be9aa3c..55dff4c 100644 --- a/workers/shoryuken_test.yml +++ b/workers/shoryuken_test.yml @@ -1,2 +1,2 @@ queues: - - https://sqs.ap-northeast-1.amazonaws.com/503315808870/soa-codepraise-clone-test + - https://sqs.ap-northeast-1.amazonaws.com/503315808870/soa-codepraise-worker-test From 0552c3dfb223899c6f9a4e02d648b7bce65a2176 Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Tue, 23 Dec 2025 13:30:50 +0800 Subject: [PATCH 16/18] chore: rename worker service namespace and remove legacy code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Worker service: - Rename Worker::AppraiseProject → Appraiser::Service::AppraiseProject - Move service into Appraiser::Service namespace for consistency - Remove legacy perform_clone_only method (no longer needed) - Remove appraisal_request? check (all requests are appraisal requests) Controller simplification: - Remove redundant comments and conditional branches - Simplify cache hit response handling Code style: - Use Ruby 3.4 'it' parameter instead of '_1' in method chains --- app/application/controllers/app.rb | 14 +- .../services/fetch_or_request_appraisal.rb | 3 +- spec/tests/unit/worker_appraise_spec.rb | 16 +- workers/application/controllers/worker.rb | 26 +- .../application/services/appraise_project.rb | 268 +++++++++--------- 5 files changed, 150 insertions(+), 177 deletions(-) diff --git a/app/application/controllers/app.rb b/app/application/controllers/app.rb index 0d3a9ad..729269c 100644 --- a/app/application/controllers/app.rb +++ b/app/application/controllers/app.rb @@ -33,7 +33,6 @@ class App < Roda # GET /projects/{owner_name}/{project_name}[/folder_namepath/] routing.get do # Appraisal results cached in Redis by worker (1-day TTL) - # No HTTP caching needed - Redis is the source of truth request_id = [request.env, request.path, Time.now.to_f].hash path_request = Request::ProjectPath.new( @@ -47,21 +46,16 @@ class App < Roda ) if result.failure? + # Failure includes appraisal request being processed by worker failed = Representer::HttpResponse.new(result.failure) routing.halt failed.http_status_code, failed.to_json end # Cache hit - return pre-serialized JSON directly appraisal_result = result.value! - if appraisal_result[:cache_hit] - response.status = 200 - appraisal_result[:cached_json] - else - # Should not reach here - success means cache hit - # Worker requests return Failure with :processing status - response.status = 200 - appraisal_result[:cached_json] - end + response.status = appraisal_result[:cache_hit] ? 200 : 500 + + appraisal_result[:cached_json] end # POST /projects/{owner_name}/{project_name} diff --git a/app/application/services/fetch_or_request_appraisal.rb b/app/application/services/fetch_or_request_appraisal.rb index 5f31763..f9e6132 100644 --- a/app/application/services/fetch_or_request_appraisal.rb +++ b/app/application/services/fetch_or_request_appraisal.rb @@ -92,8 +92,7 @@ def appraisal_request_json(input) input[:project], input[:requested].folder_name || '', input[:request_id] - ).then { Representer::AppraisalRequest.new(_1) } - .then(&:to_json) + ).then { Representer::AppraisalRequest.new(it).to_json } end def log_error(error) diff --git a/spec/tests/unit/worker_appraise_spec.rb b/spec/tests/unit/worker_appraise_spec.rb index 7bba4f3..6ec557a 100644 --- a/spec/tests/unit/worker_appraise_spec.rb +++ b/spec/tests/unit/worker_appraise_spec.rb @@ -4,7 +4,7 @@ require_relative '../../helpers/cache_helper' require_relative '../../../workers/application/services/appraise_project' -describe 'Unit test of Worker::AppraiseProject' do +describe 'Unit test of Appraiser::Service::AppraiseProject' do before do @cache = CacheHelper.create_test_cache CacheHelper.wipe_cache(@cache) @@ -35,7 +35,7 @@ describe 'build_project_entity helper' do it 'should convert OpenStruct to Entity::Project' do - service = Worker::AppraiseProject.new + service = Appraiser::Service::AppraiseProject.new # Access private method for testing entity = service.send(:build_project_entity, @project_ostruct) @@ -48,7 +48,7 @@ end it 'should handle empty contributors' do - service = Worker::AppraiseProject.new + service = Appraiser::Service::AppraiseProject.new entity = service.send(:build_project_entity, @project_ostruct) _(entity.contributors).must_equal [] @@ -59,7 +59,7 @@ OpenStruct.new(origin_id: 789, username: 'contributor1', email: 'c1@example.com') ] - service = Worker::AppraiseProject.new + service = Appraiser::Service::AppraiseProject.new entity = service.send(:build_project_entity, @project_ostruct) _(entity.contributors.length).must_equal 1 @@ -69,22 +69,22 @@ describe 'scale_clone_progress helper' do it 'should scale Cloning to 25' do - service = Worker::AppraiseProject.new + service = Appraiser::Service::AppraiseProject.new _(service.send(:scale_clone_progress, 'Cloning into...')).must_equal 25 end it 'should scale Receiving to 40' do - service = Worker::AppraiseProject.new + service = Appraiser::Service::AppraiseProject.new _(service.send(:scale_clone_progress, 'Receiving objects: 50%')).must_equal 40 end it 'should scale Checking to 50' do - service = Worker::AppraiseProject.new + service = Appraiser::Service::AppraiseProject.new _(service.send(:scale_clone_progress, 'Checking connectivity...')).must_equal 50 end it 'should default unknown stages to 30' do - service = Worker::AppraiseProject.new + service = Appraiser::Service::AppraiseProject.new _(service.send(:scale_clone_progress, 'Unknown stage')).must_equal 30 end end diff --git a/workers/application/controllers/worker.rb b/workers/application/controllers/worker.rb index aa16254..537ef23 100644 --- a/workers/application/controllers/worker.rb +++ b/workers/application/controllers/worker.rb @@ -33,12 +33,7 @@ def self.config = Figaro.env def perform(_sqs_msg, request) job = JobReporter.new(request, Worker.config) - - if appraisal_request?(request) - perform_appraisal(job) - else - perform_clone_only(job) - end + perform_appraisal(job) rescue CodePraise::GitRepo::Errors::CannotOverwriteLocalGitRepo # worker should crash fail early - only catch errors we expect! puts 'CLONE EXISTS -- ignoring request' @@ -46,16 +41,10 @@ def perform(_sqs_msg, request) private - # Check if this is the new AppraisalRequest format - def appraisal_request?(request) - JSON.parse(request).key?('folder_path') - end - - # New flow: clone + appraise + cache def perform_appraisal(job) gitrepo = CodePraise::GitRepo.new(job.project, Worker.config) - ::Worker::AppraiseProject.new.call( + Service::AppraiseProject.new.call( project: job.project, folder_path: job.folder_path, config: Worker.config, @@ -66,16 +55,5 @@ def perform_appraisal(job) # Keep sending finished status to any latecoming subscribers job.report_each_second(5) { AppraisalMonitor.finished_percent } end - - # Legacy flow: clone only (for backwards compatibility) - def perform_clone_only(job) - job.report(CloneMonitor.starting_percent) - CodePraise::GitRepo.new(job.project, Worker.config).clone_locally do |line| - job.report CloneMonitor.progress(line) - end - - # Keep sending finished status to any latecoming subscribers - job.report_each_second(5) { CloneMonitor.finished_percent } - end end end diff --git a/workers/application/services/appraise_project.rb b/workers/application/services/appraise_project.rb index be4de0f..1db3382 100644 --- a/workers/application/services/appraise_project.rb +++ b/workers/application/services/appraise_project.rb @@ -2,152 +2,154 @@ require 'dry/transaction' -module Worker - # Service to clone repo, appraise contributions, and cache result - # Uses Dry::Transaction for composable steps with railway-oriented error handling - class AppraiseProject - include Dry::Transaction - - step :prepare_inputs - step :clone_repo - step :appraise_contributions - step :cache_result - - private - - # Input: { project:, folder_path:, config:, gitrepo:, progress: } - # project: OpenStruct from deserializing AppraisalRequest - # folder_path: String path to folder being appraised - # config: Worker config with Redis URL - # gitrepo: GitRepo instance - # progress: Proc to report progress - - def prepare_inputs(input) - # Convert OpenStruct project to Entity::Project for Value::Appraisal - input[:project_entity] = build_project_entity(input[:project]) - Success(input) - rescue StandardError => e - puts "PREPARE ERROR: #{e.message}" - Failure(input.merge(error: { type: 'prepare_failed', message: e.message })) - end - - def clone_repo(input) - input[:progress].call(15) # STARTED +module Appraiser + module Service + # Service to clone repo, appraise contributions, and cache result + # Uses Dry::Transaction for composable steps with railway-oriented error handling + class AppraiseProject + include Dry::Transaction + + step :prepare_inputs + step :clone_repo + step :appraise_contributions + step :cache_result + + # Input: { project:, folder_path:, config:, gitrepo:, progress: } + # project: OpenStruct from deserializing AppraisalRequest + # folder_path: String path to folder being appraised + # config: Worker config with Redis URL + # gitrepo: GitRepo instance + # progress: Proc to report progress + + def prepare_inputs(input) + # Convert OpenStruct project to Entity::Project for Value::Appraisal + input[:project_entity] = build_project_entity(input[:project]) + Success(input) + rescue StandardError => e + puts "PREPARE ERROR: #{e.message}" + Failure(input.merge(error: { type: 'prepare_failed', message: e.message })) + end - if input[:gitrepo].exists_locally? - input[:progress].call(50) # Skip to post-clone - else - input[:gitrepo].clone_locally do |line| - # Scale clone progress from 15 to 50 - percent = scale_clone_progress(line) - input[:progress].call(percent) + def clone_repo(input) + input[:progress].call(15) # STARTED + + if input[:gitrepo].exists_locally? + input[:progress].call(50) # Skip to post-clone + else + input[:gitrepo].clone_locally do |line| + # Scale clone progress from 15 to 50 + percent = scale_clone_progress(line) + input[:progress].call(percent) + end end + + Success(input) + rescue StandardError => e + # Create error appraisal and cache it + input[:appraisal] = CodePraise::Value::Appraisal.error( + project: input[:project_entity], + folder_path: input[:folder_path], + error_type: 'clone_failed', + error_message: e.message + ) + # Continue to cache the error + Success(input) end - Success(input) - rescue StandardError => e - # Create error appraisal and cache it - input[:appraisal] = CodePraise::Value::Appraisal.error( - project: input[:project_entity], - folder_path: input[:folder_path], - error_type: 'clone_failed', - error_message: e.message - ) - # Continue to cache the error - Success(input) - end + def appraise_contributions(input) + # Skip if already errored in clone step + return Success(input) if input[:appraisal]&.error? + + input[:progress].call(55) # Starting appraisal + + folder = CodePraise::Mapper::Contributions + .new(input[:gitrepo]) + .for_folder(input[:folder_path]) + + input[:progress].call(85) # Appraisal complete + + # Build successful appraisal value object + input[:appraisal] = CodePraise::Value::Appraisal.success( + project: input[:project_entity], + folder_path: input[:folder_path], + folder: folder + ) + + Success(input) + rescue StandardError => e + # Create error appraisal + input[:appraisal] = CodePraise::Value::Appraisal.error( + project: input[:project_entity], + folder_path: input[:folder_path], + error_type: 'appraisal_failed', + error_message: e.message + ) + + # Still cache the error - return Success to continue to cache_result + Success(input) + end - def appraise_contributions(input) - # Skip if already errored in clone step - return Success(input) if input[:appraisal]&.error? - - input[:progress].call(55) # Starting appraisal - - folder = CodePraise::Mapper::Contributions - .new(input[:gitrepo]) - .for_folder(input[:folder_path]) - - input[:progress].call(85) # Appraisal complete - - # Build successful appraisal value object - input[:appraisal] = CodePraise::Value::Appraisal.success( - project: input[:project_entity], - folder_path: input[:folder_path], - folder: folder - ) - - Success(input) - rescue StandardError => e - # Create error appraisal - input[:appraisal] = CodePraise::Value::Appraisal.error( - project: input[:project_entity], - folder_path: input[:folder_path], - error_type: 'appraisal_failed', - error_message: e.message - ) - - # Still cache the error - return Success to continue to cache_result - Success(input) - end + def cache_result(input) + input[:progress].call(90) # Caching - def cache_result(input) - input[:progress].call(90) # Caching + appraisal = input[:appraisal] + json = CodePraise::Representer::Appraisal.new(appraisal).to_json - appraisal = input[:appraisal] - json = CodePraise::Representer::Appraisal.new(appraisal).to_json + cache = CodePraise::Cache::Remote.new(input[:config]) + cache.set(appraisal.cache_key, json, ttl: appraisal.ttl) - cache = CodePraise::Cache::Remote.new(input[:config]) - cache.set(appraisal.cache_key, json, ttl: appraisal.ttl) + input[:progress].call(100) # FINISHED - input[:progress].call(100) # FINISHED + Success(input) + rescue StandardError => e + # Cache failure - still report 100% so client can retry + puts "CACHE ERROR: #{e.message}" + input[:progress].call(100) + Success(input) + end - Success(input) - rescue StandardError => e - # Cache failure - still report 100% so client can retry - puts "CACHE ERROR: #{e.message}" - input[:progress].call(100) - Success(input) - end + private - # Scale git clone progress (15-50 range) - def scale_clone_progress(line) - clone_stages = { - 'Cloning' => 25, - 'remote' => 35, - 'Receiving' => 40, - 'Resolving' => 45, - 'Checking' => 50 - } - - first_word = line.match(/^[A-Za-z]+/).to_s - clone_stages[first_word] || 30 - end + # Scale git clone progress (15-50 range) + def scale_clone_progress(line) + clone_stages = { + 'Cloning' => 25, + 'remote' => 35, + 'Receiving' => 40, + 'Resolving' => 45, + 'Checking' => 50 + } - # Convert OpenStruct project (from JSON) to Entity::Project - def build_project_entity(project_ostruct) - owner = build_member_entity(project_ostruct.owner) - contributors = (project_ostruct.contributors || []).map { |c| build_member_entity(c) } - - CodePraise::Entity::Project.new( - id: nil, - origin_id: project_ostruct.origin_id, - name: project_ostruct.name, - size: project_ostruct.size, - ssh_url: project_ostruct.ssh_url, - http_url: project_ostruct.http_url, - owner: owner, - contributors: contributors - ) - end + first_word = line.match(/^[A-Za-z]+/).to_s + clone_stages[first_word] || 30 + end - # Convert OpenStruct member to Entity::Member - def build_member_entity(member_ostruct) - CodePraise::Entity::Member.new( - id: nil, - origin_id: member_ostruct.origin_id, - username: member_ostruct.username, - email: member_ostruct.email || '' - ) + # Convert OpenStruct project (from JSON) to Entity::Project + def build_project_entity(project_ostruct) + owner = build_member_entity(project_ostruct.owner) + contributors = (project_ostruct.contributors || []).map { |c| build_member_entity(c) } + + CodePraise::Entity::Project.new( + id: nil, + origin_id: project_ostruct.origin_id, + name: project_ostruct.name, + size: project_ostruct.size, + ssh_url: project_ostruct.ssh_url, + http_url: project_ostruct.http_url, + owner: owner, + contributors: contributors + ) + end + + # Convert OpenStruct member to Entity::Member + def build_member_entity(member_ostruct) + CodePraise::Entity::Member.new( + id: nil, + origin_id: member_ostruct.origin_id, + username: member_ostruct.username, + email: member_ostruct.email || '' + ) + end end end end From 5993c73daf58fa713c22acaaeb6b63e575777e75 Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Tue, 23 Dec 2025 15:04:56 +0800 Subject: [PATCH 17/18] chore: use REDIS_URL secret in GitHub Actions workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep environment variable configuration consistent across local, CI, and production environments. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .github/workflows/api.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index cbbeac0..859ee60 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -74,7 +74,7 @@ jobs: DB_FILENAME: ${{ secrets.DB_FILENAME }} REPOSTORE_PATH: ${{ secrets.REPOSTORE_PATH }} API_HOST: ${{ secrets.API_HOST }} - REDIS_URL: redis://localhost:6379/1 + REDIS_URL: ${{ secrets.REDIS_URL }} WORKER_QUEUE_URL: ${{ secrets.WORKER_QUEUE_URL }} AWS_REGION: ${{ secrets.AWS_REGION }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} From 0a32fb0a99320fc62262f9ffccd41116fa34c971 Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Tue, 23 Dec 2025 15:18:38 +0800 Subject: [PATCH 18/18] ci: simplify workflow to Linux-only with latest actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove macOS matrix (Linux matches Heroku production environment) - Update actions/checkout from v4 to v6 - Keep ruby/setup-ruby@v1 (recommended semantic versioning) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .github/workflows/api.yml | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index 859ee60..27c2c6e 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -16,20 +16,12 @@ on: # A workflow is defined of one or more jobs that can run sequentially or in parallel jobs: - # This workflow contains a single job called "api_test" api_test: - strategy: - # don't cancel other jobs if one fails - fail-fast: false - # maximum number of jobs that can run simultaneously - max-parallel: 1 - matrix: - os: [ubuntu, macos] - runs-on: ${{ matrix.os }}-latest # Runs on latest builds of matrix OSes + runs-on: ubuntu-latest env: BUNDLE_WITHOUT: production # skip installing production gem (pg) - # Redis service container for caching (Linux only - macOS uses brew) + # Redis service container for caching services: redis: image: redis @@ -40,25 +32,10 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 - # Service containers only work on Linux runners - # macOS will install Redis via Homebrew in the steps below - # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - # Install Redis on macOS (service containers only work on Linux) - - name: Install Redis (macOS) - if: runner.os == 'macOS' - run: | - brew install redis - brew services start redis - # Wait for Redis to be ready - sleep 2 - redis-cli ping - - # Builds on a predefined action that has Ruby installed - uses: ruby/setup-ruby@v1 with: bundler-cache: true # runs 'bundle install' and caches installed gems automatically