Coffee journaling on ATProto (alpha) alpha.arabica.social
coffee
17
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: modernization fixes with `go fix`

+221 -230
+2 -2
cmd/server/logging_test.go
··· 62 62 } 63 63 64 64 // Parse as JSON to verify structure 65 - var logEntry map[string]interface{} 65 + var logEntry map[string]any 66 66 if err := json.Unmarshal([]byte(strings.TrimSpace(logOutput)), &logEntry); err != nil { 67 67 t.Fatalf("Failed to parse log as JSON: %v\nOutput: %s", err, logOutput) 68 68 } ··· 77 77 } 78 78 79 79 // Verify DIDs array is present 80 - didsFromLog, ok := logEntry["dids"].([]interface{}) 80 + didsFromLog, ok := logEntry["dids"].([]any) 81 81 if !ok { 82 82 t.Fatalf("Expected 'dids' to be an array, got %T", logEntry["dids"]) 83 83 }
+2 -3
internal/atproto/cache.go
··· 1 1 package atproto 2 2 3 3 import ( 4 + "maps" 4 5 "sync" 5 6 "time" 6 7 ··· 53 54 var dirty map[string]bool 54 55 if c.DirtyCollections != nil { 55 56 dirty = make(map[string]bool, len(c.DirtyCollections)) 56 - for k, v := range c.DirtyCollections { 57 - dirty[k] = v 58 - } 57 + maps.Copy(dirty, c.DirtyCollections) 59 58 } 60 59 return &UserCache{ 61 60 Beans: c.Beans,
+12 -12
internal/atproto/cache_test.go
··· 428 428 wg.Add(numGoroutines * 2) 429 429 430 430 // Writers 431 - for i := 0; i < numGoroutines; i++ { 431 + for i := range numGoroutines { 432 432 go func(id int) { 433 433 defer wg.Done() 434 - for j := 0; j < numOperations; j++ { 434 + for range numOperations { 435 435 sessionID := "session" 436 436 userCache := &UserCache{ 437 437 Beans: []*models.Bean{{RKey: "bean"}}, ··· 443 443 } 444 444 445 445 // Readers 446 - for i := 0; i < numGoroutines; i++ { 446 + for i := range numGoroutines { 447 447 go func(id int) { 448 448 defer wg.Done() 449 - for j := 0; j < numOperations; j++ { 449 + for range numOperations { 450 450 cache.Get("session") 451 451 } 452 452 }(i) ··· 470 470 471 471 go func() { 472 472 defer wg.Done() 473 - for i := 0; i < numOperations; i++ { 473 + for range numOperations { 474 474 cache.SetBeans(sessionID, []*models.Bean{{RKey: "bean"}}) 475 475 } 476 476 }() 477 477 478 478 go func() { 479 479 defer wg.Done() 480 - for i := 0; i < numOperations; i++ { 480 + for range numOperations { 481 481 cache.SetRoasters(sessionID, []*models.Roaster{{RKey: "roaster"}}) 482 482 } 483 483 }() 484 484 485 485 go func() { 486 486 defer wg.Done() 487 - for i := 0; i < numOperations; i++ { 487 + for range numOperations { 488 488 cache.InvalidateBeans(sessionID) 489 489 } 490 490 }() 491 491 492 492 go func() { 493 493 defer wg.Done() 494 - for i := 0; i < numOperations; i++ { 494 + for range numOperations { 495 495 cache.InvalidateRoasters(sessionID) 496 496 } 497 497 }() 498 498 499 499 go func() { 500 500 defer wg.Done() 501 - for i := 0; i < numOperations; i++ { 501 + for range numOperations { 502 502 cache.Get(sessionID) 503 503 } 504 504 }() ··· 514 514 // Writer 515 515 go func() { 516 516 defer wg.Done() 517 - for i := 0; i < numOperations; i++ { 517 + for range numOperations { 518 518 cache.Set("session", &UserCache{ 519 519 Beans: []*models.Bean{{RKey: "bean"}}, 520 520 Timestamp: time.Now(), ··· 525 525 // Reader 526 526 go func() { 527 527 defer wg.Done() 528 - for i := 0; i < numOperations; i++ { 528 + for range numOperations { 529 529 cache.Get("session") 530 530 } 531 531 }() ··· 533 533 // Cleanup 534 534 go func() { 535 535 defer wg.Done() 536 - for i := 0; i < numOperations; i++ { 536 + for range numOperations { 537 537 cache.Cleanup() 538 538 } 539 539 }()
+13 -13
internal/atproto/client.go
··· 82 82 // CreateRecordInput contains parameters for creating a record 83 83 type CreateRecordInput struct { 84 84 Collection string 85 - Record interface{} 85 + Record any 86 86 RKey *string // Optional, if nil a TID will be generated 87 87 } 88 88 ··· 104 104 } 105 105 106 106 // Build the request body 107 - body := map[string]interface{}{ 107 + body := map[string]any{ 108 108 "repo": did.String(), 109 109 "collection": input.Collection, 110 110 "record": input.Record, ··· 159 159 type GetRecordOutput struct { 160 160 URI string 161 161 CID string 162 - Value map[string]interface{} 162 + Value map[string]any 163 163 } 164 164 165 165 // GetRecord retrieves a single record by its rkey ··· 182 182 183 183 // Use the API client's Get method to call com.atproto.repo.getRecord 184 184 var result struct { 185 - URI string `json:"uri"` 186 - CID string `json:"cid"` 187 - Value map[string]interface{} `json:"value"` 185 + URI string `json:"uri"` 186 + CID string `json:"cid"` 187 + Value map[string]any `json:"value"` 188 188 } 189 189 190 190 err = apiClient.Get(ctx, "com.atproto.repo.getRecord", params, &result) ··· 236 236 type Record struct { 237 237 URI string 238 238 CID string 239 - Value map[string]interface{} 239 + Value map[string]any 240 240 } 241 241 242 242 // ListRecords retrieves a list of records from a collection ··· 266 266 // Use the API client's Get method to call com.atproto.repo.listRecords 267 267 var result struct { 268 268 Records []struct { 269 - URI string `json:"uri"` 270 - CID string `json:"cid"` 271 - Value map[string]interface{} `json:"value"` 269 + URI string `json:"uri"` 270 + CID string `json:"cid"` 271 + Value map[string]any `json:"value"` 272 272 } `json:"records"` 273 273 Cursor *string `json:"cursor,omitempty"` 274 274 } ··· 382 382 type PutRecordInput struct { 383 383 Collection string 384 384 RKey string 385 - Record interface{} 385 + Record any 386 386 } 387 387 388 388 // PutRecord updates an existing record in the user's repository ··· 397 397 } 398 398 399 399 // Build the request body 400 - body := map[string]interface{}{ 400 + body := map[string]any{ 401 401 "repo": did.String(), 402 402 "collection": input.Collection, 403 403 "rkey": input.RKey, ··· 456 456 } 457 457 458 458 // Build the request body 459 - body := map[string]interface{}{ 459 + body := map[string]any{ 460 460 "repo": did.String(), 461 461 "collection": input.Collection, 462 462 "rkey": input.RKey,
+2 -2
internal/atproto/public_client.go
··· 158 158 break 159 159 } 160 160 } 161 - } else if strings.HasPrefix(did, "did:web:") { 161 + } else if after, ok := strings.CutPrefix(did, "did:web:"); ok { 162 162 // Web DID - the domain is the PDS 163 163 // Validate domain to prevent SSRF attacks 164 - domain := strings.TrimPrefix(did, "did:web:") 164 + domain := after 165 165 // Handle percent-encoded colons for ports (e.g., did:web:example.com%3A8080) 166 166 domain = strings.ReplaceAll(domain, "%3A", ":") 167 167
+43 -43
internal/atproto/records.go
··· 11 11 12 12 // toFloat64 extracts a numeric value from an interface{} that may be int or float64. 13 13 // JSON decoding produces float64, but in-memory maps may contain int. 14 - func toFloat64(v interface{}) (float64, bool) { 14 + func toFloat64(v any) (float64, bool) { 15 15 switch n := v.(type) { 16 16 case float64: 17 17 return n, true ··· 25 25 // ========== Recipe Conversions ========== 26 26 27 27 // RecipeToRecord converts a models.Recipe to an atproto record map 28 - func RecipeToRecord(recipe *models.Recipe, brewerURI string) (map[string]interface{}, error) { 29 - record := map[string]interface{}{ 28 + func RecipeToRecord(recipe *models.Recipe, brewerURI string) (map[string]any, error) { 29 + record := map[string]any{ 30 30 "$type": NSIDRecipe, 31 31 "name": recipe.Name, 32 32 "createdAt": recipe.CreatedAt.Format(time.RFC3339), ··· 52 52 } 53 53 54 54 if len(recipe.Pours) > 0 { 55 - pours := make([]map[string]interface{}, len(recipe.Pours)) 55 + pours := make([]map[string]any, len(recipe.Pours)) 56 56 for i, pour := range recipe.Pours { 57 - pours[i] = map[string]interface{}{ 57 + pours[i] = map[string]any{ 58 58 "waterAmount": pour.WaterAmount, 59 59 "timeSeconds": pour.TimeSeconds, 60 60 } ··· 66 66 } 67 67 68 68 // RecordToRecipe converts an atproto record map to a models.Recipe 69 - func RecordToRecipe(record map[string]interface{}, atURI string) (*models.Recipe, error) { 69 + func RecordToRecipe(record map[string]any, atURI string) (*models.Recipe, error) { 70 70 recipe := &models.Recipe{} 71 71 72 72 if atURI != "" { ··· 109 109 recipe.SourceRef = sourceRef 110 110 } 111 111 112 - if poursRaw, ok := record["pours"].([]interface{}); ok { 112 + if poursRaw, ok := record["pours"].([]any); ok { 113 113 recipe.Pours = make([]*models.Pour, len(poursRaw)) 114 114 for i, pourRaw := range poursRaw { 115 - pourMap, ok := pourRaw.(map[string]interface{}) 115 + pourMap, ok := pourRaw.(map[string]any) 116 116 if !ok { 117 117 continue 118 118 } ··· 135 135 136 136 // BrewToRecord converts a models.Brew to an atproto record map 137 137 // Note: References (beanRef, grinderRef, brewerRef, recipeRef) must be AT-URIs 138 - func BrewToRecord(brew *models.Brew, beanURI, grinderURI, brewerURI, recipeURI string) (map[string]interface{}, error) { 138 + func BrewToRecord(brew *models.Brew, beanURI, grinderURI, brewerURI, recipeURI string) (map[string]any, error) { 139 139 if beanURI == "" { 140 140 return nil, fmt.Errorf("beanRef (AT-URI) is required") 141 141 } 142 142 143 - record := map[string]interface{}{ 143 + record := map[string]any{ 144 144 "$type": NSIDBrew, 145 145 "beanRef": beanURI, 146 146 "createdAt": brew.CreatedAt.Format(time.RFC3339), ··· 184 184 185 185 // Convert pours to embedded array 186 186 if len(brew.Pours) > 0 { 187 - pours := make([]map[string]interface{}, len(brew.Pours)) 187 + pours := make([]map[string]any, len(brew.Pours)) 188 188 for i, pour := range brew.Pours { 189 - pours[i] = map[string]interface{}{ 189 + pours[i] = map[string]any{ 190 190 "waterAmount": pour.WaterAmount, 191 191 "timeSeconds": pour.TimeSeconds, 192 192 } ··· 196 196 197 197 // Espresso-specific params 198 198 if brew.EspressoParams != nil { 199 - ep := map[string]interface{}{} 199 + ep := map[string]any{} 200 200 if brew.EspressoParams.YieldWeight > 0 { 201 201 ep["yieldWeight"] = int(brew.EspressoParams.YieldWeight * 10) // tenths of a gram 202 202 } ··· 213 213 214 214 // Pour-over-specific params 215 215 if brew.PouroverParams != nil { 216 - pp := map[string]interface{}{} 216 + pp := map[string]any{} 217 217 if brew.PouroverParams.BloomWater > 0 { 218 218 pp["bloomWater"] = brew.PouroverParams.BloomWater 219 219 } ··· 236 236 237 237 // RecordToBrew converts an atproto record map to a models.Brew 238 238 // The atURI parameter should be the full AT-URI of this brew record 239 - func RecordToBrew(record map[string]interface{}, atURI string) (*models.Brew, error) { 239 + func RecordToBrew(record map[string]any, atURI string) (*models.Brew, error) { 240 240 brew := &models.Brew{} 241 241 242 242 // Extract rkey from AT-URI ··· 295 295 } 296 296 297 297 // Convert pours from embedded array 298 - if poursRaw, ok := record["pours"].([]interface{}); ok { 298 + if poursRaw, ok := record["pours"].([]any); ok { 299 299 brew.Pours = make([]*models.Pour, len(poursRaw)) 300 300 for i, pourRaw := range poursRaw { 301 - pourMap, ok := pourRaw.(map[string]interface{}) 301 + pourMap, ok := pourRaw.(map[string]any) 302 302 if !ok { 303 303 continue 304 304 } ··· 315 315 } 316 316 317 317 // Espresso params 318 - if epRaw, ok := record["espressoParams"].(map[string]interface{}); ok { 318 + if epRaw, ok := record["espressoParams"].(map[string]any); ok { 319 319 ep := &models.EspressoParams{} 320 320 if v, ok := toFloat64(epRaw["yieldWeight"]); ok { 321 321 ep.YieldWeight = v / 10.0 ··· 330 330 } 331 331 332 332 // Pour-over params 333 - if ppRaw, ok := record["pouroverParams"].(map[string]interface{}); ok { 333 + if ppRaw, ok := record["pouroverParams"].(map[string]any); ok { 334 334 pp := &models.PouroverParams{} 335 335 if v, ok := toFloat64(ppRaw["bloomWater"]); ok { 336 336 pp.BloomWater = int(v) ··· 353 353 // ========== Bean Conversions ========== 354 354 355 355 // BeanToRecord converts a models.Bean to an atproto record map 356 - func BeanToRecord(bean *models.Bean, roasterURI string) (map[string]interface{}, error) { 357 - record := map[string]interface{}{ 356 + func BeanToRecord(bean *models.Bean, roasterURI string) (map[string]any, error) { 357 + record := map[string]any{ 358 358 "$type": NSIDBean, 359 359 "name": bean.Name, 360 360 "createdAt": bean.CreatedAt.Format(time.RFC3339), ··· 392 392 } 393 393 394 394 // RecordToBean converts an atproto record map to a models.Bean 395 - func RecordToBean(record map[string]interface{}, atURI string) (*models.Bean, error) { 395 + func RecordToBean(record map[string]any, atURI string) (*models.Bean, error) { 396 396 bean := &models.Bean{} 397 397 398 398 // Extract rkey from AT-URI ··· 455 455 // ========== Roaster Conversions ========== 456 456 457 457 // RoasterToRecord converts a models.Roaster to an atproto record map 458 - func RoasterToRecord(roaster *models.Roaster) (map[string]interface{}, error) { 459 - record := map[string]interface{}{ 458 + func RoasterToRecord(roaster *models.Roaster) (map[string]any, error) { 459 + record := map[string]any{ 460 460 "$type": NSIDRoaster, 461 461 "name": roaster.Name, 462 462 "createdAt": roaster.CreatedAt.Format(time.RFC3339), ··· 477 477 } 478 478 479 479 // RecordToRoaster converts an atproto record map to a models.Roaster 480 - func RecordToRoaster(record map[string]interface{}, atURI string) (*models.Roaster, error) { 480 + func RecordToRoaster(record map[string]any, atURI string) (*models.Roaster, error) { 481 481 roaster := &models.Roaster{} 482 482 483 483 // Extract rkey from AT-URI ··· 524 524 // ========== Grinder Conversions ========== 525 525 526 526 // GrinderToRecord converts a models.Grinder to an atproto record map 527 - func GrinderToRecord(grinder *models.Grinder) (map[string]interface{}, error) { 528 - record := map[string]interface{}{ 527 + func GrinderToRecord(grinder *models.Grinder) (map[string]any, error) { 528 + record := map[string]any{ 529 529 "$type": NSIDGrinder, 530 530 "name": grinder.Name, 531 531 "createdAt": grinder.CreatedAt.Format(time.RFC3339), ··· 549 549 } 550 550 551 551 // RecordToGrinder converts an atproto record map to a models.Grinder 552 - func RecordToGrinder(record map[string]interface{}, atURI string) (*models.Grinder, error) { 552 + func RecordToGrinder(record map[string]any, atURI string) (*models.Grinder, error) { 553 553 grinder := &models.Grinder{} 554 554 555 555 // Extract rkey from AT-URI ··· 599 599 // ========== Brewer Conversions ========== 600 600 601 601 // BrewerToRecord converts a models.Brewer to an atproto record map 602 - func BrewerToRecord(brewer *models.Brewer) (map[string]interface{}, error) { 603 - record := map[string]interface{}{ 602 + func BrewerToRecord(brewer *models.Brewer) (map[string]any, error) { 603 + record := map[string]any{ 604 604 "$type": NSIDBrewer, 605 605 "name": brewer.Name, 606 606 "createdAt": brewer.CreatedAt.Format(time.RFC3339), ··· 621 621 } 622 622 623 623 // RecordToBrewer converts an atproto record map to a models.Brewer 624 - func RecordToBrewer(record map[string]interface{}, atURI string) (*models.Brewer, error) { 624 + func RecordToBrewer(record map[string]any, atURI string) (*models.Brewer, error) { 625 625 brewer := &models.Brewer{} 626 626 627 627 // Extract rkey from AT-URI ··· 669 669 670 670 // LikeToRecord converts a models.Like to an atproto record map 671 671 // Uses com.atproto.repo.strongRef format for the subject 672 - func LikeToRecord(like *models.Like) (map[string]interface{}, error) { 672 + func LikeToRecord(like *models.Like) (map[string]any, error) { 673 673 if like.SubjectURI == "" { 674 674 return nil, fmt.Errorf("subject URI is required") 675 675 } ··· 677 677 return nil, fmt.Errorf("subject CID is required") 678 678 } 679 679 680 - record := map[string]interface{}{ 680 + record := map[string]any{ 681 681 "$type": NSIDLike, 682 - "subject": map[string]interface{}{ 682 + "subject": map[string]any{ 683 683 "uri": like.SubjectURI, 684 684 "cid": like.SubjectCID, 685 685 }, ··· 690 690 } 691 691 692 692 // RecordToLike converts an atproto record map to a models.Like 693 - func RecordToLike(record map[string]interface{}, atURI string) (*models.Like, error) { 693 + func RecordToLike(record map[string]any, atURI string) (*models.Like, error) { 694 694 like := &models.Like{} 695 695 696 696 // Extract rkey from AT-URI ··· 703 703 } 704 704 705 705 // Required field: subject (strongRef) 706 - subject, ok := record["subject"].(map[string]interface{}) 706 + subject, ok := record["subject"].(map[string]any) 707 707 if !ok { 708 708 return nil, fmt.Errorf("subject is required") 709 709 } ··· 737 737 738 738 // CommentToRecord converts a models.Comment to an atproto record map 739 739 // Uses com.atproto.repo.strongRef format for the subject 740 - func CommentToRecord(comment *models.Comment) (map[string]interface{}, error) { 740 + func CommentToRecord(comment *models.Comment) (map[string]any, error) { 741 741 if comment.SubjectURI == "" { 742 742 return nil, fmt.Errorf("subject URI is required") 743 743 } ··· 748 748 return nil, fmt.Errorf("text is required") 749 749 } 750 750 751 - record := map[string]interface{}{ 751 + record := map[string]any{ 752 752 "$type": NSIDComment, 753 - "subject": map[string]interface{}{ 753 + "subject": map[string]any{ 754 754 "uri": comment.SubjectURI, 755 755 "cid": comment.SubjectCID, 756 756 }, ··· 760 760 761 761 // Add optional parent reference for replies 762 762 if comment.ParentURI != "" && comment.ParentCID != "" { 763 - record["parent"] = map[string]interface{}{ 763 + record["parent"] = map[string]any{ 764 764 "uri": comment.ParentURI, 765 765 "cid": comment.ParentCID, 766 766 } ··· 770 770 } 771 771 772 772 // RecordToComment converts an atproto record map to a models.Comment 773 - func RecordToComment(record map[string]interface{}, atURI string) (*models.Comment, error) { 773 + func RecordToComment(record map[string]any, atURI string) (*models.Comment, error) { 774 774 comment := &models.Comment{} 775 775 776 776 // Extract rkey from AT-URI ··· 783 783 } 784 784 785 785 // Required field: subject (strongRef) 786 - subject, ok := record["subject"].(map[string]interface{}) 786 + subject, ok := record["subject"].(map[string]any) 787 787 if !ok { 788 788 return nil, fmt.Errorf("subject is required") 789 789 } ··· 818 818 comment.CreatedAt = createdAt 819 819 820 820 // Optional field: parent (strongRef for replies) 821 - if parent, ok := record["parent"].(map[string]interface{}); ok { 821 + if parent, ok := record["parent"].(map[string]any); ok { 822 822 if parentURI, ok := parent["uri"].(string); ok && parentURI != "" { 823 823 comment.ParentURI = parentURI 824 824 }
+16 -16
internal/atproto/records_test.go
··· 79 79 } 80 80 81 81 // Check pours 82 - pours, ok := record["pours"].([]map[string]interface{}) 82 + pours, ok := record["pours"].([]map[string]any) 83 83 if !ok { 84 84 t.Fatalf("pours is not []map[string]interface{}") 85 85 } ··· 135 135 136 136 func TestRecordToBrew(t *testing.T) { 137 137 t.Run("full record", func(t *testing.T) { 138 - record := map[string]interface{}{ 138 + record := map[string]any{ 139 139 "$type": NSIDBrew, 140 140 "beanRef": "at://did:plc:test/social.arabica.alpha.bean/bean123", 141 141 "createdAt": "2025-01-10T12:00:00Z", ··· 148 148 "brewerRef": "at://did:plc:test/social.arabica.alpha.brewer/brewer123", 149 149 "tastingNotes": "Fruity", 150 150 "rating": float64(8), 151 - "pours": []interface{}{ 152 - map[string]interface{}{"waterAmount": float64(50), "timeSeconds": float64(30)}, 153 - map[string]interface{}{"waterAmount": float64(100), "timeSeconds": float64(60)}, 151 + "pours": []any{ 152 + map[string]any{"waterAmount": float64(50), "timeSeconds": float64(30)}, 153 + map[string]any{"waterAmount": float64(100), "timeSeconds": float64(60)}, 154 154 }, 155 155 } 156 156 ··· 198 198 }) 199 199 200 200 t.Run("error without beanRef", func(t *testing.T) { 201 - record := map[string]interface{}{ 201 + record := map[string]any{ 202 202 "$type": NSIDBrew, 203 203 "createdAt": "2025-01-10T12:00:00Z", 204 204 } ··· 210 210 }) 211 211 212 212 t.Run("error without createdAt", func(t *testing.T) { 213 - record := map[string]interface{}{ 213 + record := map[string]any{ 214 214 "$type": NSIDBrew, 215 215 "beanRef": "at://did:plc:test/social.arabica.alpha.bean/bean123", 216 216 } ··· 222 222 }) 223 223 224 224 t.Run("error with invalid AT-URI", func(t *testing.T) { 225 - record := map[string]interface{}{ 225 + record := map[string]any{ 226 226 "$type": NSIDBrew, 227 227 "beanRef": "at://did:plc:test/social.arabica.alpha.bean/bean123", 228 228 "createdAt": "2025-01-10T12:00:00Z", ··· 297 297 298 298 func TestRecordToBean(t *testing.T) { 299 299 t.Run("full record", func(t *testing.T) { 300 - record := map[string]interface{}{ 300 + record := map[string]any{ 301 301 "$type": NSIDBean, 302 302 "name": "Ethiopian Yirgacheffe", 303 303 "origin": "Ethiopia", ··· 334 334 }) 335 335 336 336 t.Run("error without name", func(t *testing.T) { 337 - record := map[string]interface{}{ 337 + record := map[string]any{ 338 338 "$type": NSIDBean, 339 339 "createdAt": "2025-01-10T12:00:00Z", 340 340 } ··· 379 379 380 380 func TestRecordToRoaster(t *testing.T) { 381 381 t.Run("full record", func(t *testing.T) { 382 - record := map[string]interface{}{ 382 + record := map[string]any{ 383 383 "$type": NSIDRoaster, 384 384 "name": "Counter Culture", 385 385 "location": "Durham, NC", ··· 408 408 }) 409 409 410 410 t.Run("error without name", func(t *testing.T) { 411 - record := map[string]interface{}{ 411 + record := map[string]any{ 412 412 "$type": NSIDRoaster, 413 413 "createdAt": "2025-01-10T12:00:00Z", 414 414 } ··· 457 457 458 458 func TestRecordToGrinder(t *testing.T) { 459 459 t.Run("full record", func(t *testing.T) { 460 - record := map[string]interface{}{ 460 + record := map[string]any{ 461 461 "$type": NSIDGrinder, 462 462 "name": "Comandante C40", 463 463 "grinderType": "Hand", ··· 519 519 520 520 func TestRecordToBrewer(t *testing.T) { 521 521 t.Run("full record", func(t *testing.T) { 522 - record := map[string]interface{}{ 522 + record := map[string]any{ 523 523 "$type": NSIDBrewer, 524 524 "name": "Hario V60", 525 525 "description": "Pour-over dripper", ··· 738 738 assert.NoError(t, err) 739 739 740 740 // Verify espressoParams is in the record 741 - ep, ok := record["espressoParams"].(map[string]interface{}) 741 + ep, ok := record["espressoParams"].(map[string]any) 742 742 assert.True(t, ok) 743 743 assert.Equal(t, 360, ep["yieldWeight"]) // 36.0 * 10 744 744 assert.Equal(t, 90, ep["pressure"]) // 9.0 * 10 ··· 767 767 record, err := BrewToRecord(original, "at://did:plc:test/social.arabica.alpha.bean/abc123", "", "", "") 768 768 assert.NoError(t, err) 769 769 770 - pp, ok := record["pouroverParams"].(map[string]interface{}) 770 + pp, ok := record["pouroverParams"].(map[string]any) 771 771 assert.True(t, ok) 772 772 assert.Equal(t, 50, pp["bloomWater"]) 773 773 assert.Equal(t, 45, pp["bloomSeconds"])
+1 -1
internal/atproto/resolver.go
··· 38 38 atURI string, 39 39 sessionID string, 40 40 expectedCollection string, 41 - convert func(map[string]interface{}, string) (*T, error), 41 + convert func(map[string]any, string) (*T, error), 42 42 ) (*T, error) { 43 43 if atURI == "" { 44 44 return nil, nil
+4 -4
internal/atproto/store.go
··· 49 49 } 50 50 51 51 // witnessRecordToMap is a package-internal alias for WitnessRecordToMap. 52 - func witnessRecordToMap(wr *WitnessRecord) (map[string]interface{}, error) { 52 + func witnessRecordToMap(wr *WitnessRecord) (map[string]any, error) { 53 53 return WitnessRecordToMap(wr) 54 54 } 55 55 ··· 101 101 // writeThroughWitness upserts a record into the witness cache after a 102 102 // successful PDS write so subsequent reads see the latest data without 103 103 // waiting for the firehose to re-index. 104 - func (s *AtprotoStore) writeThroughWitness(collection, rkey, cid string, record interface{}) { 104 + func (s *AtprotoStore) writeThroughWitness(collection, rkey, cid string, record any) { 105 105 if s.witnessCache == nil { 106 106 return 107 107 } ··· 146 146 // resolveBrewRefsFromWitness resolves a brew's references (bean, grinder, brewer, recipe) 147 147 // entirely from the witness cache, avoiding any PDS calls. Falls back to PDS-based resolution 148 148 // only if a witness lookup fails for any referenced record. 149 - func (s *AtprotoStore) resolveBrewRefsFromWitness(ctx context.Context, brew *models.Brew, record map[string]interface{}) { 149 + func (s *AtprotoStore) resolveBrewRefsFromWitness(ctx context.Context, brew *models.Brew, record map[string]any) { 150 150 // Resolve bean (and its roaster) 151 151 if beanRef, _ := record["beanRef"].(string); beanRef != "" { 152 152 if beanWR := s.getWitnessRecordByURI(ctx, beanRef); beanWR != nil { ··· 227 227 // ========== Brew Helpers ========== 228 228 229 229 // ExtractBrewRefRKeys extracts rkeys from AT-URI references in a brew record's raw values. 230 - func ExtractBrewRefRKeys(brew *models.Brew, record map[string]interface{}) { 230 + func ExtractBrewRefRKeys(brew *models.Brew, record map[string]any) { 231 231 if beanRef, _ := record["beanRef"].(string); beanRef != "" { 232 232 if c, err := ResolveATURI(beanRef); err == nil { 233 233 brew.BeanRKey = c.RKey
+2 -2
internal/atproto/witness.go
··· 20 20 21 21 // WitnessRecordToMap unmarshals a WitnessRecord's raw JSON into the map format 22 22 // expected by the Record* conversion functions. 23 - func WitnessRecordToMap(wr *WitnessRecord) (map[string]interface{}, error) { 24 - var m map[string]interface{} 23 + func WitnessRecordToMap(wr *WitnessRecord) (map[string]any, error) { 24 + var m map[string]any 25 25 if err := json.Unmarshal(wr.Record, &m); err != nil { 26 26 return nil, err 27 27 }
+12 -14
internal/firehose/consumer.go
··· 84 84 85 85 // Start begins consuming events in a background goroutine 86 86 func (c *Consumer) Start(ctx context.Context) { 87 - c.wg.Add(1) 88 - go func() { 89 - defer c.wg.Done() 87 + c.wg.Go(func() { 90 88 c.run(ctx) 91 - }() 89 + }) 92 90 } 93 91 94 92 // Stop gracefully stops the consumer ··· 343 341 344 342 // Special handling for likes - index for counts 345 343 if commit.Collection == "social.arabica.alpha.like" { 346 - var recordData map[string]interface{} 344 + var recordData map[string]any 347 345 if err := json.Unmarshal(commit.Record, &recordData); err == nil { 348 - if subject, ok := recordData["subject"].(map[string]interface{}); ok { 346 + if subject, ok := recordData["subject"].(map[string]any); ok { 349 347 if subjectURI, ok := subject["uri"].(string); ok { 350 348 if err := c.index.UpsertLike(context.Background(), event.DID, commit.RKey, subjectURI); err != nil { 351 349 log.Warn().Err(err).Str("did", event.DID).Str("subject", subjectURI).Msg("failed to index like") ··· 359 357 360 358 // Special handling for comments - index for counts and retrieval 361 359 if commit.Collection == "social.arabica.alpha.comment" { 362 - var recordData map[string]interface{} 360 + var recordData map[string]any 363 361 if err := json.Unmarshal(commit.Record, &recordData); err == nil { 364 - if subject, ok := recordData["subject"].(map[string]interface{}); ok { 362 + if subject, ok := recordData["subject"].(map[string]any); ok { 365 363 if subjectURI, ok := subject["uri"].(string); ok { 366 364 text, _ := recordData["text"].(string) 367 365 var createdAt time.Time ··· 376 374 } 377 375 // Extract optional parent URI for threading 378 376 var parentURI string 379 - if parent, ok := recordData["parent"].(map[string]interface{}); ok { 377 + if parent, ok := recordData["parent"].(map[string]any); ok { 380 378 parentURI, _ = parent["uri"].(string) 381 379 } 382 380 if err := c.index.UpsertComment(context.Background(), event.DID, commit.RKey, subjectURI, parentURI, commit.CID, text, createdAt); err != nil { ··· 397 395 context.Background(), 398 396 fmt.Sprintf("at://%s/%s/%s", event.DID, commit.Collection, commit.RKey), 399 397 ); err == nil && existingRecord != nil { 400 - var recordData map[string]interface{} 398 + var recordData map[string]any 401 399 if err := json.Unmarshal(existingRecord.Record, &recordData); err == nil { 402 - if subject, ok := recordData["subject"].(map[string]interface{}); ok { 400 + if subject, ok := recordData["subject"].(map[string]any); ok { 403 401 if subjectURI, ok := subject["uri"].(string); ok { 404 402 if err := c.index.DeleteLike(context.Background(), event.DID, subjectURI); err != nil { 405 403 log.Warn().Err(err).Str("did", event.DID).Str("subject", subjectURI).Msg("failed to delete like index") ··· 418 416 context.Background(), 419 417 fmt.Sprintf("at://%s/%s/%s", event.DID, commit.Collection, commit.RKey), 420 418 ); err == nil && existingRecord != nil { 421 - var recordData map[string]interface{} 419 + var recordData map[string]any 422 420 if err := json.Unmarshal(existingRecord.Record, &recordData); err == nil { 423 - if subject, ok := recordData["subject"].(map[string]interface{}); ok { 421 + if subject, ok := recordData["subject"].(map[string]any); ok { 424 422 if subjectURI, ok := subject["uri"].(string); ok { 425 423 var parentURI string 426 - if parent, ok := recordData["parent"].(map[string]interface{}); ok { 424 + if parent, ok := recordData["parent"].(map[string]any); ok { 427 425 parentURI, _ = parent["uri"].(string) 428 426 } 429 427 if err := c.index.DeleteComment(context.Background(), event.DID, commit.RKey, subjectURI); err != nil {
+4 -7
internal/firehose/index.go
··· 1476 1476 1477 1477 switch collection { 1478 1478 case atproto.NSIDLike: 1479 - if subject, ok := record.Value["subject"].(map[string]interface{}); ok { 1479 + if subject, ok := record.Value["subject"].(map[string]any); ok { 1480 1480 if subjectURI, ok := subject["uri"].(string); ok { 1481 1481 if err := idx.UpsertLike(ctx, did, rkey, subjectURI); err != nil { 1482 1482 log.Warn().Err(err).Str("uri", record.URI).Msg("failed to index like during backfill") ··· 1484 1484 } 1485 1485 } 1486 1486 case atproto.NSIDComment: 1487 - if subject, ok := record.Value["subject"].(map[string]interface{}); ok { 1487 + if subject, ok := record.Value["subject"].(map[string]any); ok { 1488 1488 if subjectURI, ok := subject["uri"].(string); ok { 1489 1489 text, _ := record.Value["text"].(string) 1490 1490 var createdAt time.Time ··· 1498 1498 createdAt = time.Now() 1499 1499 } 1500 1500 var parentURI string 1501 - if parent, ok := record.Value["parent"].(map[string]interface{}); ok { 1501 + if parent, ok := record.Value["parent"].(map[string]any); ok { 1502 1502 parentURI, _ = parent["uri"].(string) 1503 1503 } 1504 1504 if err := idx.UpsertComment(ctx, did, rkey, subjectURI, parentURI, record.CID, text, createdAt); err != nil { ··· 1841 1841 if limit > 0 && len(result) >= limit { 1842 1842 return 1843 1843 } 1844 - visualDepth := depth 1845 - if visualDepth > 2 { 1846 - visualDepth = 2 1847 - } 1844 + visualDepth := min(depth, 2) 1848 1845 comment.Depth = visualDepth 1849 1846 result = append(result, *comment) 1850 1847
+11 -11
internal/firehose/index_test.go
··· 167 167 // Create a chain of comments: depth 0 -> 1 -> 2 -> 3 -> 4 168 168 now := time.Now() 169 169 parentURI := "" 170 - for i := 0; i < 5; i++ { 170 + for i := range 5 { 171 171 rkey := "comment" + string(rune('A'+i)) 172 172 err = idx.UpsertComment(ctx, "did:plc:user", rkey, subjectURI, parentURI, "cid"+rkey, "Comment", now.Add(time.Duration(i)*time.Second)) 173 173 assert.NoError(t, err) ··· 273 273 274 274 // User1 rates bean1: 6, 8 275 275 for i, rating := range []int{6, 8} { 276 - record := []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":%d,"createdAt":"2025-01-01T00:00:00Z"}`, bean1, rating)) 276 + record := fmt.Appendf(nil, `{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":%d,"createdAt":"2025-01-01T00:00:00Z"}`, bean1, rating) 277 277 assert.NoError(t, idx.UpsertRecord(ctx, "did:plc:user1", "social.arabica.alpha.brew", fmt.Sprintf("u1b1_%d", i), "cid", record, now)) 278 278 } 279 279 280 280 // User2 rates bean1: 10 281 - record := []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":10,"createdAt":"2025-01-01T00:00:00Z"}`, bean1)) 281 + record := fmt.Appendf(nil, `{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":10,"createdAt":"2025-01-01T00:00:00Z"}`, bean1) 282 282 assert.NoError(t, idx.UpsertRecord(ctx, "did:plc:user2", "social.arabica.alpha.brew", "u2b1_0", "cid", record, now)) 283 283 284 284 // User1 rates bean2: 4 285 - record = []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":4,"createdAt":"2025-01-01T00:00:00Z"}`, bean2)) 285 + record = fmt.Appendf(nil, `{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":4,"createdAt":"2025-01-01T00:00:00Z"}`, bean2) 286 286 assert.NoError(t, idx.UpsertRecord(ctx, "did:plc:user1", "social.arabica.alpha.brew", "u1b2_0", "cid", record, now)) 287 287 288 288 // Per-user1: bean1 avg=7, bean2 avg=4 ··· 312 312 beanURI := "at://did:plc:user1/social.arabica.alpha.bean/bean1" 313 313 314 314 // Brew with rating 315 - record := []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":7,"createdAt":"2025-01-01T00:00:00Z"}`, beanURI)) 315 + record := fmt.Appendf(nil, `{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":7,"createdAt":"2025-01-01T00:00:00Z"}`, beanURI) 316 316 assert.NoError(t, idx.UpsertRecord(ctx, "did:plc:user1", "social.arabica.alpha.brew", "brew1", "cid", record, now)) 317 317 318 318 // Brew without rating 319 - record = []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.brew","beanRef":"%s","createdAt":"2025-01-02T00:00:00Z"}`, beanURI)) 319 + record = fmt.Appendf(nil, `{"$type":"social.arabica.alpha.brew","beanRef":"%s","createdAt":"2025-01-02T00:00:00Z"}`, beanURI) 320 320 assert.NoError(t, idx.UpsertRecord(ctx, "did:plc:user1", "social.arabica.alpha.brew", "brew2", "cid", record, now)) 321 321 322 322 stats := idx.AvgBrewRatingByBeanURI(ctx, "") ··· 338 338 roasterURI := "at://did:plc:user1/social.arabica.alpha.roaster/roaster1" 339 339 340 340 // Insert the bean record with roaster reference 341 - beanRecord := []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.bean","name":"Ethiopia Yirgacheffe","roasterRef":"%s","createdAt":"2025-01-01T00:00:00Z"}`, roasterURI)) 341 + beanRecord := fmt.Appendf(nil, `{"$type":"social.arabica.alpha.bean","name":"Ethiopia Yirgacheffe","roasterRef":"%s","createdAt":"2025-01-01T00:00:00Z"}`, roasterURI) 342 342 assert.NoError(t, idx.UpsertRecord(ctx, did, "social.arabica.alpha.bean", "bean1", "cid", beanRecord, now)) 343 343 344 344 // Insert brews referencing that bean with ratings 345 345 for i, rating := range []int{6, 8, 10} { 346 - record := []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":%d,"createdAt":"2025-01-0%dT00:00:00Z"}`, beanURI, rating, i+1)) 346 + record := fmt.Appendf(nil, `{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":%d,"createdAt":"2025-01-0%dT00:00:00Z"}`, beanURI, rating, i+1) 347 347 assert.NoError(t, idx.UpsertRecord(ctx, did, "social.arabica.alpha.brew", fmt.Sprintf("brew%d", i), "cid", record, now)) 348 348 } 349 349 ··· 375 375 376 376 // Two beans from the same roaster 377 377 for _, b := range []struct{ uri, rkey string }{{bean1URI, "bean1"}, {bean2URI, "bean2"}} { 378 - record := []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.bean","name":"Bean","roasterRef":"%s","createdAt":"2025-01-01T00:00:00Z"}`, roasterURI)) 378 + record := fmt.Appendf(nil, `{"$type":"social.arabica.alpha.bean","name":"Bean","roasterRef":"%s","createdAt":"2025-01-01T00:00:00Z"}`, roasterURI) 379 379 assert.NoError(t, idx.UpsertRecord(ctx, did, "social.arabica.alpha.bean", b.rkey, "cid", record, now)) 380 380 } 381 381 382 382 // Brews: bean1 rated 6, bean2 rated 10 383 - record := []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":6,"createdAt":"2025-01-01T00:00:00Z"}`, bean1URI)) 383 + record := fmt.Appendf(nil, `{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":6,"createdAt":"2025-01-01T00:00:00Z"}`, bean1URI) 384 384 assert.NoError(t, idx.UpsertRecord(ctx, did, "social.arabica.alpha.brew", "brew1", "cid", record, now)) 385 - record = []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":10,"createdAt":"2025-01-01T00:00:00Z"}`, bean2URI)) 385 + record = fmt.Appendf(nil, `{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":10,"createdAt":"2025-01-01T00:00:00Z"}`, bean2URI) 386 386 assert.NoError(t, idx.UpsertRecord(ctx, did, "social.arabica.alpha.brew", "brew2", "cid", record, now)) 387 387 388 388 stats := idx.AvgBrewRatingByRoasterURI(ctx, "")
+3 -3
internal/firehose/notifications_test.go
··· 100 100 assert.Equal(t, 0, idx.GetUnreadCount(targetDID)) 101 101 102 102 // Add some notifications 103 - for i := 0; i < 3; i++ { 103 + for i := range 3 { 104 104 notif := models.Notification{ 105 105 Type: models.NotificationLike, 106 106 ActorDID: "did:plc:actor" + string(rune('a'+i)), ··· 120 120 baseTime := time.Now().Add(-time.Minute) // use past times to avoid race 121 121 122 122 // Add notifications 123 - for i := 0; i < 3; i++ { 123 + for i := range 3 { 124 124 notif := models.Notification{ 125 125 Type: models.NotificationLike, 126 126 ActorDID: "did:plc:actor" + string(rune('a'+i)), ··· 152 152 baseTime := time.Now().Add(-time.Minute) 153 153 154 154 // Add 5 notifications 155 - for i := 0; i < 5; i++ { 155 + for i := range 5 { 156 156 notif := models.Notification{ 157 157 Type: models.NotificationLike, 158 158 ActorDID: "did:plc:actor" + string(rune('a'+i)),
+5 -5
internal/handlers/auth.go
··· 311 311 log.Warn().Err(err).Str("did", resolveResult.DID).Msg("Failed to fetch profile") 312 312 // Return just the DID if we can't get the profile 313 313 w.Header().Set("Content-Type", "application/json") 314 - if err := json.NewEncoder(w).Encode(map[string]interface{}{ 314 + if err := json.NewEncoder(w).Encode(map[string]any{ 315 315 "did": resolveResult.DID, 316 316 "handle": handle, 317 317 }); err != nil { ··· 324 324 if profileResp.StatusCode != 200 { 325 325 // Return just the DID if we can't get the profile 326 326 w.Header().Set("Content-Type", "application/json") 327 - if err := json.NewEncoder(w).Encode(map[string]interface{}{ 327 + if err := json.NewEncoder(w).Encode(map[string]any{ 328 328 "did": resolveResult.DID, 329 329 "handle": handle, 330 330 }); err != nil { ··· 343 343 log.Warn().Err(err).Str("did", resolveResult.DID).Msg("Failed to decode profile") 344 344 // Return just the DID if we can't parse the profile 345 345 w.Header().Set("Content-Type", "application/json") 346 - if err := json.NewEncoder(w).Encode(map[string]interface{}{ 346 + if err := json.NewEncoder(w).Encode(map[string]any{ 347 347 "did": resolveResult.DID, 348 348 "handle": handle, 349 349 }); err != nil { ··· 400 400 Msg("Unexpected status searching actors") 401 401 // Return empty results instead of error 402 402 w.Header().Set("Content-Type", "application/json") 403 - if err := json.NewEncoder(w).Encode(map[string]interface{}{"actors": []interface{}{}}); err != nil { 403 + if err := json.NewEncoder(w).Encode(map[string]any{"actors": []any{}}); err != nil { 404 404 log.Error().Err(err).Msg("Failed to encode empty actors response") 405 405 } 406 406 return ··· 419 419 log.Warn().Err(err).Str("query", query).Msg("Failed to decode search response") 420 420 // Return empty results instead of error 421 421 w.Header().Set("Content-Type", "application/json") 422 - if err := json.NewEncoder(w).Encode(map[string]interface{}{"actors": []interface{}{}}); err != nil { 422 + if err := json.NewEncoder(w).Encode(map[string]any{"actors": []any{}}); err != nil { 423 423 log.Error().Err(err).Msg("Failed to encode empty actors response") 424 424 } 425 425 return
+3 -3
internal/handlers/brew.go
··· 318 318 } 319 319 320 320 // resolveBrewReferences resolves bean, grinder, and brewer references for a brew 321 - func (h *Handler) resolveBrewReferences(ctx context.Context, brew *models.Brew, ownerDID string, record map[string]interface{}) error { 321 + func (h *Handler) resolveBrewReferences(ctx context.Context, brew *models.Brew, ownerDID string, record map[string]any) error { 322 322 publicClient := atproto.NewPublicClient() 323 323 324 324 // Resolve bean reference ··· 366 366 367 367 // resolveBrewRefsFromWitness resolves a brew's references (bean, grinder, brewer, recipe) 368 368 // from the witness cache, avoiding PDS calls for public brew views. 369 - func (h *Handler) resolveBrewRefsFromWitness(ctx context.Context, brew *models.Brew, ownerDID string, record map[string]interface{}) { 369 + func (h *Handler) resolveBrewRefsFromWitness(ctx context.Context, brew *models.Brew, ownerDID string, record map[string]any) { 370 370 if h.witnessCache == nil { 371 371 return 372 372 } ··· 543 543 func parsePours(r *http.Request) []models.CreatePourData { 544 544 var pours []models.CreatePourData 545 545 546 - for i := 0; i < maxPours; i++ { 546 + for i := range maxPours { 547 547 waterKey := "pour_water_" + strconv.Itoa(i) 548 548 timeKey := "pour_time_" + strconv.Itoa(i) 549 549
+1 -1
internal/handlers/entities.go
··· 186 186 // Link beans to roasters 187 187 atproto.LinkBeansToRoasters(beans, roasters) 188 188 189 - response := map[string]interface{}{ 189 + response := map[string]any{ 190 190 "did": userDID, 191 191 "beans": beans, 192 192 "roasters": roasters,
+2 -2
internal/handlers/handlers.go
··· 177 177 // decodeRequest decodes either JSON or form data into the target interface based on Content-Type. 178 178 // The parseForm function is called when the request is form-encoded (not JSON). 179 179 // Returns an error if parsing fails. 180 - func decodeRequest(r *http.Request, target interface{}, parseForm func() error) error { 180 + func decodeRequest(r *http.Request, target any, parseForm func() error) error { 181 181 if isJSONRequest(r) { 182 182 // Parse as JSON 183 183 if err := json.NewDecoder(r.Body).Decode(target); err != nil { ··· 208 208 } 209 209 210 210 // writeJSON encodes and writes a JSON response 211 - func writeJSON(w http.ResponseWriter, v interface{}, entityName string) { 211 + func writeJSON(w http.ResponseWriter, v any, entityName string) { 212 212 w.Header().Set("Content-Type", "application/json") 213 213 if err := json.NewEncoder(w).Encode(v); err != nil { 214 214 log.Error().Err(err).Msg("Failed to encode " + entityName + " response")
+3 -3
internal/handlers/recipe.go
··· 504 504 type parsedRecord struct { 505 505 uri string 506 506 did string 507 - data map[string]interface{} 507 + data map[string]any 508 508 recipe *models.Recipe 509 509 sourceRef string 510 510 sourceDID string ··· 512 512 } 513 513 parsed := make([]parsedRecord, 0, len(records)) 514 514 for i := range records { 515 - var recordData map[string]interface{} 515 + var recordData map[string]any 516 516 if err := json.Unmarshal(records[i].Record, &recordData); err != nil { 517 517 continue 518 518 } ··· 587 587 recipe.BrewerRKey = c.RKey 588 588 } 589 589 if brewerRec, ok := brewerRecords[brewerRef]; ok { 590 - var brewerData map[string]interface{} 590 + var brewerData map[string]any 591 591 if err := json.Unmarshal(brewerRec.Record, &brewerData); err == nil { 592 592 if brewer, err := atproto.RecordToBrewer(brewerData, brewerRef); err == nil { 593 593 recipe.BrewerObj = brewer
+2 -2
internal/handlers/testutil.go
··· 133 133 ) 134 134 135 135 // NewAuthenticatedRequest creates a request with authentication context 136 - func NewAuthenticatedRequest(method, path string, body interface{}) *http.Request { 136 + func NewAuthenticatedRequest(method, path string, body any) *http.Request { 137 137 req := httptest.NewRequest(method, path, nil) 138 138 139 139 // Add authenticated DID to context using the same keys as OAuth middleware ··· 150 150 151 151 // AssertResponseCode checks if the response has the expected status code 152 152 func AssertResponseCode(t interface { 153 - Errorf(format string, args ...interface{}) 153 + Errorf(format string, args ...any) 154 154 }, rec *httptest.ResponseRecorder, expected int) { 155 155 if rec.Code != expected { 156 156 t.Errorf("Expected status code %d, got %d. Body: %s", expected, rec.Code, rec.Body.String())
+2 -2
internal/middleware/logging.go
··· 19 19 // Check X-Forwarded-For header (can contain multiple IPs: client, proxy1, proxy2) 20 20 if xff := r.Header.Get("X-Forwarded-For"); xff != "" { 21 21 // Take the first IP (the original client) 22 - if idx := strings.Index(xff, ","); idx != -1 { 23 - return strings.TrimSpace(xff[:idx]) 22 + if before, _, ok := strings.Cut(xff, ","); ok { 23 + return strings.TrimSpace(before) 24 24 } 25 25 return strings.TrimSpace(xff) 26 26 }
+5 -5
internal/middleware/security_test.go
··· 65 65 66 66 wrapped := SecurityHeadersMiddleware(handler) 67 67 68 - for i := 0; i < 10; i++ { 68 + for range 10 { 69 69 req := httptest.NewRequest(http.MethodGet, "/", nil) 70 70 rec := httptest.NewRecorder() 71 71 wrapped.ServeHTTP(rec, req) ··· 145 145 wrapped := middleware(handler) 146 146 147 147 t.Run("auth endpoints use auth limiter", func(t *testing.T) { 148 - for i := 0; i < 2; i++ { 148 + for range 2 { 149 149 req := httptest.NewRequest(http.MethodPost, "/auth/login", nil) 150 150 req.RemoteAddr = "1.1.1.1:1234" 151 151 rec := httptest.NewRecorder() ··· 162 162 }) 163 163 164 164 t.Run("api endpoints use api limiter", func(t *testing.T) { 165 - for i := 0; i < 3; i++ { 165 + for range 3 { 166 166 req := httptest.NewRequest(http.MethodGet, "/api/brews", nil) 167 167 req.RemoteAddr = "2.2.2.2:1234" 168 168 rec := httptest.NewRecorder() ··· 178 178 }) 179 179 180 180 t.Run("other endpoints use global limiter", func(t *testing.T) { 181 - for i := 0; i < 5; i++ { 181 + for range 5 { 182 182 req := httptest.NewRequest(http.MethodGet, "/brews", nil) 183 183 req.RemoteAddr = "3.3.3.3:1234" 184 184 rec := httptest.NewRecorder() ··· 194 194 }) 195 195 196 196 t.Run("login path uses auth limiter", func(t *testing.T) { 197 - for i := 0; i < 2; i++ { 197 + for range 2 { 198 198 req := httptest.NewRequest(http.MethodPost, "/login", nil) 199 199 req.RemoteAddr = "4.4.4.4:1234" 200 200 rec := httptest.NewRecorder()
+3 -6
internal/moderation/models.go
··· 1 1 package moderation 2 2 3 + import "slices" 4 + 3 5 import "time" 4 6 5 7 // Permission represents a moderation action that can be performed ··· 47 49 48 50 // HasPermission checks if this role has the given permission 49 51 func (r *Role) HasPermission(perm Permission) bool { 50 - for _, p := range r.Permissions { 51 - if p == perm { 52 - return true 53 - } 54 - } 55 - return false 52 + return slices.Contains(r.Permissions, perm) 56 53 } 57 54 58 55 // ModeratorUser represents a user with moderation privileges
+68 -68
internal/suggestions/suggestions_test.go
··· 27 27 return idx 28 28 } 29 29 30 - func insertRecord(t *testing.T, idx *firehose.FeedIndex, did, collection, rkey string, fields map[string]interface{}) { 30 + func insertRecord(t *testing.T, idx *firehose.FeedIndex, did, collection, rkey string, fields map[string]any) { 31 31 t.Helper() 32 32 fields["$type"] = collection 33 33 fields["createdAt"] = time.Now().Format(time.RFC3339) ··· 88 88 func TestRoasterDedup_SameNameDifferentLocation(t *testing.T) { 89 89 idx := newTestFeedIndex(t) 90 90 91 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDRoaster, "r1", map[string]interface{}{ 91 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDRoaster, "r1", map[string]any{ 92 92 "name": "Stumptown Coffee", 93 93 "location": "Portland, OR", 94 94 }) 95 - insertRecord(t, idx, "did:plc:bob", atproto.NSIDRoaster, "r2", map[string]interface{}{ 95 + insertRecord(t, idx, "did:plc:bob", atproto.NSIDRoaster, "r2", map[string]any{ 96 96 "name": "Stumptown Coffee", 97 97 "location": "New York, NY", 98 98 }) 99 99 100 - results, err := Search(context.Background(), idx,atproto.NSIDRoaster, "stumptown", 10) 100 + results, err := Search(context.Background(), idx, atproto.NSIDRoaster, "stumptown", 10) 101 101 assert.NoError(t, err) 102 102 assert.Len(t, results, 2, "different locations should produce separate suggestions") 103 103 } ··· 106 106 idx := newTestFeedIndex(t) 107 107 108 108 // Same roaster, different name variations, same location 109 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDRoaster, "r1", map[string]interface{}{ 109 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDRoaster, "r1", map[string]any{ 110 110 "name": "Counter Culture Coffee", 111 111 "location": "Durham, NC", 112 112 "website": "https://counterculturecoffee.com", 113 113 }) 114 - insertRecord(t, idx, "did:plc:bob", atproto.NSIDRoaster, "r2", map[string]interface{}{ 114 + insertRecord(t, idx, "did:plc:bob", atproto.NSIDRoaster, "r2", map[string]any{ 115 115 "name": "Counter Culture Coffee Roasters", 116 116 "location": "Durham, NC", 117 117 }) 118 118 119 - results, err := Search(context.Background(), idx,atproto.NSIDRoaster, "counter", 10) 119 + results, err := Search(context.Background(), idx, atproto.NSIDRoaster, "counter", 10) 120 120 assert.NoError(t, err) 121 121 assert.Len(t, results, 1, "fuzzy name + same location should merge") 122 122 assert.Equal(t, 2, results[0].Count) ··· 126 126 idx := newTestFeedIndex(t) 127 127 128 128 // Both have no location — should merge on fuzzy name alone 129 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDRoaster, "r1", map[string]interface{}{ 129 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDRoaster, "r1", map[string]any{ 130 130 "name": "Blue Bottle Coffee", 131 131 }) 132 - insertRecord(t, idx, "did:plc:bob", atproto.NSIDRoaster, "r2", map[string]interface{}{ 132 + insertRecord(t, idx, "did:plc:bob", atproto.NSIDRoaster, "r2", map[string]any{ 133 133 "name": "Blue Bottle", 134 134 }) 135 135 136 - results, err := Search(context.Background(), idx,atproto.NSIDRoaster, "blue", 10) 136 + results, err := Search(context.Background(), idx, atproto.NSIDRoaster, "blue", 10) 137 137 assert.NoError(t, err) 138 138 assert.Len(t, results, 1, "same fuzzy name with no location should merge") 139 139 assert.Equal(t, 2, results[0].Count) ··· 144 144 func TestGrinderDedup_SameNameDifferentType(t *testing.T) { 145 145 idx := newTestFeedIndex(t) 146 146 147 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDGrinder, "g1", map[string]interface{}{ 147 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDGrinder, "g1", map[string]any{ 148 148 "name": "Baratza Encore", 149 149 "grinderType": "electric", 150 150 "burrType": "conical", 151 151 }) 152 - insertRecord(t, idx, "did:plc:bob", atproto.NSIDGrinder, "g2", map[string]interface{}{ 152 + insertRecord(t, idx, "did:plc:bob", atproto.NSIDGrinder, "g2", map[string]any{ 153 153 "name": "Baratza Encore", 154 154 "grinderType": "electric", 155 155 "burrType": "flat", 156 156 }) 157 157 158 - results, err := Search(context.Background(), idx,atproto.NSIDGrinder, "baratza", 10) 158 + results, err := Search(context.Background(), idx, atproto.NSIDGrinder, "baratza", 10) 159 159 assert.NoError(t, err) 160 160 assert.Len(t, results, 2, "different burr types should produce separate suggestions") 161 161 } ··· 163 163 func TestGrinderDedup_SameEverythingMerges(t *testing.T) { 164 164 idx := newTestFeedIndex(t) 165 165 166 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDGrinder, "g1", map[string]interface{}{ 166 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDGrinder, "g1", map[string]any{ 167 167 "name": "1Zpresso JX Pro", 168 168 "grinderType": "hand", 169 169 "burrType": "conical", 170 170 }) 171 - insertRecord(t, idx, "did:plc:bob", atproto.NSIDGrinder, "g2", map[string]interface{}{ 171 + insertRecord(t, idx, "did:plc:bob", atproto.NSIDGrinder, "g2", map[string]any{ 172 172 "name": "1Zpresso JX Pro", 173 173 "grinderType": "hand", 174 174 "burrType": "conical", 175 175 }) 176 176 177 - results, err := Search(context.Background(), idx,atproto.NSIDGrinder, "1zp", 10) 177 + results, err := Search(context.Background(), idx, atproto.NSIDGrinder, "1zp", 10) 178 178 assert.NoError(t, err) 179 179 assert.Len(t, results, 1) 180 180 assert.Equal(t, 2, results[0].Count) ··· 185 185 func TestBrewerDedup_SameNameDifferentType(t *testing.T) { 186 186 idx := newTestFeedIndex(t) 187 187 188 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDBrewer, "br1", map[string]interface{}{ 188 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDBrewer, "br1", map[string]any{ 189 189 "name": "Hario V60", 190 190 "brewerType": "pour-over", 191 191 }) 192 - insertRecord(t, idx, "did:plc:bob", atproto.NSIDBrewer, "br2", map[string]interface{}{ 192 + insertRecord(t, idx, "did:plc:bob", atproto.NSIDBrewer, "br2", map[string]any{ 193 193 "name": "Hario V60", 194 194 "brewerType": "dripper", 195 195 }) 196 196 197 - results, err := Search(context.Background(), idx,atproto.NSIDBrewer, "hario", 10) 197 + results, err := Search(context.Background(), idx, atproto.NSIDBrewer, "hario", 10) 198 198 assert.NoError(t, err) 199 199 assert.Len(t, results, 2, "different brewer types should produce separate suggestions") 200 200 } ··· 202 202 func TestBrewerDedup_SameNameSameTypeMerges(t *testing.T) { 203 203 idx := newTestFeedIndex(t) 204 204 205 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDBrewer, "br1", map[string]interface{}{ 205 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDBrewer, "br1", map[string]any{ 206 206 "name": "AeroPress", 207 207 "brewerType": "immersion", 208 208 }) 209 - insertRecord(t, idx, "did:plc:bob", atproto.NSIDBrewer, "br2", map[string]interface{}{ 209 + insertRecord(t, idx, "did:plc:bob", atproto.NSIDBrewer, "br2", map[string]any{ 210 210 "name": "AeroPress", 211 211 "brewerType": "immersion", 212 212 }) 213 213 214 - results, err := Search(context.Background(), idx,atproto.NSIDBrewer, "aero", 10) 214 + results, err := Search(context.Background(), idx, atproto.NSIDBrewer, "aero", 10) 215 215 assert.NoError(t, err) 216 216 assert.Len(t, results, 1) 217 217 assert.Equal(t, 2, results[0].Count) ··· 222 222 func TestBeanDedup_SameNameDifferentProcess(t *testing.T) { 223 223 idx := newTestFeedIndex(t) 224 224 225 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDBean, "b1", map[string]interface{}{ 225 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDBean, "b1", map[string]any{ 226 226 "name": "Yirgacheffe", 227 227 "origin": "Ethiopia", 228 228 "process": "Washed", 229 229 }) 230 - insertRecord(t, idx, "did:plc:bob", atproto.NSIDBean, "b2", map[string]interface{}{ 230 + insertRecord(t, idx, "did:plc:bob", atproto.NSIDBean, "b2", map[string]any{ 231 231 "name": "Yirgacheffe", 232 232 "origin": "Ethiopia", 233 233 "process": "Natural", 234 234 }) 235 235 236 - results, err := Search(context.Background(), idx,atproto.NSIDBean, "yirga", 10) 236 + results, err := Search(context.Background(), idx, atproto.NSIDBean, "yirga", 10) 237 237 assert.NoError(t, err) 238 238 assert.Len(t, results, 2, "different processes should produce separate suggestions") 239 239 } ··· 241 241 func TestBeanDedup_SameNameDifferentOrigin(t *testing.T) { 242 242 idx := newTestFeedIndex(t) 243 243 244 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDBean, "b1", map[string]interface{}{ 244 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDBean, "b1", map[string]any{ 245 245 "name": "Gesha", 246 246 "origin": "Panama", 247 247 }) 248 - insertRecord(t, idx, "did:plc:bob", atproto.NSIDBean, "b2", map[string]interface{}{ 248 + insertRecord(t, idx, "did:plc:bob", atproto.NSIDBean, "b2", map[string]any{ 249 249 "name": "Gesha", 250 250 "origin": "Ethiopia", 251 251 }) 252 252 253 - results, err := Search(context.Background(), idx,atproto.NSIDBean, "gesha", 10) 253 + results, err := Search(context.Background(), idx, atproto.NSIDBean, "gesha", 10) 254 254 assert.NoError(t, err) 255 255 assert.Len(t, results, 2, "different origins should produce separate suggestions") 256 256 } ··· 258 258 func TestBeanDedup_SameEverythingMerges(t *testing.T) { 259 259 idx := newTestFeedIndex(t) 260 260 261 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDBean, "b1", map[string]interface{}{ 261 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDBean, "b1", map[string]any{ 262 262 "name": "Ethiopian Yirgacheffe", 263 263 "origin": "Ethiopia", 264 264 "roastLevel": "Light", 265 265 "process": "Washed", 266 266 }) 267 - insertRecord(t, idx, "did:plc:bob", atproto.NSIDBean, "b2", map[string]interface{}{ 267 + insertRecord(t, idx, "did:plc:bob", atproto.NSIDBean, "b2", map[string]any{ 268 268 "name": "Ethiopian Yirgacheffe", 269 269 "origin": "Ethiopia", 270 270 "process": "Washed", 271 271 }) 272 272 273 - results, err := Search(context.Background(), idx,atproto.NSIDBean, "ethiopia", 10) 273 + results, err := Search(context.Background(), idx, atproto.NSIDBean, "ethiopia", 10) 274 274 assert.NoError(t, err) 275 275 assert.Len(t, results, 1) 276 276 assert.Equal(t, 2, results[0].Count) ··· 281 281 func TestSearch_PrefixMatch(t *testing.T) { 282 282 idx := newTestFeedIndex(t) 283 283 284 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDRoaster, "r1", map[string]interface{}{ 284 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDRoaster, "r1", map[string]any{ 285 285 "name": "Black & White Coffee", 286 286 "location": "Raleigh, NC", 287 287 }) 288 - insertRecord(t, idx, "did:plc:bob", atproto.NSIDRoaster, "r2", map[string]interface{}{ 288 + insertRecord(t, idx, "did:plc:bob", atproto.NSIDRoaster, "r2", map[string]any{ 289 289 "name": "Blue Bottle", 290 290 "location": "Oakland, CA", 291 291 }) 292 292 293 - results, err := Search(context.Background(), idx,atproto.NSIDRoaster, "bl", 10) 293 + results, err := Search(context.Background(), idx, atproto.NSIDRoaster, "bl", 10) 294 294 assert.NoError(t, err) 295 295 assert.Len(t, results, 2) 296 296 assert.Equal(t, "Black & White Coffee", results[0].Name) ··· 300 300 func TestSearch_CaseInsensitive(t *testing.T) { 301 301 idx := newTestFeedIndex(t) 302 302 303 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDRoaster, "r1", map[string]interface{}{ 303 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDRoaster, "r1", map[string]any{ 304 304 "name": "Stumptown Coffee", 305 305 }) 306 306 307 - results, err := Search(context.Background(), idx,atproto.NSIDRoaster, "STUMP", 10) 307 + results, err := Search(context.Background(), idx, atproto.NSIDRoaster, "STUMP", 10) 308 308 assert.NoError(t, err) 309 309 assert.Len(t, results, 1) 310 310 assert.Equal(t, "Stumptown Coffee", results[0].Name) ··· 313 313 func TestSearch_SubstringMatch(t *testing.T) { 314 314 idx := newTestFeedIndex(t) 315 315 316 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDRoaster, "r1", map[string]interface{}{ 316 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDRoaster, "r1", map[string]any{ 317 317 "name": "Red Rooster Coffee", 318 318 "location": "Floyd, VA", 319 319 }) 320 320 321 - results, err := Search(context.Background(), idx,atproto.NSIDRoaster, "floyd", 10) 321 + results, err := Search(context.Background(), idx, atproto.NSIDRoaster, "floyd", 10) 322 322 assert.NoError(t, err) 323 323 assert.Len(t, results, 1) 324 324 assert.Equal(t, "Red Rooster Coffee", results[0].Name) ··· 328 328 idx := newTestFeedIndex(t) 329 329 330 330 // Two users have the same roaster, same location (one with website, one without) 331 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDRoaster, "r1", map[string]interface{}{ 331 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDRoaster, "r1", map[string]any{ 332 332 "name": "Counter Culture Coffee", 333 333 "location": "Durham, NC", 334 334 "website": "https://counterculturecoffee.com", 335 335 }) 336 - insertRecord(t, idx, "did:plc:bob", atproto.NSIDRoaster, "r2", map[string]interface{}{ 336 + insertRecord(t, idx, "did:plc:bob", atproto.NSIDRoaster, "r2", map[string]any{ 337 337 "name": "Counter Culture", 338 338 "location": "Durham, NC", 339 339 }) 340 340 341 - results, err := Search(context.Background(), idx,atproto.NSIDRoaster, "counter", 10) 341 + results, err := Search(context.Background(), idx, atproto.NSIDRoaster, "counter", 10) 342 342 assert.NoError(t, err) 343 343 assert.Len(t, results, 1) 344 344 assert.Equal(t, 2, results[0].Count) ··· 348 348 func TestSearch_Limit(t *testing.T) { 349 349 idx := newTestFeedIndex(t) 350 350 351 - for i := 0; i < 5; i++ { 351 + for i := range 5 { 352 352 rkey := "r" + string(rune('0'+i)) 353 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDGrinder, rkey, map[string]interface{}{ 353 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDGrinder, rkey, map[string]any{ 354 354 "name": "Grinder " + string(rune('A'+i)), 355 355 "grinderType": "hand", 356 356 }) 357 357 } 358 358 359 - results, err := Search(context.Background(), idx,atproto.NSIDGrinder, "grinder", 3) 359 + results, err := Search(context.Background(), idx, atproto.NSIDGrinder, "grinder", 3) 360 360 assert.NoError(t, err) 361 361 assert.Len(t, results, 3) 362 362 } ··· 364 364 func TestSearch_ShortQuery(t *testing.T) { 365 365 idx := newTestFeedIndex(t) 366 366 367 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDRoaster, "r1", map[string]interface{}{ 367 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDRoaster, "r1", map[string]any{ 368 368 "name": "ABC", 369 369 }) 370 370 371 - results, err := Search(context.Background(), idx,atproto.NSIDRoaster, "a", 10) 371 + results, err := Search(context.Background(), idx, atproto.NSIDRoaster, "a", 10) 372 372 assert.NoError(t, err) 373 373 assert.Empty(t, results) 374 374 375 - results, err = Search(context.Background(), idx,atproto.NSIDRoaster, "ab", 10) 375 + results, err = Search(context.Background(), idx, atproto.NSIDRoaster, "ab", 10) 376 376 assert.NoError(t, err) 377 377 assert.Len(t, results, 1) 378 378 } ··· 380 380 func TestSearch_EmptyQuery(t *testing.T) { 381 381 idx := newTestFeedIndex(t) 382 382 383 - results, err := Search(context.Background(), idx,atproto.NSIDRoaster, "", 10) 383 + results, err := Search(context.Background(), idx, atproto.NSIDRoaster, "", 10) 384 384 assert.NoError(t, err) 385 385 assert.Empty(t, results) 386 386 } ··· 388 388 func TestSearch_UnknownCollection(t *testing.T) { 389 389 idx := newTestFeedIndex(t) 390 390 391 - results, err := Search(context.Background(), idx,"unknown.collection", "test", 10) 391 + results, err := Search(context.Background(), idx, "unknown.collection", "test", 10) 392 392 assert.NoError(t, err) 393 393 assert.Empty(t, results) 394 394 } ··· 396 396 func TestSearch_GrinderFields(t *testing.T) { 397 397 idx := newTestFeedIndex(t) 398 398 399 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDGrinder, "g1", map[string]interface{}{ 399 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDGrinder, "g1", map[string]any{ 400 400 "name": "1Zpresso JX Pro", 401 401 "grinderType": "hand", 402 402 "burrType": "conical", 403 403 }) 404 404 405 - results, err := Search(context.Background(), idx,atproto.NSIDGrinder, "1zp", 10) 405 + results, err := Search(context.Background(), idx, atproto.NSIDGrinder, "1zp", 10) 406 406 assert.NoError(t, err) 407 407 assert.Len(t, results, 1) 408 408 assert.Equal(t, "hand", results[0].Fields["grinderType"]) ··· 412 412 func TestSearch_BeanFields(t *testing.T) { 413 413 idx := newTestFeedIndex(t) 414 414 415 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDBean, "b1", map[string]interface{}{ 415 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDBean, "b1", map[string]any{ 416 416 "name": "Ethiopian Yirgacheffe", 417 417 "origin": "Ethiopia", 418 418 "roastLevel": "Light", 419 419 "process": "Washed", 420 420 }) 421 421 422 - results, err := Search(context.Background(), idx,atproto.NSIDBean, "ethiopia", 10) 422 + results, err := Search(context.Background(), idx, atproto.NSIDBean, "ethiopia", 10) 423 423 assert.NoError(t, err) 424 424 assert.Len(t, results, 1) 425 425 assert.Equal(t, "Ethiopian Yirgacheffe", results[0].Name) ··· 429 429 func TestSearch_BrewerFields(t *testing.T) { 430 430 idx := newTestFeedIndex(t) 431 431 432 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDBrewer, "br1", map[string]interface{}{ 432 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDBrewer, "br1", map[string]any{ 433 433 "name": "Hario V60", 434 434 "brewerType": "Pour-Over", 435 435 }) 436 436 437 - results, err := Search(context.Background(), idx,atproto.NSIDBrewer, "hario", 10) 437 + results, err := Search(context.Background(), idx, atproto.NSIDBrewer, "hario", 10) 438 438 assert.NoError(t, err) 439 439 assert.Len(t, results, 1) 440 440 assert.Equal(t, "Pour-Over", results[0].Fields["brewerType"]) ··· 445 445 func TestRecipeDedup_SameNameDifferentBrewerType(t *testing.T) { 446 446 idx := newTestFeedIndex(t) 447 447 448 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDRecipe, "rec1", map[string]interface{}{ 448 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDRecipe, "rec1", map[string]any{ 449 449 "name": "V60 Standard", 450 450 "brewerType": "pourover", 451 451 }) 452 - insertRecord(t, idx, "did:plc:bob", atproto.NSIDRecipe, "rec2", map[string]interface{}{ 452 + insertRecord(t, idx, "did:plc:bob", atproto.NSIDRecipe, "rec2", map[string]any{ 453 453 "name": "V60 Standard", 454 454 "brewerType": "immersion", 455 455 }) 456 456 457 - results, err := Search(context.Background(), idx,atproto.NSIDRecipe, "v60", 10) 457 + results, err := Search(context.Background(), idx, atproto.NSIDRecipe, "v60", 10) 458 458 assert.NoError(t, err) 459 459 assert.Len(t, results, 2, "different brewer types should produce separate suggestions") 460 460 } ··· 462 462 func TestRecipeDedup_SameNameSameTypeMerges(t *testing.T) { 463 463 idx := newTestFeedIndex(t) 464 464 465 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDRecipe, "rec1", map[string]interface{}{ 465 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDRecipe, "rec1", map[string]any{ 466 466 "name": "AeroPress Standard", 467 467 "brewerType": "immersion", 468 468 }) 469 - insertRecord(t, idx, "did:plc:bob", atproto.NSIDRecipe, "rec2", map[string]interface{}{ 469 + insertRecord(t, idx, "did:plc:bob", atproto.NSIDRecipe, "rec2", map[string]any{ 470 470 "name": "AeroPress Standard", 471 471 "brewerType": "immersion", 472 472 }) 473 473 474 - results, err := Search(context.Background(), idx,atproto.NSIDRecipe, "aero", 10) 474 + results, err := Search(context.Background(), idx, atproto.NSIDRecipe, "aero", 10) 475 475 assert.NoError(t, err) 476 476 assert.Len(t, results, 1) 477 477 assert.Equal(t, 2, results[0].Count) ··· 480 480 func TestSearch_RecipeFields(t *testing.T) { 481 481 idx := newTestFeedIndex(t) 482 482 483 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDRecipe, "rec1", map[string]interface{}{ 483 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDRecipe, "rec1", map[string]any{ 484 484 "name": "James Hoffmann V60", 485 485 "brewerType": "pourover", 486 486 }) 487 487 488 - results, err := Search(context.Background(), idx,atproto.NSIDRecipe, "hoffmann", 10) 488 + results, err := Search(context.Background(), idx, atproto.NSIDRecipe, "hoffmann", 10) 489 489 assert.NoError(t, err) 490 490 assert.Len(t, results, 1) 491 491 assert.Equal(t, "James Hoffmann V60", results[0].Name) ··· 496 496 idx := newTestFeedIndex(t) 497 497 498 498 // "Alpha Roasters" used by 3 people (same location so they merge) 499 - insertRecord(t, idx, "did:plc:alice", atproto.NSIDRoaster, "r1", map[string]interface{}{"name": "Alpha Roasters"}) 500 - insertRecord(t, idx, "did:plc:bob", atproto.NSIDRoaster, "r2", map[string]interface{}{"name": "Alpha Roasters"}) 501 - insertRecord(t, idx, "did:plc:charlie", atproto.NSIDRoaster, "r3", map[string]interface{}{"name": "Alpha Roasters"}) 499 + insertRecord(t, idx, "did:plc:alice", atproto.NSIDRoaster, "r1", map[string]any{"name": "Alpha Roasters"}) 500 + insertRecord(t, idx, "did:plc:bob", atproto.NSIDRoaster, "r2", map[string]any{"name": "Alpha Roasters"}) 501 + insertRecord(t, idx, "did:plc:charlie", atproto.NSIDRoaster, "r3", map[string]any{"name": "Alpha Roasters"}) 502 502 503 503 // "Alpha Beta" used by 1 person 504 - insertRecord(t, idx, "did:plc:dave", atproto.NSIDRoaster, "r4", map[string]interface{}{"name": "Alpha Beta"}) 504 + insertRecord(t, idx, "did:plc:dave", atproto.NSIDRoaster, "r4", map[string]any{"name": "Alpha Beta"}) 505 505 506 - results, err := Search(context.Background(), idx,atproto.NSIDRoaster, "alpha", 10) 506 + results, err := Search(context.Background(), idx, atproto.NSIDRoaster, "alpha", 10) 507 507 assert.NoError(t, err) 508 508 assert.Len(t, results, 2) 509 509 assert.Equal(t, "Alpha Roasters", results[0].Name)