From 538c659ba56e899ea59e6504385f6d14984e446a Mon Sep 17 00:00:00 2001 From: Siddhartha Basu Date: Tue, 21 Apr 2026 06:46:54 -0500 Subject: [PATCH 01/17] docs: remove Go coding conventions documentation The project-specific coding guidelines are being removed from the repository to avoid duplication or potential drift from standard Go practices and to simplify the documentation maintenance. --- CLAUDE.md | 973 ------------------------------------------------------ 1 file changed, 973 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 534bab9f..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,973 +0,0 @@ -# Go Coding Conventions - --- **Build, Test, and Lint Commands** - - Run all tests: `gotestsum --format-hide-empty-pkg --format dots` - - Run specific test: `gotestsum --format-hide-empty-pkg --format dots -- -run TestFindSimilar ./...` - - Run tests with verbose output: `gotestum --format-hide-empty-pkg --format standard-verbose` - - Format code: `gofumpt -w .` - - Lint codebase: `golangci-lint run` - -- **Code Style Guidelines** - - Imports: Standard library first, then external packages, then internal packages - - Prefer functional programming utilities from collection package where appropriate - - Use `slices.DeleteFunc` for conditional element removal instead of manual for loops - ```go - import "slices" - - // Avoid: Manual loop for conditional removal - var result []Item - for _, item := range items { - if !shouldRemove(item) { - result = append(result, item) - } - } - items = result - - // Preferred: Use slices.DeleteFunc - items = slices.DeleteFunc(items, shouldRemove) - - // Example: Remove users with specific role - users = slices.DeleteFunc(users, func(u User) bool { - return u.Role == "banned" - }) - - // Example: Remove tags matching both name and value - tags = slices.DeleteFunc(tags, func(tag TagProperty) bool { - return tag.Name == targetName && tag.Value == targetValue - }) - ``` - - Essential utility functions: - ```go - func Map[T, U any](ts []T, f func(T) U) []U { - us := make([]U, len(ts)) - for i, t := range ts { - us[i] = f(t) - } - return us - } - - func Filter[T any](slice []T, predicate func(T) bool) []T { - var result []T - for _, item := range slice { - if predicate(item) { - result = append(result, item) - } - } - return result - } - - func Find[T any](slice []T, predicate func(T) bool) (*T, bool) { - for i := range slice { - if predicate(slice[i]) { - return &slice[i], true - } - } - return nil, false - } - - func MapWithError[T, U any](ts []T, f func(T) (U, error)) ([]U, error) { - us := make([]U, len(ts)) - for i, t := range ts { - u, err := f(t) - if err != nil { - return nil, err - } - us[i] = u - } - return us, nil - } - ``` - - Usage examples: - ```go - // Helper functions for transformations - func stringToInt(s string) int { - n, _ := strconv.Atoi(s) - return n - } - - func isEven(n int) bool { - return n%2 == 0 - } - - func isBob(u string) bool { - return u == "bob" - } - - // Transform slice elements - strings := []string{"1", "2", "3"} - numbers := Map(strings, stringToInt) - - // Filter slice elements - numbers := []int{1, 2, 3, 4, 5} - evens := Filter(numbers, isEven) - - // Find first matching element - users := []string{"alice", "bob", "charlie"} - user, found := Find(users, isBob) - - // For complex operations with error handling - func (c *Client) processArticleWithError(pmid string) (*Article, error) { - article, err := c.GetArticle(pmid) - if err != nil { - return nil, fmt.Errorf("failed to process article %s: %w", pmid, err) - } - return article, nil - } - - // Usage with MapWithError - pmids := []string{"12345", "67890", "11111"} - articles, err := MapWithError(pmids, c.processArticleWithError) - ``` - - **Functional Programming with fp-go Library** - - Use `github.com/IBM/fp-go` for monadic patterns, pipes, and functional composition - - Import pattern: Use single-letter aliases for fp-go modules - ```go - import ( - A "github.com/IBM/fp-go/array" - E "github.com/IBM/fp-go/either" - F "github.com/IBM/fp-go/function" - O "github.com/IBM/fp-go/option" - T "github.com/IBM/fp-go/tuple" - S "github.com/IBM/fp-go/string" - J "github.com/IBM/fp-go/json" - IOE "github.com/IBM/fp-go/ioeither" - H "github.com/IBM/fp-go/context/readerioeither/http" - "github.com/IBM/fp-go/ioeither/file" - ) - ``` - - - **Either Monad for Error Handling** - ```go - // Use Either for operations that can fail - func validateAndProcess(input string) E.Either[error, ProcessedData] { - if input == "" { - return E.Left[ProcessedData](fmt.Errorf("input cannot be empty")) - } - - processed := ProcessedData{Value: strings.ToUpper(input)} - return E.Right[error](processed) - } - - // Chain Either operations with Map and Bind - func processWorkflow(input string) E.Either[error, string] { - return F.Pipe3( - input, - validateAndProcess, - E.Map[error](func(data ProcessedData) ProcessedData { - data.Timestamp = time.Now() - return data - }), - E.Map[error](func(data ProcessedData) string { - return fmt.Sprintf("%s at %v", data.Value, data.Timestamp) - }), - ) - } - - // Handle Either results with Fold - result := F.Pipe1( - processWorkflow("hello"), - E.Fold[error, string]( - func(err error) string { - return fmt.Sprintf("Error: %v", err) - }, - func(success string) string { - return success - }, - ), - ) - ``` - - - **Option Monad for Nullable Values** - ```go - // Use Option instead of pointers for optional values - func findUser(id string) O.Option[User] { - user, found := userDatabase[id] - if found { - return O.Some(user) - } - return O.None[User]() - } - - // Chain Option operations - func getUserEmail(id string) O.Option[string] { - return F.Pipe1( - findUser(id), - O.Map(func(user User) string { - return user.Email - }), - ) - } - - // Option with fallback using Alt - func getConfigValue(key string) O.Option[string] { - return F.Pipe2( - getFromEnv(key), - O.Alt(func() O.Option[string] { - return getFromFile(key) - }), - O.Alt(func() O.Option[string] { - return getDefaultValue(key) - }), - ) - } - - // Extract Option values safely - email := F.Pipe1( - getUserEmail("user123"), - O.GetOrElse(F.Constant("no-email@example.com")), - ) - ``` - - - **Function Composition with Pipe and Flow** - ```go - // Use F.Pipe for sequential composition (left to right) - func processFileName(fileName string) string { - return F.Pipe7( - fileName, - filepath.Base, - strings.Split("."), - A.Head[string], - O.GetOrElse(F.Constant("")), - strings.Split("_"), - A.SliceRight[string](2), - S.Join(":"), - ) - } - - // Use F.Flow for creating reusable composed functions - var processUserData = F.Flow3( - validateInput, - normalizeData, - saveToDatabase, - ) - - // Complex pipeline with error handling - func processFiles(files []string) E.Either[error, []ProcessedFile] { - return F.Pipe2( - E.TryCatchError(readFiles(files)), - E.Map[error](func(fileContents []string) []ProcessedFile { - return F.Pipe3( - fileContents, - A.Filter(isValidContent), - A.Map(parseContent), - A.FilterMap(validateParsedContent), - ) - }), - E.Fold[error, []ProcessedFile]( - func(err error) E.Either[error, []ProcessedFile] { - return E.Left[[]ProcessedFile](err) - }, - func(result []ProcessedFile) E.Either[error, []ProcessedFile] { - return E.Right[error](result) - }, - ), - ) - } - ``` - - - **Curried Functions for Reusability** - ```go - // Create curried functions for partial application - var HasField = F.Curry2(func(name string, field FieldType) bool { - return field.Name == name - }) - - var SearchUser = F.Curry2(func(email string, workspace WorkspaceResp) int { - return F.Pipe3( - workspace.Users, - A.FindFirst(HasUser(email)), - O.Map(func(user WorkspaceUserResp) int { return user.Id }), - O.GetOrElse(F.Constant(0)), - ) - }) - - // Usage of curried functions - hasEmailField := HasField("email") - if hasEmailField(userField) { - // process email field - } - - searchInWorkspace := SearchUser("user@example.com") - userID := searchInWorkspace(workspace) - - // Curry custom functions - var assignedByIdHandler = F.Curry2( - func(aid int, loader *DataLoader) *DataLoader { - if aid != 0 { - loader.Payload.AssignedBy = []AssignedBy{{Id: aid}} - } - return loader - }) - ``` - - - **Array/Slice Operations** - ```go - // Use fp-go array functions for slice operations - func processUserList(users []User) []string { - return F.Pipe3( - users, - A.Filter(func(u User) bool { return u.Active }), - A.Map(func(u User) string { return u.Email }), - A.FilterMap(func(email string) O.Option[string] { - if isValidEmail(email) { - return O.Some(email) - } - return O.None[string]() - }), - ) - } - - // Find operations with Option return - func findFirstAdmin(users []User) O.Option[User] { - return F.Pipe1( - users, - A.FindFirst(func(u User) bool { return u.Role == "admin" }), - ) - } - - // Complex array transformations - func extractTextFromCells(cells []Cell, startCol, endCol int) string { - return F.Pipe3( - createCellRange(startCol, endCol, len(cells)), - A.FilterMap(extractTextFromCellAtIndex(cells)), - A.Head[string], - O.GetOrElse(F.Constant("")), - ) - } - ``` - - - **Error Handling Patterns** - ```go - // Combine Either with TryCatchError for exception handling - func safeOperation(input string) E.Either[error, Result] { - return F.Pipe2( - E.TryCatchError(riskyOperation(input)), - E.Map[error](func(rawResult RawResult) Result { - return processResult(rawResult) - }), - E.MapLeft[Result](func(err error) error { - return fmt.Errorf("operation failed: %w", err) - }), - ) - } - - // Chain multiple Either operations - func processWorkflowWithValidation(input Input) E.Either[error, Output] { - return F.Pipe4( - E.Right[error](input), - E.Bind(validateInput), - E.Bind(processData), - E.Bind(formatOutput), - E.MapLeft[Output](func(err error) error { - return fmt.Errorf("workflow failed: %w", err) - }), - ) - } - ``` - - - **IOEither for I/O and Side Effects** - ```go - import ( - IOE "github.com/IBM/fp-go/ioeither" - "github.com/IBM/fp-go/ioeither/file" - ) - - // IOEither represents a computation that performs I/O and may fail - // It's a function type: func() Either[error, T] - // Use IOEither for file I/O, network calls, and other side effects - - // Convert Go functions returning (T, error) to IOEither using Eitherize - // Eitherize1 for functions with 1 parameter: func(A) (B, error) - // Eitherize2 for functions with 2 parameters: func(A, B) (C, error) - - // Example: Converting csv.Reader.ReadAll to IOEither - func csvReadAll(f *os.File) ([][]string, error) { - defer f.Close() - r := csv.NewReader(f) - records, err := r.ReadAll() - if err != nil { - return nil, fmt.Errorf("failed to read CSV data: %w", err) - } - return records, nil - } - - // Convert to IOEither using point-free style with Eitherize1 - var readCsvRecords = IOE.Eitherize1(csvReadAll) - - // Execute IOEither to get Either result - func ToEither[A any](ioe IOE.IOEither[error, A]) E.Either[error, A] { - return ioe() - } - - // File I/O with IOEither and functional composition - func readCsvFile(filePath string) E.Either[error, [][]string] { - return F.Pipe2( - file.Open(filePath), // IOEither[error, *os.File] - IOE.Chain(readCsvRecords), // Chain another IOEither operation - ToEither, // Execute to get Either result - ) - } - - // Curried functions with IOEither for CLI handlers - var processFileAction = F.Curry2( - func(config *Config, fileName string) error { - return F.Pipe3( - fileName, - createParams(config), // String -> Either[error, Params] - E.Chain(readCsvFile), // Either -> Either chaining - E.Fold( // Handle Either result - func(err error) error { - return fmt.Errorf("processing failed: %w", err) - }, - processRecords, // Success handler - ), - ) - }, - ) - - // Complex I/O pipeline with validation and processing - func processDataFile(params FileParams) E.Either[error, ProcessedData] { - return F.Pipe4( - E.Right[error](params), - validateParams, // Either[error, Params] - E.Chain(func(p FileParams) E.Either[error, [][]string] { - return F.Pipe2( - file.Open(p.FilePath), // IOEither for file open - IOE.Chain(readAndParseFile), // Chain I/O operations - ToEither, // Execute IOEither - ) - }), - E.Map[error](transformRecords), // Transform data - E.Map[error](aggregateResults), // Further processing - ) - } - - // Point-free composition with IOEither - var readConfigFile = F.Flow2( - file.Open, // string -> IOEither[error, *os.File] - IOE.Chain(IOE.Eitherize1(parseConfig)), // Chain parsing operation - ) - - // Multiple IOEither operations in sequence - func setupApplication() E.Either[error, App] { - return F.Pipe5( - file.Open("config.json"), - IOE.Chain(IOE.Eitherize1(readConfig)), - IOE.Chain(func(cfg Config) IOE.IOEither[error, Database] { - return IOE.Eitherize1(connectDatabase)(cfg.DBUrl) - }), - IOE.Map[error](func(db Database) App { - return App{DB: db} - }), - ToEither, - ) - } - - // Error handling with IOEither and MapLeft - func safeFileRead(path string) E.Either[error, string] { - return F.Pipe3( - file.Open(path), - IOE.Chain(IOE.Eitherize1(io.ReadAll)), - IOE.MapLeft[[]byte](func(err error) error { - return fmt.Errorf("failed to read file %s: %w", path, err) - }), - ToEither, - ) - } - ``` - - - **Tuple Operations** - ```go - // Use tuples for functions returning multiple values - func extractTextWithIndex(cells []Cell, index int) O.Option[T.Tuple2[string, int]] { - return F.Pipe1( - extractTextFromSingleCell(cells[index]), - O.Map(func(text string) T.Tuple2[string, int] { - return T.MakeTuple2(text, index) - }), - ) - } - - // Extract tuple values - result := extractTextWithIndex(cells, 0) - text, index := F.Pipe1( - result, - O.GetOrElse(F.Constant(T.MakeTuple2("", -1))), - ).F1, result.F2 - ``` - - Use options pattern for configurable components - ```go - // Option type for functional options - type Option func(*Config) - - // Config struct holds the configuration - type Config struct { - timeout time.Duration - retries int - debug bool - maxConnections int - } - - // Option functions - func WithTimeout(timeout time.Duration) Option { - return func(c *Config) { - c.timeout = timeout - } - } - - func WithRetries(retries int) Option { - return func(c *Config) { - c.retries = retries - } - } - - func WithDebug(debug bool) Option { - return func(c *Config) { - c.debug = debug - } - } - - func WithMaxConnections(max int) Option { - return func(c *Config) { - c.maxConnections = max - } - } - - // Constructor with default values and options - func NewClient(opts ...Option) *Client { - cfg := &Config{ - timeout: 30 * time.Second, - retries: 3, - debug: false, - maxConnections: 10, - } - - for _, opt := range opts { - opt(cfg) - } - - return &Client{config: cfg} - } - - // Usage examples - client1 := NewClient() // Uses all defaults - - client2 := NewClient( - WithTimeout(60*time.Second), - WithRetries(5), - WithDebug(true), - ) - - client3 := NewClient(WithMaxConnections(20)) - ``` - - Document all exported functions, types, and constants with proper Go doc comments - - Test coverage should be comprehensive with both unit and integration tests - - Use `github.com/stretchr/testify/require` for all unit test assertions - ```go - import ( - "testing" - "github.com/stretchr/testify/require" - ) - - // Basic assertions - func TestBasicAssertions(t *testing.T) { - value := "hello world" - number := 42 - result := []string{"a", "b", "c"} - - // String assertions - require.Equal(t, "hello world", value) - require.NotEqual(t, "goodbye", value) - require.Contains(t, value, "world") - require.NotEmpty(t, value) - - // Numeric assertions - require.Equal(t, 42, number) - require.Greater(t, number, 40) - require.GreaterOrEqual(t, number, 42) - require.Less(t, number, 50) - require.LessOrEqual(t, number, 42) - - // Collection assertions - require.Len(t, result, 3) - require.Contains(t, result, "b") - require.NotContains(t, result, "d") - require.ElementsMatch(t, []string{"c", "a", "b"}, result) // Order independent - } - - // Error handling tests - func TestErrorHandling(t *testing.T) { - // Function that should return error - _, err := someFunction("invalid input") - require.Error(t, err) - require.ErrorContains(t, err, "invalid input") - - // Function that should succeed - result, err := someFunction("valid input") - require.NoError(t, err) - require.NotNil(t, result) - } - - // Nil/Not Nil assertions - func TestNilAssertions(t *testing.T) { - var ptr *string - value := "test" - - require.Nil(t, ptr) - require.NotNil(t, &value) - } - - // Boolean assertions - func TestBooleanAssertions(t *testing.T) { - active := true - disabled := false - - require.True(t, active) - require.False(t, disabled) - } - - // Type assertions - func TestTypeAssertions(t *testing.T) { - var value interface{} = "hello" - - require.IsType(t, "", value) - require.Implements(t, (*io.Reader)(nil), &bytes.Buffer{}) - } - - // Testing with subtests - func TestUserValidation(t *testing.T) { - tests := []struct { - name string - input User - expected error - }{ - { - name: "valid user", - input: User{Name: "John", Email: "john@example.com"}, - expected: nil, - }, - { - name: "missing name", - input: User{Email: "john@example.com"}, - expected: ErrMissingName, - }, - { - name: "invalid email", - input: User{Name: "John", Email: "invalid"}, - expected: ErrInvalidEmail, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := ValidateUser(tt.input) - if tt.expected == nil { - require.NoError(t, err) - } else { - require.ErrorIs(t, err, tt.expected) - } - }) - } - } - - // Testing collections and maps - func TestCollections(t *testing.T) { - users := []User{ - {ID: 1, Name: "Alice"}, - {ID: 2, Name: "Bob"}, - } - userMap := map[string]int{ - "alice": 1, - "bob": 2, - } - - // Slice assertions - require.Len(t, users, 2) - require.Equal(t, "Alice", users[0].Name) - - // Map assertions - require.Len(t, userMap, 2) - require.Equal(t, 1, userMap["alice"]) - require.Contains(t, userMap, "bob") - require.NotContains(t, userMap, "charlie") - } - - // Testing with require.Eventually for async operations - func TestAsyncOperation(t *testing.T) { - service := NewAsyncService() - service.Start() - - // Wait up to 5 seconds for condition to be true - require.Eventually(t, func() bool { - return service.IsReady() - }, 5*time.Second, 100*time.Millisecond) - - require.True(t, service.IsReady()) - } - - // Testing with custom error messages - func TestWithCustomMessages(t *testing.T) { - result := calculateSum([]int{1, 2, 3}) - require.Equal(t, 6, result, "Sum calculation should equal 6") - - user := findUser("unknown") - require.Nil(t, user, "User should not be found for unknown ID") - } - - // Testing HTTP handlers - func TestHTTPHandler(t *testing.T) { - req := httptest.NewRequest("GET", "/users/123", nil) - recorder := httptest.NewRecorder() - - handler := NewUserHandler() - handler.ServeHTTP(recorder, req) - - require.Equal(t, http.StatusOK, recorder.Code) - require.Equal(t, "application/json", recorder.Header().Get("Content-Type")) - require.Contains(t, recorder.Body.String(), "user_id") - } - - // Setup and teardown patterns - func TestWithSetup(t *testing.T) { - // Setup - db := setupTestDatabase(t) - defer cleanupDatabase(db) - - user := &User{Name: "Test User", Email: "test@example.com"} - err := db.CreateUser(user) - require.NoError(t, err) - require.NotZero(t, user.ID) - - // Verify user was created - retrievedUser, err := db.GetUser(user.ID) - require.NoError(t, err) - require.Equal(t, user.Name, retrievedUser.Name) - require.Equal(t, user.Email, retrievedUser.Email) - } - - // Helper function for test setup - func setupTestDatabase(t *testing.T) *Database { - db, err := NewDatabase("test_connection_string") - require.NoError(t, err, "Failed to create test database") - return db - } - - func cleanupDatabase(db *Database) { - if db != nil { - db.Close() - } - } - ``` - - Use `github.com/go-playground/validator/v10` for struct field and parameter validation. Consult the library API documentation using context7 MCP when needed for specific validation rules and usage patterns. - - Any function or method receiving more than three parameters should use a type struct - ```go - // Avoid: Too many parameters - func CreateUser(name, email, phone, address string, age int, active bool) error { - // implementation - } - - // Preferred: Use a struct for parameters - type CreateUserParams struct { - Name string - Email string - Phone string - Address string - Age int - Active bool - } - - func CreateUser(params CreateUserParams) error { - // implementation - } - - // Usage - err := CreateUser(CreateUserParams{ - Name: "John Doe", - Email: "john@example.com", - Phone: "555-0123", - Address: "123 Main St", - Age: 30, - Active: true, - }) - ``` - -- **Project Structure** - - Primary interface definitions in package root - - Implementations in subdirectories by backing technology - -- **Variable Name Length:** - - Favor variable names that are at least three characters long, except for - loop indices (e.g., `i`, `j`), method receivers (e.g., `r` for `receiver`), - and extremely common types (e.g., `r` for `io.Reader`, `w` for `io.Writer`). - - Prioritize clarity and readability. Use the shortest name that - effectively conveys the variable's purpose within its context. - - Variable naming: camelCase, descriptive names, no abbreviations except for - common ones - -- **Naming Style:** - - Use `camelCase` for variable and function names (e.g., `myVariableName`, `calculateTotal`). - - Use `PascalCase` for exported (public) types, functions, and constants (e.g., `MyType`, `CalculateTotal`). - - Avoid `snake_case` (e.g., `my_variable_name`) in most cases. - -- **Clarity and Context:** - - The further a variable is used from its declaration, the more descriptive - its name should be - ```go - func processData() { - // Short scope: single letter acceptable - for i := 0; i < 10; i++ { - fmt.Println(i) - } - - // Medium scope: short but descriptive - users := fetchUsers() - for _, user := range users { - processUser(user) - } - } - - func longRunningFunction() { - // Long scope: highly descriptive names - authenticatedUserRepository := NewUserRepository() - configurationManager := NewConfigManager() - emailNotificationService := NewEmailService() - - // These variables are used throughout the function - for page := 1; page <= totalPages; page++ { - paginatedUserResults := authenticatedUserRepository.GetUsersByPage(page) - - for _, individualUser := range paginatedUserResults { - userEmailAddress := individualUser.Email - notificationPreferences := configurationManager.GetPreferences(individualUser.ID) - - if notificationPreferences.EmailEnabled { - emailNotificationService.Send(userEmailAddress, "Welcome!") - } - } - } - } - - // Function parameters and package-level variables: descriptive - func CalculateMonthlySubscriptionRevenue(subscriptionDetails []Subscription, - discountCalculator DiscountService) decimal.Decimal { - totalMonthlyRevenue := decimal.Zero - - for _, subscription := range subscriptionDetails { - monthlyAmount := subscription.MonthlyPrice - applicableDiscount := discountCalculator.Calculate(subscription) - finalAmount := monthlyAmount.Sub(applicableDiscount) - totalMonthlyRevenue = totalMonthlyRevenue.Add(finalAmount) - } - - return totalMonthlyRevenue - } - ``` - - Choose names that clearly indicate the variable's purpose and the type of - data it holds. - -- **Avoidance:** - - Do not use spaces in variable names. - - Variable names should start with a letter or underscore. - - Do not use Go keywords as variable names. - -- **Constants:** - - Use `PascalCase` for constants. If a constant is unscoped, all letters in - the constant should be capitalized. `const MAX_SIZE = 100` - -- **Error Handling:** - - When naming error variables, use `err` as the prefix: `errMyCustomError`. - - Always check errors and return meaningful wrapped errors - ```go - import ( - "fmt" - "io" - "os" - ) - - // Avoid: Ignoring errors - func badExample() { - file, _ := os.Open("config.txt") - data, _ := io.ReadAll(file) - fmt.Println(string(data)) - } - - // Preferred: Always check and wrap errors with context - func goodExample() error { - file, err := os.Open("config.txt") - if err != nil { - return fmt.Errorf("failed to open config file: %w", err) - } - defer file.Close() - - data, err := io.ReadAll(file) - if err != nil { - return fmt.Errorf("failed to read config file: %w", err) - } - - fmt.Println(string(data)) - return nil - } - - // Multiple operations: preserve error chain - func processUserData(userID string) error { - user, err := fetchUser(userID) - if err != nil { - return fmt.Errorf("failed to fetch user %s: %w", userID, err) - } - - profile, err := loadProfile(user.ProfileID) - if err != nil { - return fmt.Errorf("failed to load profile for user %s: %w", userID, err) - } - - err = validateProfile(profile) - if err != nil { - return fmt.Errorf("invalid profile for user %s: %w", userID, err) - } - - err = saveProcessedData(user, profile) - if err != nil { - return fmt.Errorf("failed to save processed data for user %s: %w", userID, err) - } - - return nil - } - - // Custom error types for better error handling - type ValidationError struct { - Field string - Value interface{} - Message string - } - - func (e ValidationError) Error() string { - return fmt.Sprintf("validation failed for field '%s' with value '%v': %s", - e.Field, e.Value, e.Message) - } - - func validateEmail(email string) error { - if email == "" { - return ValidationError{ - Field: "email", - Value: email, - Message: "email cannot be empty", - } - } - - if !strings.Contains(email, "@") { - return fmt.Errorf("invalid email format: %w", ValidationError{ - Field: "email", - Value: email, - Message: "must contain @ symbol", - }) - } - - return nil - } - ``` - -- **Receivers:** - - Use short, one or two-letter receiver names that reflect the type (e.g., - `r` for `io.Reader`, `f` for `*File`). - - From e802be0aafda7f81b66c69c5f8dee35892d2ef81 Mon Sep 17 00:00:00 2001 From: Siddhartha Basu Date: Tue, 21 Apr 2026 12:38:56 +0000 Subject: [PATCH 02/17] fix(plasmid): refactor ListPlasmids resolver state pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace cascading state structs with tuple-based pipeline - Streamline filter build process - Add validation for plasmid list filters (id, in_stock) 💘 Generated with Crush Assisted-by: Google: Gemini 3.1 Flash Lite Preview via Crush --- internal/graphql/resolver/stock.go | 6 +- internal/graphql/resolver/stock_plasmid_fp.go | 204 ++++++++++++------ 2 files changed, 146 insertions(+), 64 deletions(-) diff --git a/internal/graphql/resolver/stock.go b/internal/graphql/resolver/stock.go index 3c14af70..6b04166f 100644 --- a/internal/graphql/resolver/stock.go +++ b/internal/graphql/resolver/stock.go @@ -328,9 +328,9 @@ func (qrs *QueryResolver) ListPlasmids( limit: limit, filter: filter, }), - IOE.Let[error](setListPlasmidParams, computeListPlasmidParams), - IOE.Bind(setListPlasmidFilter, buildListPlasmidFilterQuery), - IOE.Bind(setListPlasmidCollection, fetchListPlasmidCollection), + IOE.Map[error](computeListPlasmidParams), + IOE.Chain(buildListPlasmidFilterQuery), + IOE.Chain(fetchListPlasmidCollection), IOE.Map[error](extractListPlasmidResult), toEither[error, *models.PlasmidListWithCursor], E.Fold(onPlasmidListError, onPlasmidListSuccess), diff --git a/internal/graphql/resolver/stock_plasmid_fp.go b/internal/graphql/resolver/stock_plasmid_fp.go index c9df4ae1..348b69b3 100644 --- a/internal/graphql/resolver/stock_plasmid_fp.go +++ b/internal/graphql/resolver/stock_plasmid_fp.go @@ -2,17 +2,22 @@ package resolver import ( "context" + "fmt" A "github.com/IBM/fp-go/v2/array" E "github.com/IBM/fp-go/v2/either" F "github.com/IBM/fp-go/v2/function" IOE "github.com/IBM/fp-go/v2/ioeither" + O "github.com/IBM/fp-go/v2/option" + P "github.com/IBM/fp-go/v2/predicate" + R "github.com/IBM/fp-go/v2/record" + S "github.com/IBM/fp-go/v2/string" T "github.com/IBM/fp-go/v2/tuple" "github.com/dictyBase/aphgrpc" pb "github.com/dictyBase/go-genproto/dictybaseapis/stock" "github.com/dictyBase/graphql-server/internal/graphql/models" - "github.com/dictyBase/graphql-server/internal/graphql/resolverutils" + "github.com/dictyBase/graphql-server/internal/registry" ) type plasmidResultContext struct { @@ -32,53 +37,55 @@ type listPlasmidsContext struct { filter *models.PlasmidListFilter } -type withListPlasmidParams struct { - listPlasmidsContext - cus int64 - lmt int64 -} - -type withListPlasmidFilter struct { - withListPlasmidParams - filterQuery string -} - -type withListPlasmidCollection struct { - withListPlasmidFilter - collection *pb.PlasmidCollection -} +type ( + listPlasmidParamsTuple = T.Tuple3[listPlasmidsContext, int64, int64] + listPlasmidFilterBuildTuple = T.Tuple5[ + listPlasmidsContext, + int64, + int64, + *models.PlasmidListFilter, + models.PlasmidType, + ] + listPlasmidFilterTuple = T.Tuple4[listPlasmidsContext, int64, int64, string] + listPlasmidCollectionTuple = T.Tuple5[listPlasmidsContext, int64, int64, string, *pb.PlasmidCollection] +) var ( - setListPlasmidParams = F.Curry2( - func(params T.Tuple2[int64, int64], ctx listPlasmidsContext) withListPlasmidParams { - return withListPlasmidParams{ - listPlasmidsContext: ctx, - cus: params.F1, - lmt: params.F2, - } + ptrString = func(s string) *string { return &s } + + formatPlasmidFieldQuery = F.Curry2( + func(format string, value *string) string { + return S.Format[string](format)(*value) }, ) - setListPlasmidFilter = F.Curry2( - func(query string, ctx withListPlasmidParams) withListPlasmidFilter { - return withListPlasmidFilter{ - withListPlasmidParams: ctx, - filterQuery: query, - } - }, + compactOptionStrings = A.FilterMap(O.Fold( + F.Constant(O.None[string]()), + O.Some[string], + )) + + isNilPlasmidIDFilter = F.Pipe1( + P.IsZero[*string](), + P.ContraMap(func(filter *models.PlasmidListFilter) *string { + return filter.ID + }), ) - setListPlasmidCollection = F.Curry2( - func(coll *pb.PlasmidCollection, ctx withListPlasmidFilter) withListPlasmidCollection { - return withListPlasmidCollection{ - withListPlasmidFilter: ctx, - collection: coll, - } + checkNilPlasmidInStockFilter = E.FromPredicate( + F.Pipe1( + P.IsZero[*bool](), + P.ContraMap(func(filter *models.PlasmidListFilter) *bool { + return filter.InStock + }), + ), + func(filter *models.PlasmidListFilter) error { + return fmt.Errorf( + "plasmid list filter %v: in_stock filter is not yet supported in stock query conversion", + filter, + ) }, ) - ptrString = func(s string) *string { return &s } - convertPlasmidDataItem = func(item *pb.PlasmidCollection_Data) *models.Plasmid { return &models.Plasmid{ ID: item.Id, @@ -119,44 +126,85 @@ func onPlasmidListSuccess( func computeListPlasmidParams( ctx listPlasmidsContext, -) T.Tuple2[int64, int64] { - return T.MakeTuple2( +) listPlasmidParamsTuple { + return T.MakeTuple3( + ctx, resolverutils.GetCursorFP(ctx.cursor), resolverutils.GetLimitFP(ctx.limit), ) } -func buildListPlasmidFilterQuery(ctx withListPlasmidParams) IOE.IOEither[error, string] { - return IOE.TryCatchError(func() (string, error) { - return resolverutils.PlasmidFilterToQuery(ctx.filter) - }) +func buildListPlasmidFilterQuery( + state listPlasmidParamsTuple, +) IOE.IOEither[error, listPlasmidFilterTuple] { + return F.Pipe6( + state.F1.filter, + O.FromNillable[models.PlasmidListFilter], + O.GetOrElse(F.Constant(&models.PlasmidListFilter{ + PlasmidType: models.PlasmidTypeAll, + })), + E.FromPredicate( + isNilPlasmidIDFilter, + func(filter *models.PlasmidListFilter) error { + return fmt.Errorf( + "plasmid list filter %v: id filter is not yet supported in stock query conversion", + filter, + ) + }, + ), + E.Chain(checkNilPlasmidInStockFilter), + E.Chain(func(filter *models.PlasmidListFilter) E.Either[error, listPlasmidFilterTuple] { + return F.Pipe3( + filter.PlasmidType, + E.FromPredicate( + func(plasmidType models.PlasmidType) bool { + return plasmidType.IsValid() + }, + func(plasmidType models.PlasmidType) error { + return fmt.Errorf("invalid plasmid type %s", plasmidType.String()) + }, + ), + E.Map[error](func(plasmidType models.PlasmidType) listPlasmidFilterBuildTuple { + return T.MakeTuple5(state.F1, state.F2, state.F3, filter, plasmidType) + }), + E.Map[error](buildListPlasmidFilterTuple), + ) + }), + IOE.FromEither[error, listPlasmidFilterTuple], + ) } func fetchListPlasmidCollection( - ctx withListPlasmidFilter, -) IOE.IOEither[error, *pb.PlasmidCollection] { - return IOE.TryCatchError(func() (*pb.PlasmidCollection, error) { - return ctx.client.ListPlasmids( - ctx.gctx, - &pb.StockParameters{ - Cursor: ctx.cus, - Limit: ctx.lmt, - Filter: ctx.filterQuery, - }) - }) + state listPlasmidFilterTuple, +) IOE.IOEither[error, listPlasmidCollectionTuple] { + return F.Pipe1( + IOE.TryCatchError(func() (*pb.PlasmidCollection, error) { + return state.F1.client.ListPlasmids( + state.F1.gctx, + &pb.StockParameters{ + Cursor: state.F2, + Limit: state.F3, + Filter: state.F4, + }, + ) + }), + IOE.Map[error](func(coll *pb.PlasmidCollection) listPlasmidCollectionTuple { + return T.MakeTuple5(state.F1, state.F2, state.F3, state.F4, coll) + }), + ) } func extractListPlasmidResult( - ctx withListPlasmidCollection, + state listPlasmidCollectionTuple, ) *models.PlasmidListWithCursor { return F.Pipe2( T.MakeTuple2( - ctx.collection.Data, + state.F5.Data, plasmidResultContext{ - limit: ctx.collection.Meta.Limit, - nextCursor: ctx.collection.Meta.NextCursor, - total: ctx.collection.Meta.Total, - cursor: ctx.cus, + limit: state.F5.Meta.Limit, + nextCursor: state.F5.Meta.NextCursor, + total: state.F5.Meta.Total, + cursor: state.F2, }, ), T.Map2(A.Map(convertPlasmidDataItem), F.Identity[plasmidResultContext]), @@ -172,3 +220,37 @@ func extractListPlasmidResult( }, ) } + +func buildListPlasmidFilterTuple(ctx listPlasmidFilterBuildTuple) listPlasmidFilterTuple { + return T.MakeTuple4( + ctx.F1, + ctx.F2, + ctx.F3, + F.Pipe2( + []O.Option[string]{ + F.Pipe1( + O.FromNillable(ctx.F4.Summary), + O.Map(formatPlasmidFieldQuery("summary=~%s")), + ), + F.Pipe1( + O.FromNillable(ctx.F4.Name), + O.Map(formatPlasmidFieldQuery("plasmid_name===%s")), + ), + R.Lookup[string](ctx.F5)(map[models.PlasmidType]string{ + models.PlasmidTypeRegular: fmt.Sprintf( + "ontology==%s;tag==%s", + registry.DictyPlasmidPropOntology, + registry.RegularPlasmidTag, + ), + models.PlasmidTypeGoldenBraid: fmt.Sprintf( + "ontology==%s;tag==%s", + registry.DictyPlasmidPropOntology, + registry.GoldenBraidPlasmidTag, + ), + }), + }, + compactOptionStrings, + A.Intercalate(S.Monoid)(";"), + ), + ) +} From 73ccb26bee3c54e25e2211269f00ae4bd9b49e6f Mon Sep 17 00:00:00 2001 From: Siddhartha Basu Date: Tue, 21 Apr 2026 12:39:20 +0000 Subject: [PATCH 03/17] docs: add AGENTS.md with essential development commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Include testing instructions with gotestsum - Include linting and formatting commands using golangci-lint 💘 Generated with Crush Assisted-by: Google: Gemini 3.1 Flash Lite Preview via Crush --- AGENTS.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..3beb5546 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,22 @@ +# AGENTS.md + + +## Essential Commands + +```bash +# Test +gotestsum --format pkgname-and-test-fails --format-hide-empty-pkg -- ./... + +# Test (verbose) +gotestsum --format testdox --format-hide-empty-pkg -- ./... + +# Watch mode +gotestsum --watch --format pkgname-and-test-fails --format-hide-empty-pkg -- ./... + +# Lint +golangci-lint run ./... + +# Format +golangci-lint fmt + + From eaa1213dd83c96160bc957493148abfe78b439c1 Mon Sep 17 00:00:00 2001 From: Siddhartha Basu Date: Tue, 21 Apr 2026 13:58:59 +0000 Subject: [PATCH 04/17] refactor(plasmid): extract plasmid filter validation to curried function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the plasmid type validation and state building logic out of the buildListPlasmidFilterQuery pipeline into validateAndBuildPlasmidFilter. 💘 Generated with Crush Assisted-by: Google: Gemini 3 Flash Preview via Crush --- internal/graphql/resolver/stock_plasmid_fp.go | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/internal/graphql/resolver/stock_plasmid_fp.go b/internal/graphql/resolver/stock_plasmid_fp.go index 348b69b3..5bf2a6e3 100644 --- a/internal/graphql/resolver/stock_plasmid_fp.go +++ b/internal/graphql/resolver/stock_plasmid_fp.go @@ -106,6 +106,26 @@ var ( }, } } + + validateAndBuildPlasmidFilter = F.Curry2( + func(state listPlasmidParamsTuple, filter *models.PlasmidListFilter) E.Either[error, listPlasmidFilterTuple] { + return F.Pipe3( + filter.PlasmidType, + E.FromPredicate( + func(plasmidType models.PlasmidType) bool { + return plasmidType.IsValid() + }, + func(plasmidType models.PlasmidType) error { + return fmt.Errorf("invalid plasmid type %s", plasmidType.String()) + }, + ), + E.Map[error](func(plasmidType models.PlasmidType) listPlasmidFilterBuildTuple { + return T.MakeTuple5(state.F1, state.F2, state.F3, filter, plasmidType) + }), + E.Map[error](buildListPlasmidFilterTuple), + ) + }, + ) ) func toEither[ERR, A any](ioe IOE.IOEither[ERR, A]) E.Either[ERR, A] { @@ -153,23 +173,7 @@ func buildListPlasmidFilterQuery( }, ), E.Chain(checkNilPlasmidInStockFilter), - E.Chain(func(filter *models.PlasmidListFilter) E.Either[error, listPlasmidFilterTuple] { - return F.Pipe3( - filter.PlasmidType, - E.FromPredicate( - func(plasmidType models.PlasmidType) bool { - return plasmidType.IsValid() - }, - func(plasmidType models.PlasmidType) error { - return fmt.Errorf("invalid plasmid type %s", plasmidType.String()) - }, - ), - E.Map[error](func(plasmidType models.PlasmidType) listPlasmidFilterBuildTuple { - return T.MakeTuple5(state.F1, state.F2, state.F3, filter, plasmidType) - }), - E.Map[error](buildListPlasmidFilterTuple), - ) - }), + E.Chain(validateAndBuildPlasmidFilter(state)), IOE.FromEither[error, listPlasmidFilterTuple], ) } From 55a47663bb8baef325fbc80e260b7beac0d6b80b Mon Sep 17 00:00:00 2001 From: Siddhartha Basu Date: Tue, 21 Apr 2026 14:01:12 +0000 Subject: [PATCH 05/17] refactor(plasmid): refine types in plasmidResultContext fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update limit, cursor, and total fields in plasmidResultContext to use domain-specific types for better alignment with the resolver pipeline. 💘 Generated with Crush Assisted-by: Google: Gemini 3 Flash Preview via Crush --- internal/graphql/resolver/stock_plasmid_fp.go | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/internal/graphql/resolver/stock_plasmid_fp.go b/internal/graphql/resolver/stock_plasmid_fp.go index 5bf2a6e3..b6b77e94 100644 --- a/internal/graphql/resolver/stock_plasmid_fp.go +++ b/internal/graphql/resolver/stock_plasmid_fp.go @@ -21,10 +21,10 @@ import ( ) type plasmidResultContext struct { - limit int64 - nextCursor int64 - total int64 - cursor int64 + limit limit + nextCursor cursor + total total + cursor cursor } type plasmidResultTuple = T.Tuple2[[]*models.Plasmid, plasmidResultContext] @@ -38,16 +38,23 @@ type listPlasmidsContext struct { } type ( - listPlasmidParamsTuple = T.Tuple3[listPlasmidsContext, int64, int64] + cursor = int64 + limit = int64 + total = int64 + filterQuery = string +) + +type ( + listPlasmidParamsTuple = T.Tuple3[listPlasmidsContext, cursor, limit] listPlasmidFilterBuildTuple = T.Tuple5[ listPlasmidsContext, - int64, - int64, + cursor, + limit, *models.PlasmidListFilter, models.PlasmidType, ] - listPlasmidFilterTuple = T.Tuple4[listPlasmidsContext, int64, int64, string] - listPlasmidCollectionTuple = T.Tuple5[listPlasmidsContext, int64, int64, string, *pb.PlasmidCollection] + listPlasmidFilterTuple = T.Tuple4[listPlasmidsContext, cursor, limit, filterQuery] + listPlasmidCollectionTuple = T.Tuple5[listPlasmidsContext, cursor, limit, filterQuery, *pb.PlasmidCollection] ) var ( From cca90af5b11891e7aad5babf04197e966e0fc793 Mon Sep 17 00:00:00 2001 From: Siddhartha Basu Date: Tue, 21 Apr 2026 16:18:50 +0000 Subject: [PATCH 06/17] refactor(plasmid): expand listPlasmidsContext and add filterValidationPair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 💘 Generated with Crush Assisted-by: Google: Gemini 3 Flash Preview via Crush --- internal/graphql/resolver/stock_plasmid_fp.go | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/internal/graphql/resolver/stock_plasmid_fp.go b/internal/graphql/resolver/stock_plasmid_fp.go index b6b77e94..ee2516d7 100644 --- a/internal/graphql/resolver/stock_plasmid_fp.go +++ b/internal/graphql/resolver/stock_plasmid_fp.go @@ -11,8 +11,8 @@ import ( O "github.com/IBM/fp-go/v2/option" P "github.com/IBM/fp-go/v2/predicate" R "github.com/IBM/fp-go/v2/record" + Pa "github.com/IBM/fp-go/v2/pair" S "github.com/IBM/fp-go/v2/string" - T "github.com/IBM/fp-go/v2/tuple" "github.com/dictyBase/aphgrpc" pb "github.com/dictyBase/go-genproto/dictybaseapis/stock" "github.com/dictyBase/graphql-server/internal/graphql/models" @@ -20,23 +20,24 @@ import ( "github.com/dictyBase/graphql-server/internal/registry" ) -type plasmidResultContext struct { - limit limit - nextCursor cursor - total total - cursor cursor -} - -type plasmidResultTuple = T.Tuple2[[]*models.Plasmid, plasmidResultContext] - type listPlasmidsContext struct { + // inputs (set by caller) client pb.StockServiceClient gctx context.Context cursor *int limit *int filter *models.PlasmidListFilter + // computed by pipeline steps + resolvedCursor cursor + resolvedLimit limit + filterQuery filterQuery + collection *pb.PlasmidCollection } +// filterValidationPair threads the stable context alongside the evolving filter +// through the inner validation sub-pipe, keeping all validators univariate. +type filterValidationPair = Pa.Pair[listPlasmidsContext, *models.PlasmidListFilter] + type ( cursor = int64 limit = int64 @@ -44,19 +45,6 @@ type ( filterQuery = string ) -type ( - listPlasmidParamsTuple = T.Tuple3[listPlasmidsContext, cursor, limit] - listPlasmidFilterBuildTuple = T.Tuple5[ - listPlasmidsContext, - cursor, - limit, - *models.PlasmidListFilter, - models.PlasmidType, - ] - listPlasmidFilterTuple = T.Tuple4[listPlasmidsContext, cursor, limit, filterQuery] - listPlasmidCollectionTuple = T.Tuple5[listPlasmidsContext, cursor, limit, filterQuery, *pb.PlasmidCollection] -) - var ( ptrString = func(s string) *string { return &s } From c77d40a585f3e16a387df650eccdf049fee29a1d Mon Sep 17 00:00:00 2001 From: Siddhartha Basu Date: Tue, 21 Apr 2026 16:18:58 +0000 Subject: [PATCH 07/17] refactor(plasmid): update filter predicates to operate on filterValidationPair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 💘 Generated with Crush Assisted-by: Google: Gemini 3 Flash Preview via Crush --- internal/graphql/resolver/stock_plasmid_fp.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/graphql/resolver/stock_plasmid_fp.go b/internal/graphql/resolver/stock_plasmid_fp.go index ee2516d7..9878b5cb 100644 --- a/internal/graphql/resolver/stock_plasmid_fp.go +++ b/internal/graphql/resolver/stock_plasmid_fp.go @@ -61,22 +61,22 @@ var ( isNilPlasmidIDFilter = F.Pipe1( P.IsZero[*string](), - P.ContraMap(func(filter *models.PlasmidListFilter) *string { - return filter.ID + P.ContraMap(func(pair filterValidationPair) *string { + return Pa.Tail(pair).ID }), ) checkNilPlasmidInStockFilter = E.FromPredicate( F.Pipe1( P.IsZero[*bool](), - P.ContraMap(func(filter *models.PlasmidListFilter) *bool { - return filter.InStock + P.ContraMap(func(pair filterValidationPair) *bool { + return Pa.Tail(pair).InStock }), ), - func(filter *models.PlasmidListFilter) error { + func(pair filterValidationPair) error { return fmt.Errorf( "plasmid list filter %v: in_stock filter is not yet supported in stock query conversion", - filter, + Pa.Tail(pair), ) }, ) From f4b649117212d5f02821bf4c1d51d16314e21aa9 Mon Sep 17 00:00:00 2001 From: Siddhartha Basu Date: Tue, 21 Apr 2026 16:19:10 +0000 Subject: [PATCH 08/17] refactor(plasmid): validateAndBuildPlasmidFilter is now univariate via filterValidationPair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 💘 Generated with Crush Assisted-by: Google: Gemini 3 Flash Preview via Crush --- internal/graphql/resolver/stock_plasmid_fp.go | 94 +++++++++---------- 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/internal/graphql/resolver/stock_plasmid_fp.go b/internal/graphql/resolver/stock_plasmid_fp.go index 9878b5cb..a820bd7c 100644 --- a/internal/graphql/resolver/stock_plasmid_fp.go +++ b/internal/graphql/resolver/stock_plasmid_fp.go @@ -102,25 +102,25 @@ var ( } } - validateAndBuildPlasmidFilter = F.Curry2( - func(state listPlasmidParamsTuple, filter *models.PlasmidListFilter) E.Either[error, listPlasmidFilterTuple] { - return F.Pipe3( - filter.PlasmidType, - E.FromPredicate( - func(plasmidType models.PlasmidType) bool { - return plasmidType.IsValid() - }, - func(plasmidType models.PlasmidType) error { - return fmt.Errorf("invalid plasmid type %s", plasmidType.String()) - }, - ), - E.Map[error](func(plasmidType models.PlasmidType) listPlasmidFilterBuildTuple { - return T.MakeTuple5(state.F1, state.F2, state.F3, filter, plasmidType) - }), - E.Map[error](buildListPlasmidFilterTuple), - ) - }, - ) + validateAndBuildPlasmidFilter = func(pair filterValidationPair) E.Either[error, listPlasmidsContext] { + filter := Pa.Tail(pair) + ctx := Pa.Head(pair) + return F.Pipe2( + filter.PlasmidType, + E.FromPredicate( + func(plasmidType models.PlasmidType) bool { + return plasmidType.IsValid() + }, + func(plasmidType models.PlasmidType) error { + return fmt.Errorf("invalid plasmid type %s", plasmidType.String()) + }, + ), + E.Map[error](func(plasmidType models.PlasmidType) listPlasmidsContext { + ctx.filterQuery = buildFilterQuery(filter, plasmidType) + return ctx + }), + ) + } ) func toEither[ERR, A any](ioe IOE.IOEither[ERR, A]) E.Either[ERR, A] { @@ -220,36 +220,34 @@ func extractListPlasmidResult( ) } -func buildListPlasmidFilterTuple(ctx listPlasmidFilterBuildTuple) listPlasmidFilterTuple { - return T.MakeTuple4( - ctx.F1, - ctx.F2, - ctx.F3, - F.Pipe2( - []O.Option[string]{ - F.Pipe1( - O.FromNillable(ctx.F4.Summary), - O.Map(formatPlasmidFieldQuery("summary=~%s")), +func buildFilterQuery( + filter *models.PlasmidListFilter, + plasmidType models.PlasmidType, +) filterQuery { + return F.Pipe2( + []O.Option[string]{ + F.Pipe1( + O.FromNillable(filter.Summary), + O.Map(formatPlasmidFieldQuery("summary=~%s")), + ), + F.Pipe1( + O.FromNillable(filter.Name), + O.Map(formatPlasmidFieldQuery("plasmid_name===%s")), + ), + R.Lookup[string](plasmidType)(map[models.PlasmidType]string{ + models.PlasmidTypeRegular: fmt.Sprintf( + "ontology==%s;tag==%s", + registry.DictyPlasmidPropOntology, + registry.RegularPlasmidTag, ), - F.Pipe1( - O.FromNillable(ctx.F4.Name), - O.Map(formatPlasmidFieldQuery("plasmid_name===%s")), + models.PlasmidTypeGoldenBraid: fmt.Sprintf( + "ontology==%s;tag==%s", + registry.DictyPlasmidPropOntology, + registry.GoldenBraidPlasmidTag, ), - R.Lookup[string](ctx.F5)(map[models.PlasmidType]string{ - models.PlasmidTypeRegular: fmt.Sprintf( - "ontology==%s;tag==%s", - registry.DictyPlasmidPropOntology, - registry.RegularPlasmidTag, - ), - models.PlasmidTypeGoldenBraid: fmt.Sprintf( - "ontology==%s;tag==%s", - registry.DictyPlasmidPropOntology, - registry.GoldenBraidPlasmidTag, - ), - }), - }, - compactOptionStrings, - A.Intercalate(S.Monoid)(";"), - ), + }), + }, + compactOptionStrings, + A.Intercalate(S.Monoid)(";"), ) } From 8d4234b83ce3617e7ad2104b037788eb3f2c1f0c Mon Sep 17 00:00:00 2001 From: Siddhartha Basu Date: Tue, 21 Apr 2026 16:19:20 +0000 Subject: [PATCH 09/17] refactor(plasmid): buildListPlasmidFilterQuery uses Pa.Pair for point-free validation chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 💘 Generated with Crush Assisted-by: Google: Gemini 3 Flash Preview via Crush --- internal/graphql/resolver/stock_plasmid_fp.go | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/internal/graphql/resolver/stock_plasmid_fp.go b/internal/graphql/resolver/stock_plasmid_fp.go index a820bd7c..4fc9ab97 100644 --- a/internal/graphql/resolver/stock_plasmid_fp.go +++ b/internal/graphql/resolver/stock_plasmid_fp.go @@ -150,26 +150,29 @@ func computeListPlasmidParams( } func buildListPlasmidFilterQuery( - state listPlasmidParamsTuple, -) IOE.IOEither[error, listPlasmidFilterTuple] { - return F.Pipe6( - state.F1.filter, + ctx listPlasmidsContext, +) IOE.IOEither[error, listPlasmidsContext] { + resolvedFilter := F.Pipe2( + ctx.filter, O.FromNillable[models.PlasmidListFilter], O.GetOrElse(F.Constant(&models.PlasmidListFilter{ PlasmidType: models.PlasmidTypeAll, })), + ) + return F.Pipe4( + Pa.MakePair(ctx, resolvedFilter), E.FromPredicate( isNilPlasmidIDFilter, - func(filter *models.PlasmidListFilter) error { + func(pair filterValidationPair) error { return fmt.Errorf( "plasmid list filter %v: id filter is not yet supported in stock query conversion", - filter, + Pa.Tail(pair), ) }, ), E.Chain(checkNilPlasmidInStockFilter), - E.Chain(validateAndBuildPlasmidFilter(state)), - IOE.FromEither[error, listPlasmidFilterTuple], + E.Chain(validateAndBuildPlasmidFilter), + IOE.FromEither[error, listPlasmidsContext], ) } From 073791094fc56a3090f7906debdea8157a91a710 Mon Sep 17 00:00:00 2001 From: Siddhartha Basu Date: Tue, 21 Apr 2026 16:20:41 +0000 Subject: [PATCH 10/17] refactor(plasmid): all pipeline steps operate on listPlasmidsContext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 💘 Generated with Crush Assisted-by: Google: Gemini 3 Flash Preview via Crush --- internal/graphql/resolver/stock.go | 24 +---- internal/graphql/resolver/stock_plasmid_fp.go | 102 ++++++++---------- 2 files changed, 48 insertions(+), 78 deletions(-) diff --git a/internal/graphql/resolver/stock.go b/internal/graphql/resolver/stock.go index 6b04166f..f2c24b31 100644 --- a/internal/graphql/resolver/stock.go +++ b/internal/graphql/resolver/stock.go @@ -8,7 +8,6 @@ import ( "context" "fmt" - E "github.com/IBM/fp-go/v2/either" F "github.com/IBM/fp-go/v2/function" IOE "github.com/IBM/fp-go/v2/ioeither" anno "github.com/dictyBase/go-genproto/dictybaseapis/annotation" @@ -320,33 +319,18 @@ func (qrs *QueryResolver) ListPlasmids( limit *int, filter *models.PlasmidListFilter, ) (*models.PlasmidListWithCursor, error) { - result := F.Pipe6( - IOE.Of[error](listPlasmidsContext{ + return F.Pipe4( + IOE.Of[error](computeListPlasmidParams(listPlasmidsContext{ client: qrs.GetStockClient(registry.STOCK), gctx: ctx, cursor: cursor, limit: limit, filter: filter, - }), - IOE.Map[error](computeListPlasmidParams), + })), IOE.Chain(buildListPlasmidFilterQuery), IOE.Chain(fetchListPlasmidCollection), IOE.Map[error](extractListPlasmidResult), - toEither[error, *models.PlasmidListWithCursor], - E.Fold(onPlasmidListError, onPlasmidListSuccess), - ) - - if result.F1 != nil { - errorutils.AddGQLError(ctx, result.F1) - qrs.Logger.Error(result.F1) - return result.F2, result.F1 - } - - qrs.Logger.Debugf( - "successfully retrieved list of %v plasmids", - result.F2.TotalCount, - ) - return result.F2, nil + )() } //nolint:dupl diff --git a/internal/graphql/resolver/stock_plasmid_fp.go b/internal/graphql/resolver/stock_plasmid_fp.go index 4fc9ab97..d9d20481 100644 --- a/internal/graphql/resolver/stock_plasmid_fp.go +++ b/internal/graphql/resolver/stock_plasmid_fp.go @@ -123,32 +123,6 @@ var ( } ) -func toEither[ERR, A any](ioe IOE.IOEither[ERR, A]) E.Either[ERR, A] { - return ioe() -} - -func onPlasmidListError( - err error, -) T.Tuple2[error, *models.PlasmidListWithCursor] { - return T.MakeTuple2(err, &models.PlasmidListWithCursor{}) -} - -func onPlasmidListSuccess( - data *models.PlasmidListWithCursor, -) T.Tuple2[error, *models.PlasmidListWithCursor] { - return T.MakeTuple2[error](nil, data) -} - -func computeListPlasmidParams( - ctx listPlasmidsContext, -) listPlasmidParamsTuple { - return T.MakeTuple3( - ctx, - resolverutils.GetCursorFP(ctx.cursor), - resolverutils.GetLimitFP(ctx.limit), - ) -} - func buildListPlasmidFilterQuery( ctx listPlasmidsContext, ) IOE.IOEither[error, listPlasmidsContext] { @@ -176,51 +150,63 @@ func buildListPlasmidFilterQuery( ) } +func toEither[ERR, A any](ioe IOE.IOEither[ERR, A]) E.Either[ERR, A] { + return ioe() +} + +func onPlasmidListError( + err error, +) (error, *models.PlasmidListWithCursor) { + return err, &models.PlasmidListWithCursor{} +} + +func onPlasmidListSuccess( + data *models.PlasmidListWithCursor, +) (error, *models.PlasmidListWithCursor) { + return nil, data +} + +func computeListPlasmidParams( + ctx listPlasmidsContext, +) listPlasmidsContext { + ctx.resolvedCursor = resolverutils.GetCursorFP(ctx.cursor) + ctx.resolvedLimit = resolverutils.GetLimitFP(ctx.limit) + return ctx +} + func fetchListPlasmidCollection( - state listPlasmidFilterTuple, -) IOE.IOEither[error, listPlasmidCollectionTuple] { + ctx listPlasmidsContext, +) IOE.IOEither[error, listPlasmidsContext] { return F.Pipe1( IOE.TryCatchError(func() (*pb.PlasmidCollection, error) { - return state.F1.client.ListPlasmids( - state.F1.gctx, + return ctx.client.ListPlasmids( + ctx.gctx, &pb.StockParameters{ - Cursor: state.F2, - Limit: state.F3, - Filter: state.F4, + Cursor: ctx.resolvedCursor, + Limit: ctx.resolvedLimit, + Filter: ctx.filterQuery, }, ) }), - IOE.Map[error](func(coll *pb.PlasmidCollection) listPlasmidCollectionTuple { - return T.MakeTuple5(state.F1, state.F2, state.F3, state.F4, coll) + IOE.Map[error](func(coll *pb.PlasmidCollection) listPlasmidsContext { + ctx.collection = coll + return ctx }), ) } func extractListPlasmidResult( - state listPlasmidCollectionTuple, + ctx listPlasmidsContext, ) *models.PlasmidListWithCursor { - return F.Pipe2( - T.MakeTuple2( - state.F5.Data, - plasmidResultContext{ - limit: state.F5.Meta.Limit, - nextCursor: state.F5.Meta.NextCursor, - total: state.F5.Meta.Total, - cursor: state.F2, - }, - ), - T.Map2(A.Map(convertPlasmidDataItem), F.Identity[plasmidResultContext]), - func(tuple plasmidResultTuple) *models.PlasmidListWithCursor { - lmt := int(tuple.F2.limit) - return &models.PlasmidListWithCursor{ - Plasmids: tuple.F1, - NextCursor: int(tuple.F2.nextCursor), - PreviousCursor: int(tuple.F2.cursor), - Limit: &lmt, - TotalCount: int(tuple.F2.total), - } - }, - ) + meta := ctx.collection.Meta + lmt := int(meta.Limit) + return &models.PlasmidListWithCursor{ + Plasmids: A.Map(convertPlasmidDataItem)(ctx.collection.Data), + NextCursor: int(meta.NextCursor), + PreviousCursor: int(ctx.resolvedCursor), + Limit: &lmt, + TotalCount: int(meta.Total), + } } func buildFilterQuery( From a880a8f6f8f2cc5c5947ddd66f80056d5373bbdd Mon Sep 17 00:00:00 2001 From: Siddhartha Basu Date: Tue, 21 Apr 2026 16:23:50 +0000 Subject: [PATCH 11/17] refactor(plasmid): compose list plasmid pipeline as point-free IOEither chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 💘 Generated with Crush Assisted-by: Google: Gemini 3 Flash Preview via Crush --- internal/graphql/resolver/stock.go | 29 +++++++++++-------- internal/graphql/resolver/stock_plasmid_fp.go | 17 ----------- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/internal/graphql/resolver/stock.go b/internal/graphql/resolver/stock.go index f2c24b31..2de6220e 100644 --- a/internal/graphql/resolver/stock.go +++ b/internal/graphql/resolver/stock.go @@ -8,7 +8,6 @@ import ( "context" "fmt" - F "github.com/IBM/fp-go/v2/function" IOE "github.com/IBM/fp-go/v2/ioeither" anno "github.com/dictyBase/go-genproto/dictybaseapis/annotation" pb "github.com/dictyBase/go-genproto/dictybaseapis/stock" @@ -19,6 +18,7 @@ import ( "github.com/dictyBase/graphql-server/internal/registry" "github.com/fatih/structs" "github.com/mitchellh/mapstructure" + E "github.com/IBM/fp-go/v2/either" ) func (mrs *MutationResolver) CreateStrain( @@ -319,18 +319,23 @@ func (qrs *QueryResolver) ListPlasmids( limit *int, filter *models.PlasmidListFilter, ) (*models.PlasmidListWithCursor, error) { - return F.Pipe4( - IOE.Of[error](computeListPlasmidParams(listPlasmidsContext{ - client: qrs.GetStockClient(registry.STOCK), - gctx: ctx, - cursor: cursor, - limit: limit, - filter: filter, - })), - IOE.Chain(buildListPlasmidFilterQuery), - IOE.Chain(fetchListPlasmidCollection), - IOE.Map[error](extractListPlasmidResult), + ioe := IOE.Of[error](computeListPlasmidParams(listPlasmidsContext{ + client: qrs.GetStockClient(registry.STOCK), + gctx: ctx, + cursor: cursor, + limit: limit, + filter: filter, + })) + res := IOE.Map[error](extractListPlasmidResult)( + IOE.Chain(fetchListPlasmidCollection)( + IOE.Chain(buildListPlasmidFilterQuery)(ioe), + ), )() + if E.IsLeft(res) { + return &models.PlasmidListWithCursor{}, E.ToError(res) + } + val, _ := E.Unwrap(res) + return val, nil } //nolint:dupl diff --git a/internal/graphql/resolver/stock_plasmid_fp.go b/internal/graphql/resolver/stock_plasmid_fp.go index d9d20481..5ecd8b19 100644 --- a/internal/graphql/resolver/stock_plasmid_fp.go +++ b/internal/graphql/resolver/stock_plasmid_fp.go @@ -41,7 +41,6 @@ type filterValidationPair = Pa.Pair[listPlasmidsContext, *models.PlasmidListFilt type ( cursor = int64 limit = int64 - total = int64 filterQuery = string ) @@ -150,22 +149,6 @@ func buildListPlasmidFilterQuery( ) } -func toEither[ERR, A any](ioe IOE.IOEither[ERR, A]) E.Either[ERR, A] { - return ioe() -} - -func onPlasmidListError( - err error, -) (error, *models.PlasmidListWithCursor) { - return err, &models.PlasmidListWithCursor{} -} - -func onPlasmidListSuccess( - data *models.PlasmidListWithCursor, -) (error, *models.PlasmidListWithCursor) { - return nil, data -} - func computeListPlasmidParams( ctx listPlasmidsContext, ) listPlasmidsContext { From 333616a7bfd23259006854bd25e892c1baa41ae8 Mon Sep 17 00:00:00 2001 From: Siddhartha Basu Date: Tue, 21 Apr 2026 16:37:23 +0000 Subject: [PATCH 12/17] refactor(plasmid): streamline ListPlasmids resolver with functional pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactor ListPlasmids resolver into a point-free IOEither chain. - Simplify buildListPlasmidFilterQuery validation chain. - Add pipeline helpers for error and result folding. 💘 Generated with Crush Assisted-by: Google: Gemini 3 Flash Preview via Crush --- internal/graphql/resolver/stock.go | 45 +++++++++++------- internal/graphql/resolver/stock_plasmid_fp.go | 46 ++++++++++++------- 2 files changed, 58 insertions(+), 33 deletions(-) diff --git a/internal/graphql/resolver/stock.go b/internal/graphql/resolver/stock.go index 2de6220e..6b04166f 100644 --- a/internal/graphql/resolver/stock.go +++ b/internal/graphql/resolver/stock.go @@ -8,6 +8,8 @@ import ( "context" "fmt" + E "github.com/IBM/fp-go/v2/either" + F "github.com/IBM/fp-go/v2/function" IOE "github.com/IBM/fp-go/v2/ioeither" anno "github.com/dictyBase/go-genproto/dictybaseapis/annotation" pb "github.com/dictyBase/go-genproto/dictybaseapis/stock" @@ -18,7 +20,6 @@ import ( "github.com/dictyBase/graphql-server/internal/registry" "github.com/fatih/structs" "github.com/mitchellh/mapstructure" - E "github.com/IBM/fp-go/v2/either" ) func (mrs *MutationResolver) CreateStrain( @@ -319,23 +320,33 @@ func (qrs *QueryResolver) ListPlasmids( limit *int, filter *models.PlasmidListFilter, ) (*models.PlasmidListWithCursor, error) { - ioe := IOE.Of[error](computeListPlasmidParams(listPlasmidsContext{ - client: qrs.GetStockClient(registry.STOCK), - gctx: ctx, - cursor: cursor, - limit: limit, - filter: filter, - })) - res := IOE.Map[error](extractListPlasmidResult)( - IOE.Chain(fetchListPlasmidCollection)( - IOE.Chain(buildListPlasmidFilterQuery)(ioe), - ), - )() - if E.IsLeft(res) { - return &models.PlasmidListWithCursor{}, E.ToError(res) + result := F.Pipe6( + IOE.Of[error](listPlasmidsContext{ + client: qrs.GetStockClient(registry.STOCK), + gctx: ctx, + cursor: cursor, + limit: limit, + filter: filter, + }), + IOE.Map[error](computeListPlasmidParams), + IOE.Chain(buildListPlasmidFilterQuery), + IOE.Chain(fetchListPlasmidCollection), + IOE.Map[error](extractListPlasmidResult), + toEither[error, *models.PlasmidListWithCursor], + E.Fold(onPlasmidListError, onPlasmidListSuccess), + ) + + if result.F1 != nil { + errorutils.AddGQLError(ctx, result.F1) + qrs.Logger.Error(result.F1) + return result.F2, result.F1 } - val, _ := E.Unwrap(res) - return val, nil + + qrs.Logger.Debugf( + "successfully retrieved list of %v plasmids", + result.F2.TotalCount, + ) + return result.F2, nil } //nolint:dupl diff --git a/internal/graphql/resolver/stock_plasmid_fp.go b/internal/graphql/resolver/stock_plasmid_fp.go index 5ecd8b19..faa9c48b 100644 --- a/internal/graphql/resolver/stock_plasmid_fp.go +++ b/internal/graphql/resolver/stock_plasmid_fp.go @@ -9,10 +9,11 @@ import ( F "github.com/IBM/fp-go/v2/function" IOE "github.com/IBM/fp-go/v2/ioeither" O "github.com/IBM/fp-go/v2/option" + Pa "github.com/IBM/fp-go/v2/pair" P "github.com/IBM/fp-go/v2/predicate" R "github.com/IBM/fp-go/v2/record" - Pa "github.com/IBM/fp-go/v2/pair" S "github.com/IBM/fp-go/v2/string" + T "github.com/IBM/fp-go/v2/tuple" "github.com/dictyBase/aphgrpc" pb "github.com/dictyBase/go-genproto/dictybaseapis/stock" "github.com/dictyBase/graphql-server/internal/graphql/models" @@ -122,27 +123,40 @@ var ( } ) +func toEither[ERR, A any](ioe IOE.IOEither[ERR, A]) E.Either[ERR, A] { + return ioe() +} + +func onPlasmidListError( + err error, +) T.Tuple2[error, *models.PlasmidListWithCursor] { + return T.MakeTuple2(err, &models.PlasmidListWithCursor{}) +} + +func onPlasmidListSuccess( + data *models.PlasmidListWithCursor, +) T.Tuple2[error, *models.PlasmidListWithCursor] { + return T.MakeTuple2[error](nil, data) +} + func buildListPlasmidFilterQuery( ctx listPlasmidsContext, ) IOE.IOEither[error, listPlasmidsContext] { - resolvedFilter := F.Pipe2( + return F.Pipe7( ctx.filter, O.FromNillable[models.PlasmidListFilter], - O.GetOrElse(F.Constant(&models.PlasmidListFilter{ + O.Map(func(filter *models.PlasmidListFilter) filterValidationPair { + return Pa.MakePair(ctx, filter) + }), + O.GetOrElse(F.Constant(Pa.MakePair(ctx, &models.PlasmidListFilter{ PlasmidType: models.PlasmidTypeAll, - })), - ) - return F.Pipe4( - Pa.MakePair(ctx, resolvedFilter), - E.FromPredicate( - isNilPlasmidIDFilter, - func(pair filterValidationPair) error { - return fmt.Errorf( - "plasmid list filter %v: id filter is not yet supported in stock query conversion", - Pa.Tail(pair), - ) - }, - ), + }))), + E.FromPredicate(isNilPlasmidIDFilter, func(pair filterValidationPair) error { + return fmt.Errorf( + "plasmid list filter %v: id filter is not yet supported in stock query conversion", + Pa.Tail(pair), + ) + }), E.Chain(checkNilPlasmidInStockFilter), E.Chain(validateAndBuildPlasmidFilter), IOE.FromEither[error, listPlasmidsContext], From adf9b8deb0d74a47763b63f3b8f8cb8e560c14a9 Mon Sep 17 00:00:00 2001 From: Siddhartha Basu Date: Tue, 21 Apr 2026 16:42:24 +0000 Subject: [PATCH 13/17] refactor(plasmid): unify filter validation into Either chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert plasmid ID nil check into an Either validator and chain it within the buildListPlasmidFilterQuery pipeline for better consistency. 💘 Generated with Crush Assisted-by: Google: Gemini 3 Flash Preview via Crush --- internal/graphql/resolver/stock_plasmid_fp.go | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/internal/graphql/resolver/stock_plasmid_fp.go b/internal/graphql/resolver/stock_plasmid_fp.go index faa9c48b..71ac448f 100644 --- a/internal/graphql/resolver/stock_plasmid_fp.go +++ b/internal/graphql/resolver/stock_plasmid_fp.go @@ -59,11 +59,19 @@ var ( O.Some[string], )) - isNilPlasmidIDFilter = F.Pipe1( - P.IsZero[*string](), - P.ContraMap(func(pair filterValidationPair) *string { - return Pa.Tail(pair).ID - }), + checkNilPlasmidIDFilter = E.FromPredicate( + F.Pipe1( + P.IsZero[*string](), + P.ContraMap(func(pair filterValidationPair) *string { + return Pa.Tail(pair).ID + }), + ), + func(pair filterValidationPair) error { + return fmt.Errorf( + "plasmid list filter %v: id filter is not yet supported in stock query conversion", + Pa.Tail(pair), + ) + }, ) checkNilPlasmidInStockFilter = E.FromPredicate( @@ -142,7 +150,7 @@ func onPlasmidListSuccess( func buildListPlasmidFilterQuery( ctx listPlasmidsContext, ) IOE.IOEither[error, listPlasmidsContext] { - return F.Pipe7( + return F.Pipe8( ctx.filter, O.FromNillable[models.PlasmidListFilter], O.Map(func(filter *models.PlasmidListFilter) filterValidationPair { @@ -151,12 +159,8 @@ func buildListPlasmidFilterQuery( O.GetOrElse(F.Constant(Pa.MakePair(ctx, &models.PlasmidListFilter{ PlasmidType: models.PlasmidTypeAll, }))), - E.FromPredicate(isNilPlasmidIDFilter, func(pair filterValidationPair) error { - return fmt.Errorf( - "plasmid list filter %v: id filter is not yet supported in stock query conversion", - Pa.Tail(pair), - ) - }), + E.Of[error, filterValidationPair], + E.Chain(checkNilPlasmidIDFilter), E.Chain(checkNilPlasmidInStockFilter), E.Chain(validateAndBuildPlasmidFilter), IOE.FromEither[error, listPlasmidsContext], From 2c9b62f732f061737343b6c38aa8cd8069294e0c Mon Sep 17 00:00:00 2001 From: Siddhartha Basu Date: Tue, 21 Apr 2026 17:08:23 +0000 Subject: [PATCH 14/17] refactor(plasmid): extract validateAndBuildPlasmidFilter to standalone function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move validateAndBuildPlasmidFilter out of the var block - Simplify plasmid type filter query to use direct tag matching 💘 Generated with Crush Assisted-by: Google: Gemini 3 Flash Preview via Crush --- internal/graphql/resolver/stock_plasmid_fp.go | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/internal/graphql/resolver/stock_plasmid_fp.go b/internal/graphql/resolver/stock_plasmid_fp.go index 71ac448f..be3c5693 100644 --- a/internal/graphql/resolver/stock_plasmid_fp.go +++ b/internal/graphql/resolver/stock_plasmid_fp.go @@ -109,28 +109,30 @@ var ( }, } } - - validateAndBuildPlasmidFilter = func(pair filterValidationPair) E.Either[error, listPlasmidsContext] { - filter := Pa.Tail(pair) - ctx := Pa.Head(pair) - return F.Pipe2( - filter.PlasmidType, - E.FromPredicate( - func(plasmidType models.PlasmidType) bool { - return plasmidType.IsValid() - }, - func(plasmidType models.PlasmidType) error { - return fmt.Errorf("invalid plasmid type %s", plasmidType.String()) - }, - ), - E.Map[error](func(plasmidType models.PlasmidType) listPlasmidsContext { - ctx.filterQuery = buildFilterQuery(filter, plasmidType) - return ctx - }), - ) - } ) +func validateAndBuildPlasmidFilter( + pair filterValidationPair, +) E.Either[error, listPlasmidsContext] { + filter := Pa.Tail(pair) + ctx := Pa.Head(pair) + return F.Pipe2( + filter.PlasmidType, + E.FromPredicate( + func(plasmidType models.PlasmidType) bool { + return plasmidType.IsValid() + }, + func(plasmidType models.PlasmidType) error { + return fmt.Errorf("invalid plasmid type %s", plasmidType.String()) + }, + ), + E.Map[error](func(plasmidType models.PlasmidType) listPlasmidsContext { + ctx.filterQuery = buildFilterQuery(filter, plasmidType) + return ctx + }), + ) +} + func toEither[ERR, A any](ioe IOE.IOEither[ERR, A]) E.Either[ERR, A] { return ioe() } @@ -226,13 +228,11 @@ func buildFilterQuery( ), R.Lookup[string](plasmidType)(map[models.PlasmidType]string{ models.PlasmidTypeRegular: fmt.Sprintf( - "ontology==%s;tag==%s", - registry.DictyPlasmidPropOntology, + "tag===%s", registry.RegularPlasmidTag, ), models.PlasmidTypeGoldenBraid: fmt.Sprintf( - "ontology==%s;tag==%s", - registry.DictyPlasmidPropOntology, + "tag===%s", registry.GoldenBraidPlasmidTag, ), }), From ad6bdb85014986c48b7c4637df37a00e68f4479a Mon Sep 17 00:00:00 2001 From: Siddhartha Basu Date: Tue, 21 Apr 2026 17:22:48 +0000 Subject: [PATCH 15/17] fix(plasmid): restore ontology in plasmid type filter query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore the ontology prefix and the equality operator (==) in the plasmid filter query to match stock service and test expectations. 💘 Generated with Crush Assisted-by: Google: Gemini 3 Flash Preview via Crush --- internal/graphql/resolver/stock_plasmid_fp.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/graphql/resolver/stock_plasmid_fp.go b/internal/graphql/resolver/stock_plasmid_fp.go index be3c5693..41334d1e 100644 --- a/internal/graphql/resolver/stock_plasmid_fp.go +++ b/internal/graphql/resolver/stock_plasmid_fp.go @@ -228,11 +228,13 @@ func buildFilterQuery( ), R.Lookup[string](plasmidType)(map[models.PlasmidType]string{ models.PlasmidTypeRegular: fmt.Sprintf( - "tag===%s", + "ontology==%s;tag==%s", + registry.DictyPlasmidPropOntology, registry.RegularPlasmidTag, ), models.PlasmidTypeGoldenBraid: fmt.Sprintf( - "tag===%s", + "ontology==%s;tag==%s", + registry.DictyPlasmidPropOntology, registry.GoldenBraidPlasmidTag, ), }), From 7133156b4198a634df5fe25bd2021438ca6f9fc4 Mon Sep 17 00:00:00 2001 From: Siddhartha Basu Date: Tue, 21 Apr 2026 17:23:44 +0000 Subject: [PATCH 16/17] feat(plasmid): use triple equals for exact match in filter queries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update plasmid type filter queries to use triple equals (===) for exact matching and adjust unit tests to reflect this change. 💘 Generated with Crush Assisted-by: Google: Gemini 3 Flash Preview via Crush --- internal/graphql/resolver/stock_plasmid_fp.go | 4 ++-- internal/graphql/resolver/stock_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/graphql/resolver/stock_plasmid_fp.go b/internal/graphql/resolver/stock_plasmid_fp.go index 41334d1e..d5445f1b 100644 --- a/internal/graphql/resolver/stock_plasmid_fp.go +++ b/internal/graphql/resolver/stock_plasmid_fp.go @@ -228,12 +228,12 @@ func buildFilterQuery( ), R.Lookup[string](plasmidType)(map[models.PlasmidType]string{ models.PlasmidTypeRegular: fmt.Sprintf( - "ontology==%s;tag==%s", + "ontology===%s;tag===%s", registry.DictyPlasmidPropOntology, registry.RegularPlasmidTag, ), models.PlasmidTypeGoldenBraid: fmt.Sprintf( - "ontology==%s;tag==%s", + "ontology===%s;tag===%s", registry.DictyPlasmidPropOntology, registry.GoldenBraidPlasmidTag, ), diff --git a/internal/graphql/resolver/stock_test.go b/internal/graphql/resolver/stock_test.go index c5640d89..08767ccc 100644 --- a/internal/graphql/resolver/stock_test.go +++ b/internal/graphql/resolver/stock_test.go @@ -640,7 +640,7 @@ func TestListPlasmidsRegularTypeFilter(t *testing.T) { mock.MatchedBy(func(params *pb.StockParameters) bool { return params.Cursor == 0 && params.Limit == 10 && - params.Filter == "ontology==dicty_plasmid_keyword;tag==vector" + params.Filter == "ontology===dicty_plasmid_keyword;tag===vector" }), ).Return(mocks.MockPlasmidCollection(), nil) @@ -671,7 +671,7 @@ func TestListPlasmidsGoldenBraidTypeFilter(t *testing.T) { mock.MatchedBy(func(params *pb.StockParameters) bool { return params.Cursor == 0 && params.Limit == 10 && - params.Filter == "ontology==dicty_plasmid_keyword;tag==GB vector" + params.Filter == "ontology===dicty_plasmid_keyword;tag===GB vector" }), ).Return(mocks.MockPlasmidCollection(), nil) From 5de59cbcea1d507727bcd6933c6ee4808b0ddc78 Mon Sep 17 00:00:00 2001 From: Siddhartha Basu Date: Tue, 21 Apr 2026 17:24:47 +0000 Subject: [PATCH 17/17] feat(plasmid): simplify filter query to use only tag with triple equals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove ontology prefix from plasmid type filters and use tag=== for exact matching. Update unit tests to match the simplified format. 💘 Generated with Crush Assisted-by: Google: Gemini 3 Flash Preview via Crush --- internal/graphql/resolver/stock_plasmid_fp.go | 6 ++---- internal/graphql/resolver/stock_test.go | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/internal/graphql/resolver/stock_plasmid_fp.go b/internal/graphql/resolver/stock_plasmid_fp.go index d5445f1b..be3c5693 100644 --- a/internal/graphql/resolver/stock_plasmid_fp.go +++ b/internal/graphql/resolver/stock_plasmid_fp.go @@ -228,13 +228,11 @@ func buildFilterQuery( ), R.Lookup[string](plasmidType)(map[models.PlasmidType]string{ models.PlasmidTypeRegular: fmt.Sprintf( - "ontology===%s;tag===%s", - registry.DictyPlasmidPropOntology, + "tag===%s", registry.RegularPlasmidTag, ), models.PlasmidTypeGoldenBraid: fmt.Sprintf( - "ontology===%s;tag===%s", - registry.DictyPlasmidPropOntology, + "tag===%s", registry.GoldenBraidPlasmidTag, ), }), diff --git a/internal/graphql/resolver/stock_test.go b/internal/graphql/resolver/stock_test.go index 08767ccc..d878a4e8 100644 --- a/internal/graphql/resolver/stock_test.go +++ b/internal/graphql/resolver/stock_test.go @@ -640,7 +640,7 @@ func TestListPlasmidsRegularTypeFilter(t *testing.T) { mock.MatchedBy(func(params *pb.StockParameters) bool { return params.Cursor == 0 && params.Limit == 10 && - params.Filter == "ontology===dicty_plasmid_keyword;tag===vector" + params.Filter == "tag===vector" }), ).Return(mocks.MockPlasmidCollection(), nil) @@ -671,7 +671,7 @@ func TestListPlasmidsGoldenBraidTypeFilter(t *testing.T) { mock.MatchedBy(func(params *pb.StockParameters) bool { return params.Cursor == 0 && params.Limit == 10 && - params.Filter == "ontology===dicty_plasmid_keyword;tag===GB vector" + params.Filter == "tag===GB vector" }), ).Return(mocks.MockPlasmidCollection(), nil)