Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions MODERNIZATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,17 @@ Replaced `reflect.NewAt(sType, unsafe.Pointer(nil))` with `reflect.Zero(reflect.

---

## 9. Delete the `cmp/errors.go` Package
## ~~9. Delete the `cmp/errors.go` Package and Add Structured Error Types~~ (DONE)

**File:** `cmp/errors.go`
Deleted `cmp/errors.go`, which compared errors by string — a fragile anti-pattern. Replaced all inline `errors.New`/`fmt.Errorf` calls with five typed error structs grouped by class:

This package contains a single function that compares errors by their `.Error()` string — a fragile anti-pattern. With proper use of sentinel errors, `errors.Is`, and `errors.As`, this package becomes unnecessary. The tests that use it should be updated to compare errors structurally.
- **`ValidationError{Kind ValidationErrorKind}`** — struct/function signature validation failures in `Build`/`ShouldBuild`/`BuildFunction`
- **`QueryError{Kind QueryErrorKind, ...}`** — query lookup and parameter processing failures (with `Name`, `Query`, `Position`, `TypeKind` fields as applicable)
- **`IdentifierError{Kind IdentifierErrorKind, Identifier string}`** — identifier syntax validation failures in query parameters
- **`ExtractError{Kind ExtractErrorKind, Value string, Err error}`** — path-extraction failures in `mapper/extract.go`; implements `Unwrap()` to surface wrapped strconv errors for `InvalidIndex`
- **`AssignError{Kind AssignErrorKind, ...}`** — value-assignment failures in `mapper/mapper.go`

Each type's zero-value `Kind` (the `AnyXxx` constant) acts as a wildcard: `errors.Is(err, ValidationError{})` matches any `ValidationError`; `errors.Is(err, ValidationError{Kind: NotPointer})` matches exactly. All tests updated to use `errors.Is`/`errors.As` instead of string comparison.

---

Expand Down Expand Up @@ -371,7 +377,7 @@ If `Build` returns an error, `productDao` will have nil function fields. Subsequ
**Lower priority (cleanup):**
- ~~#5 — `strings.ReplaceAll`~~ *(DONE)*
- ~~#6 — `strings.Builder`~~ *(DONE)*
- #9 — Delete `cmp/errors.go`
- ~~#9 — Delete `cmp/errors.go` and add structured error types~~ *(DONE)*
- #10 — Deprecation annotations
- #12 — Testing improvements
- #13 — Reduce duplication
Expand Down
26 changes: 13 additions & 13 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func buildFixedQueryAndParamOrder(ctx context.Context, query string, nameOrderMa
if inVar {
if curVar.Len() == 0 {
//error! must have a something
return nil, nil, fmt.Errorf("empty variable declaration at position %d", k)
return nil, nil, QueryError{Kind: EmptyVariable, Position: k}
}
curVarS := curVar.String()
id, err := validIdentifier(ctx, curVarS)
Expand All @@ -109,13 +109,13 @@ func buildFixedQueryAndParamOrder(ctx context.Context, query string, nameOrderMa
paramType := funcType.In(paramPos)
if len(path) > 1 {
if paramType == nil {
return nil, nil, fmt.Errorf("query Parameter %s has a path, but the incoming parameter is nil", paramName)
return nil, nil, QueryError{Kind: NilParameterPath, Name: paramName}
}
switch paramType.Kind() {
case reflect.Map, reflect.Struct:
//do nothing
default:
return nil, nil, fmt.Errorf("query Parameter %s has a path, but the incoming parameter is not a map or a struct it is %s", paramName, paramType.Kind())
return nil, nil, QueryError{Kind: InvalidParameterType, Name: paramName, TypeKind: paramType.Kind().String()}
}
}
pathType, err := mapper.ExtractType(ctx, paramType, path)
Expand All @@ -131,7 +131,7 @@ func buildFixedQueryAndParamOrder(ctx context.Context, query string, nameOrderMa
}
paramOrder = append(paramOrder, paramInfo{id, paramPos, isSlice})
} else {
return nil, nil, fmt.Errorf("query Parameter %s cannot be found in the incoming parameters", paramName)
return nil, nil, QueryError{Kind: ParameterNotFound, Name: paramName}
}

inVar = false
Expand All @@ -148,7 +148,7 @@ func buildFixedQueryAndParamOrder(ctx context.Context, query string, nameOrderMa
}
}
if inVar {
return nil, nil, fmt.Errorf("missing a closing : somewhere: %s", query)
return nil, nil, QueryError{Kind: MissingClosingColon, Query: query}
}

queryString := out.String()
Expand Down Expand Up @@ -232,7 +232,7 @@ func addSlice(sliceName string) string {

func validIdentifier(ctx context.Context, curVar string) (string, error) {
if strings.Contains(curVar, ";") {
return "", fmt.Errorf("; is not allowed in an identifier: %s", curVar)
return "", IdentifierError{Kind: SemicolonInIdentifier, Identifier: curVar}
}
curVarB := []byte(curVar)

Expand All @@ -253,7 +253,7 @@ loop:
switch tok {
case token.EOF:
if first || lastPeriod {
return "", fmt.Errorf("identifiers cannot be empty or end with a .: %s", curVar)
return "", IdentifierError{Kind: EmptyOrTrailingDotIdentifier, Identifier: curVar}
}
break loop
case token.SEMICOLON:
Expand All @@ -262,15 +262,15 @@ loop:
continue
case token.IDENT:
if !first && !lastPeriod && !lastFloat {
return "", fmt.Errorf(". missing between parts of an identifier: %s", curVar)
return "", IdentifierError{Kind: MissingDotInIdentifier, Identifier: curVar}
}
first = false
lastPeriod = false
lastFloat = false
identifier += lit
case token.PERIOD:
if first || lastPeriod {
return "", fmt.Errorf("identifier cannot start with . or have two . in a row: %s", curVar)
return "", IdentifierError{Kind: LeadingOrDoubleDotIdentifier, Identifier: curVar}
}
lastPeriod = true
identifier += "."
Expand All @@ -282,10 +282,10 @@ loop:
first = false
continue
}
return "", fmt.Errorf("invalid character found in identifier: %s", curVar)
return "", IdentifierError{Kind: InvalidCharacterInIdentifier, Identifier: curVar}
case token.INT:
if !dollar || first {
return "", fmt.Errorf("invalid character found in identifier: %s", curVar)
return "", IdentifierError{Kind: InvalidCharacterInIdentifier, Identifier: curVar}
}
identifier += lit
if dollar {
Expand All @@ -299,7 +299,7 @@ loop:
// returns .0 as the lit value
//Only valid for $ notation and array/slice references.
if first {
return "", fmt.Errorf("invalid character found in identifier: %s", curVar)
return "", IdentifierError{Kind: InvalidCharacterInIdentifier, Identifier: curVar}
}
identifier += lit
if dollar {
Expand All @@ -310,7 +310,7 @@ loop:
lastPeriod = true
}
default:
return "", fmt.Errorf("invalid character found in identifier: %s", curVar)
return "", IdentifierError{Kind: InvalidCharacterInIdentifier, Identifier: curVar}
}
}
return identifier, nil
Expand Down
11 changes: 0 additions & 11 deletions cmp/errors.go

This file was deleted.

166 changes: 166 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package proteus

import "fmt"

// ValidationErrorKind identifies the specific validation failure.
// The zero value (AnyValidation) acts as a wildcard for errors.Is matching.
type ValidationErrorKind int

const (
AnyValidation ValidationErrorKind = iota
NotPointer // "not a pointer"
NotPointerToStruct // "not a pointer to struct"
NotPointerToFunc // "not a pointer to func"
NeedExecutorOrQuerier // "need to supply an Executor or Querier parameter"
InvalidFirstParam // "first parameter must be of type context.Context, Executor, or Querier"
ChannelInputParam // "no input parameter can be a channel"
TooManyReturnValues // "must return 0, 1, or 2 values"
SecondReturnNotError // "2nd output parameter must be of type error"
FirstReturnIsChannel // "1st output parameter cannot be a channel"
ExecutorReturnType // "the 1st output parameter of an Executor must be int64 or sql.Result"
SQLResultWithQuerier // "output parameters of type sql.Result must be combined with Executor"
RowsMustBeNonNil // "rows must be non-nil"
NoValuesFromQuery // "no values returned from query"
ShouldNeverGetHere // "should never get here"
)

var validationMessages = map[ValidationErrorKind]string{
NotPointer: "not a pointer",
NotPointerToStruct: "not a pointer to struct",
NotPointerToFunc: "not a pointer to func",
NeedExecutorOrQuerier: "need to supply an Executor or Querier parameter",
InvalidFirstParam: "first parameter must be of type context.Context, Executor, or Querier",
ChannelInputParam: "no input parameter can be a channel",
TooManyReturnValues: "must return 0, 1, or 2 values",
SecondReturnNotError: "2nd output parameter must be of type error",
FirstReturnIsChannel: "1st output parameter cannot be a channel",
ExecutorReturnType: "the 1st output parameter of an Executor must be int64 or sql.Result",
SQLResultWithQuerier: "output parameters of type sql.Result must be combined with Executor",
RowsMustBeNonNil: "rows must be non-nil",
NoValuesFromQuery: "no values returned from query",
ShouldNeverGetHere: "should never get here",
}

// ValidationError is returned when a struct, function signature, or type passed
// to Build/ShouldBuild/BuildFunction fails validation, or when a runtime
// invariant is violated.
type ValidationError struct {
Kind ValidationErrorKind
}

func (e ValidationError) Error() string {
if msg, ok := validationMessages[e.Kind]; ok {
return msg
}
return "unknown validation error"
}

// Is matches any ValidationError when target has AnyValidation kind,
// or matches the exact kind otherwise.
func (e ValidationError) Is(target error) bool {
t, ok := target.(ValidationError)
if !ok {
return false
}
return t.Kind == AnyValidation || e.Kind == t.Kind
}

// QueryErrorKind identifies the specific query or parameter processing failure.
// The zero value (AnyQuery) acts as a wildcard for errors.Is matching.
type QueryErrorKind int

const (
AnyQuery QueryErrorKind = iota
QueryNotFound // Name: the missing query name
MissingClosingColon // Query: the full query string
EmptyVariable // Position: byte offset of the empty ::<var>
ParameterNotFound // Name: the parameter name
NilParameterPath // Name: the parameter name
InvalidParameterType // Name: the parameter name; TypeKind: the actual kind
)

// QueryError is returned when a query string or its parameters cannot be
// processed (missing query, bad syntax, unknown parameter).
type QueryError struct {
Kind QueryErrorKind
Name string // query or parameter name
Query string // full query string (MissingClosingColon)
Position int // byte offset (EmptyVariable)
TypeKind string // reflect.Kind string (InvalidParameterType)
}

func (e QueryError) Error() string {
switch e.Kind {
case QueryNotFound:
return fmt.Sprintf("no query found for name %s", e.Name)
case MissingClosingColon:
return fmt.Sprintf("missing a closing : somewhere: %s", e.Query)
case EmptyVariable:
return fmt.Sprintf("empty variable declaration at position %d", e.Position)
case ParameterNotFound:
return fmt.Sprintf("query parameter %s cannot be found in the incoming parameters", e.Name)
case NilParameterPath:
return fmt.Sprintf("query parameter %s has a path, but the incoming parameter is nil", e.Name)
case InvalidParameterType:
return fmt.Sprintf("query parameter %s has a path, but the incoming parameter is not a map or a struct it is %s", e.Name, e.TypeKind)
default:
return "unknown query error"
}
}

// Is matches any QueryError when target has AnyQuery kind,
// or matches the exact kind otherwise.
func (e QueryError) Is(target error) bool {
t, ok := target.(QueryError)
if !ok {
return false
}
return t.Kind == AnyQuery || e.Kind == t.Kind
}

// IdentifierErrorKind identifies the specific identifier parsing failure.
// The zero value (AnyIdentifier) acts as a wildcard for errors.Is matching.
type IdentifierErrorKind int

const (
AnyIdentifier IdentifierErrorKind = iota
SemicolonInIdentifier // "; is not allowed in an identifier"
EmptyOrTrailingDotIdentifier // "identifiers cannot be empty or end with a ."
MissingDotInIdentifier // ". missing between parts of an identifier"
LeadingOrDoubleDotIdentifier // "identifier cannot start with . or have two . in a row"
InvalidCharacterInIdentifier // "invalid character found in identifier"
)

// IdentifierError is returned when an identifier in a query parameter fails
// syntax validation.
type IdentifierError struct {
Kind IdentifierErrorKind
Identifier string
}

func (e IdentifierError) Error() string {
switch e.Kind {
case SemicolonInIdentifier:
return fmt.Sprintf("; is not allowed in an identifier: %s", e.Identifier)
case EmptyOrTrailingDotIdentifier:
return fmt.Sprintf("identifiers cannot be empty or end with a .: %s", e.Identifier)
case MissingDotInIdentifier:
return fmt.Sprintf(". missing between parts of an identifier: %s", e.Identifier)
case LeadingOrDoubleDotIdentifier:
return fmt.Sprintf("identifier cannot start with . or have two . in a row: %s", e.Identifier)
case InvalidCharacterInIdentifier:
return fmt.Sprintf("invalid character found in identifier: %s", e.Identifier)
default:
return "unknown identifier error"
}
}

// Is matches any IdentifierError when target has AnyIdentifier kind,
// or matches the exact kind otherwise.
func (e IdentifierError) Is(target error) bool {
t, ok := target.(IdentifierError)
if !ok {
return false
}
return t.Kind == AnyIdentifier || e.Kind == t.Kind
}
Loading
Loading