Skip to content

Commit f148c86

Browse files
committed
improve web behaviors
1 parent b011083 commit f148c86

3 files changed

Lines changed: 334 additions & 119 deletions

File tree

cmd/prcost/main.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -460,24 +460,24 @@ func efficiencyGrade(efficiencyPct float64) (string, string) {
460460
}
461461

462462
// mergeVelocityGrade returns a grade based on average PR open time in days.
463-
// A: 1 day or less, B: 2.5 days or less, C: 2.5-4 days, D: 4-5 days, F: 5+ days.
463+
// A+: 4h, A: 8h, A-: 12h, B+: 18h, B: 24h, B-: 36h, C: 100h, D: 120h, F: 120h+
464464
func mergeVelocityGrade(avgOpenDays float64) (string, string) {
465465
switch {
466-
case avgOpenDays <= 0.1875: // 4.5 hours
466+
case avgOpenDays <= 0.1667: // 4 hours
467467
return "A+", "Impeccable"
468-
case avgOpenDays <= 1.0:
468+
case avgOpenDays <= 0.3333: // 8 hours
469469
return "A", "Excellent"
470-
case avgOpenDays <= 1.5:
470+
case avgOpenDays <= 0.5: // 12 hours
471471
return "A-", "Nearly excellent"
472-
case avgOpenDays <= 2.0:
472+
case avgOpenDays <= 0.75: // 18 hours
473473
return "B+", "Acceptable+"
474-
case avgOpenDays <= 2.5:
474+
case avgOpenDays <= 1.0: // 24 hours
475475
return "B", "Acceptable"
476-
case avgOpenDays <= 3.0:
476+
case avgOpenDays <= 1.5: // 36 hours
477477
return "B-", "Nearly acceptable"
478-
case avgOpenDays <= 4.0:
478+
case avgOpenDays <= 4.1667: // 100 hours
479479
return "C", "Average"
480-
case avgOpenDays <= 5.0:
480+
case avgOpenDays <= 5.0: // 120 hours
481481
return "D", "Not good my friend."
482482
default:
483483
return "F", "Failing"

internal/server/server.go

Lines changed: 120 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"os"
1616
"os/exec"
1717
"regexp"
18+
"strconv"
1819
"strings"
1920
"sync"
2021
"time"
@@ -440,7 +441,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
440441
w.Header().Set("Access-Control-Allow-Origin", origin)
441442
w.Header().Set("Vary", "Origin")
442443
}
443-
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
444+
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
444445
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
445446

446447
// Handle preflight OPTIONS request.
@@ -452,19 +453,19 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
452453
// Route requests.
453454
switch {
454455
case r.URL.Path == "/v1/calculate":
455-
if r.Method != http.MethodPost {
456+
if r.Method != http.MethodPost && r.Method != http.MethodGet {
456457
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
457458
return
458459
}
459460
s.handleCalculate(w, r)
460461
case r.URL.Path == "/v1/calculate/repo":
461-
if r.Method != http.MethodPost {
462+
if r.Method != http.MethodPost && r.Method != http.MethodGet {
462463
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
463464
return
464465
}
465466
s.handleRepoSample(w, r)
466467
case r.URL.Path == "/v1/calculate/org":
467-
if r.Method != http.MethodPost {
468+
if r.Method != http.MethodPost && r.Method != http.MethodGet {
468469
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
469470
return
470471
}
@@ -576,14 +577,39 @@ func (s *Server) handleCalculate(writer http.ResponseWriter, request *http.Reque
576577

577578
// parseRequest parses and validates the incoming request.
578579
func (s *Server) parseRequest(ctx context.Context, r *http.Request) (*CalculateRequest, error) {
579-
// SECURITY: Limit request body size to prevent memory exhaustion DoS.
580-
const maxRequestSize = 1 << 20 // 1MB
581-
r.Body = http.MaxBytesReader(nil, r.Body, maxRequestSize)
582-
583580
var req CalculateRequest
584-
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
585-
s.logger.ErrorContext(ctx, "[parseRequest] Failed to decode JSON", errorKey, sanitizeError(err))
586-
return nil, fmt.Errorf("invalid JSON: %w", err)
581+
582+
// Handle GET requests with query parameters
583+
if r.Method == http.MethodGet {
584+
req.URL = r.URL.Query().Get("url")
585+
586+
// Parse optional config parameters
587+
if salaryStr := r.URL.Query().Get("salary"); salaryStr != "" {
588+
if req.Config == nil {
589+
req.Config = &cost.Config{}
590+
}
591+
if salary, err := strconv.ParseFloat(salaryStr, 64); err == nil {
592+
req.Config.AnnualSalary = salary
593+
}
594+
}
595+
if benefitsStr := r.URL.Query().Get("benefits"); benefitsStr != "" {
596+
if req.Config == nil {
597+
req.Config = &cost.Config{}
598+
}
599+
if benefits, err := strconv.ParseFloat(benefitsStr, 64); err == nil {
600+
req.Config.BenefitsMultiplier = benefits
601+
}
602+
}
603+
} else {
604+
// Handle POST requests with JSON body
605+
// SECURITY: Limit request body size to prevent memory exhaustion DoS.
606+
const maxRequestSize = 1 << 20 // 1MB
607+
r.Body = http.MaxBytesReader(nil, r.Body, maxRequestSize)
608+
609+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
610+
s.logger.ErrorContext(ctx, "[parseRequest] Failed to decode JSON", errorKey, sanitizeError(err))
611+
return nil, fmt.Errorf("invalid JSON: %w", err)
612+
}
587613
}
588614

589615
if req.URL == "" {
@@ -1098,13 +1124,49 @@ func (s *Server) handleOrgSample(writer http.ResponseWriter, request *http.Reque
10981124

10991125
// parseRepoSampleRequest parses and validates repository sampling requests.
11001126
func (s *Server) parseRepoSampleRequest(ctx context.Context, r *http.Request) (*RepoSampleRequest, error) {
1101-
const maxRequestSize = 1 << 20 // 1MB
1102-
r.Body = http.MaxBytesReader(nil, r.Body, maxRequestSize)
1103-
11041127
var req RepoSampleRequest
1105-
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1106-
s.logger.ErrorContext(ctx, "[parseRepoSampleRequest] Failed to decode JSON", errorKey, sanitizeError(err))
1107-
return nil, fmt.Errorf("invalid JSON: %w", err)
1128+
1129+
// Handle GET requests with query parameters
1130+
if r.Method == http.MethodGet {
1131+
req.Owner = r.URL.Query().Get("owner")
1132+
req.Repo = r.URL.Query().Get("repo")
1133+
1134+
// Parse optional parameters
1135+
if sampleStr := r.URL.Query().Get("sample"); sampleStr != "" {
1136+
if sample, err := strconv.Atoi(sampleStr); err == nil {
1137+
req.SampleSize = sample
1138+
}
1139+
}
1140+
if daysStr := r.URL.Query().Get("days"); daysStr != "" {
1141+
if days, err := strconv.Atoi(daysStr); err == nil {
1142+
req.Days = days
1143+
}
1144+
}
1145+
if salaryStr := r.URL.Query().Get("salary"); salaryStr != "" {
1146+
if req.Config == nil {
1147+
req.Config = &cost.Config{}
1148+
}
1149+
if salary, err := strconv.ParseFloat(salaryStr, 64); err == nil {
1150+
req.Config.AnnualSalary = salary
1151+
}
1152+
}
1153+
if benefitsStr := r.URL.Query().Get("benefits"); benefitsStr != "" {
1154+
if req.Config == nil {
1155+
req.Config = &cost.Config{}
1156+
}
1157+
if benefits, err := strconv.ParseFloat(benefitsStr, 64); err == nil {
1158+
req.Config.BenefitsMultiplier = benefits
1159+
}
1160+
}
1161+
} else {
1162+
// Handle POST requests with JSON body
1163+
const maxRequestSize = 1 << 20 // 1MB
1164+
r.Body = http.MaxBytesReader(nil, r.Body, maxRequestSize)
1165+
1166+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1167+
s.logger.ErrorContext(ctx, "[parseRepoSampleRequest] Failed to decode JSON", errorKey, sanitizeError(err))
1168+
return nil, fmt.Errorf("invalid JSON: %w", err)
1169+
}
11081170
}
11091171

11101172
if req.Owner == "" {
@@ -1138,13 +1200,48 @@ func (s *Server) parseRepoSampleRequest(ctx context.Context, r *http.Request) (*
11381200

11391201
// parseOrgSampleRequest parses and validates organization sampling requests.
11401202
func (s *Server) parseOrgSampleRequest(ctx context.Context, r *http.Request) (*OrgSampleRequest, error) {
1141-
const maxRequestSize = 1 << 20 // 1MB
1142-
r.Body = http.MaxBytesReader(nil, r.Body, maxRequestSize)
1143-
11441203
var req OrgSampleRequest
1145-
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1146-
s.logger.ErrorContext(ctx, "[parseOrgSampleRequest] Failed to decode JSON", errorKey, sanitizeError(err))
1147-
return nil, fmt.Errorf("invalid JSON: %w", err)
1204+
1205+
// Handle GET requests with query parameters
1206+
if r.Method == http.MethodGet {
1207+
req.Org = r.URL.Query().Get("org")
1208+
1209+
// Parse optional parameters
1210+
if sampleStr := r.URL.Query().Get("sample"); sampleStr != "" {
1211+
if sample, err := strconv.Atoi(sampleStr); err == nil {
1212+
req.SampleSize = sample
1213+
}
1214+
}
1215+
if daysStr := r.URL.Query().Get("days"); daysStr != "" {
1216+
if days, err := strconv.Atoi(daysStr); err == nil {
1217+
req.Days = days
1218+
}
1219+
}
1220+
if salaryStr := r.URL.Query().Get("salary"); salaryStr != "" {
1221+
if req.Config == nil {
1222+
req.Config = &cost.Config{}
1223+
}
1224+
if salary, err := strconv.ParseFloat(salaryStr, 64); err == nil {
1225+
req.Config.AnnualSalary = salary
1226+
}
1227+
}
1228+
if benefitsStr := r.URL.Query().Get("benefits"); benefitsStr != "" {
1229+
if req.Config == nil {
1230+
req.Config = &cost.Config{}
1231+
}
1232+
if benefits, err := strconv.ParseFloat(benefitsStr, 64); err == nil {
1233+
req.Config.BenefitsMultiplier = benefits
1234+
}
1235+
}
1236+
} else {
1237+
// Handle POST requests with JSON body
1238+
const maxRequestSize = 1 << 20 // 1MB
1239+
r.Body = http.MaxBytesReader(nil, r.Body, maxRequestSize)
1240+
1241+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1242+
s.logger.ErrorContext(ctx, "[parseOrgSampleRequest] Failed to decode JSON", errorKey, sanitizeError(err))
1243+
return nil, fmt.Errorf("invalid JSON: %w", err)
1244+
}
11481245
}
11491246

11501247
if req.Org == "" {

0 commit comments

Comments
 (0)