better message formatting
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful

This commit is contained in:
Stephan D
2025-11-19 13:54:25 +01:00
parent 62956b06ca
commit 717dafc673
26 changed files with 202 additions and 56 deletions

View File

@@ -48,5 +48,5 @@ func CreateAuth(
}
return enforcer, manager, nil
}
return nil, nil, merrors.InvalidArgument("Unknown enforcer type: " + string(config.Driver))
return nil, nil, merrors.InvalidArgument("Unknown enforcer type: "+string(config.Driver), "config.driver")
}

View File

@@ -18,6 +18,6 @@ func stringToAction(actionStr string) (model.Action, error) {
case string(model.ActionDelete):
return model.ActionDelete, nil
default:
return "", merrors.InvalidArgument(fmt.Sprintf("invalid action: %s", actionStr))
return "", merrors.InvalidArgument(fmt.Sprintf("invalid action: %s", actionStr), "action")
}
}

View File

@@ -109,7 +109,7 @@ func PrepareConfig(logger mlogger.Logger, config *Config) (*EnforcerConfig, erro
if len(adapter.DatabaseName) == 0 {
logger.Error("Database name is not set")
return nil, merrors.InvalidArgument("database name must be provided")
return nil, merrors.InvalidArgument("database name must be provided", "adapter.databaseName")
}
path := getEnvValue(logger, "model_path", "model_path_env", config.ModelPath, config.ModelPathEnv)

View File

@@ -35,7 +35,7 @@ func NewRoleManager(logger mlogger.Logger, enforcer *CasbinEnforcer, rolePermiss
func (rm *RoleManager) validateObjectIDs(ids ...primitive.ObjectID) error {
for _, id := range ids {
if id.IsZero() {
return merrors.InvalidArgument("Object references cannot be zero")
return merrors.InvalidArgument("Object references cannot be zero", "objectRef")
}
}
return nil

View File

@@ -36,7 +36,7 @@ func NewRoleManager(logger mlogger.Logger, enforcer *Enforcer, rolePermissionRef
func (rm *RoleManager) validateObjectIDs(ids ...primitive.ObjectID) error {
for _, id := range ids {
if id.IsZero() {
return merrors.InvalidArgument("Object references cannot be zero")
return merrors.InvalidArgument("Object references cannot be zero", "objectRef")
}
}
return nil

View File

@@ -47,10 +47,10 @@ func (c *MongoConnection) Ping(ctx context.Context) error {
// ConnectMongo returns a low-level MongoDB connection without constructing repositories.
func ConnectMongo(logger mlogger.Logger, config *Config) (*MongoConnection, error) {
if config == nil {
return nil, merrors.InvalidArgument("database configuration is nil")
return nil, merrors.InvalidArgument("database configuration is nil", "config")
}
if config.Driver != Mongo {
return nil, merrors.InvalidArgument("unsupported database driver: " + string(config.Driver))
return nil, merrors.InvalidArgument("unsupported database driver: "+string(config.Driver), "config.driver")
}
client, _, settings, err := mongoimpl.ConnectClient(logger, config.Settings)

View File

@@ -37,5 +37,5 @@ func NewConnection(logger mlogger.Logger, config *Config) (Factory, error) {
if config.Driver == Mongo {
return mongoimpl.NewConnection(logger, config.Settings)
}
return nil, merrors.InvalidArgument("unknown database driver: " + string(config.Driver))
return nil, merrors.InvalidArgument("unknown database driver: "+string(config.Driver), "config.driver")
}

View File

@@ -10,7 +10,7 @@ import (
func (db *OrganizationDB) Create(ctx context.Context, _, _ primitive.ObjectID, org *model.Organization) error {
if org == nil {
return merrors.InvalidArgument("Organization object is nil")
return merrors.InvalidArgument("Organization object is nil", "organization")
}
org.SetID(primitive.NewObjectID())
// Organizaiton reference must be set to the same value as own organization reference

View File

@@ -18,7 +18,7 @@ func (db *RefreshTokenDB) Create(ctx context.Context, rt *model.RefreshToken) er
// First, try to find an existing token for this account/client/device combination
var existing model.RefreshToken
if rt.AccountRef == nil {
return merrors.InvalidArgument("Account reference must have a vaild value")
return merrors.InvalidArgument("Account reference must have a vaild value", "refreshToken.accountRef")
}
if err := db.FindOne(ctx, filterByAccount(*rt.AccountRef, &rt.SessionIdentifier), &existing); err != nil {
if errors.Is(err, merrors.ErrNoData) {

View File

@@ -15,7 +15,7 @@ func (r *MongoRepository) CreateIndex(def *ri.Definition) error {
return merrors.NoData("data collection is not set")
}
if len(def.Keys) == 0 {
return merrors.InvalidArgument("Index definition has no keys")
return merrors.InvalidArgument("Index definition has no keys", "index.keys")
}
// ----- build BSON keys --------------------------------------------------

View File

@@ -83,7 +83,7 @@ func (r *MongoRepository) findOneByFilterImp(ctx context.Context, filter bson.D,
func (r *MongoRepository) Get(ctx context.Context, id primitive.ObjectID, result storable.Storable) error {
if id.IsZero() {
return merrors.InvalidArgument("zero id provided while fetching " + result.Collection())
return merrors.InvalidArgument("zero id provided while fetching "+result.Collection(), "id")
}
return r.findOneByFilterImp(ctx, idFilter(id), fmt.Sprintf("%s with ID = %s not found", result.Collection(), id.Hex()), result)
}
@@ -134,7 +134,7 @@ func (r *MongoRepository) Update(ctx context.Context, obj storable.Storable) err
func (r *MongoRepository) Patch(ctx context.Context, id primitive.ObjectID, patch builder.Patch) error {
if id.IsZero() {
return merrors.InvalidArgument("zero id provided while patching")
return merrors.InvalidArgument("zero id provided while patching", "id")
}
_, err := r.collection.UpdateByID(ctx, id, patch.Build())
return err

View File

@@ -22,7 +22,7 @@ type TimeSeries struct {
func NewMongoTimeSeriesCollection(ctx context.Context, db *mongo.Database, tsOpts *tsoptions.Options) (*TimeSeries, error) {
if tsOpts == nil {
return nil, merrors.InvalidArgument("nil time-series options provided")
return nil, merrors.InvalidArgument("nil time-series options provided", "options")
}
// Configure time-series options
granularity := tsOpts.Granularity.String()

View File

@@ -3,6 +3,7 @@ package merrors
import (
"errors"
"fmt"
"strings"
"go.mongodb.org/mongo-driver/bson/primitive"
)
@@ -27,8 +28,8 @@ func Internal(msg string) error {
var ErrInvalidArg = errors.New("invalidArgError")
func InvalidArgument(msg string) error {
return fmt.Errorf("%w: %s", ErrInvalidArg, msg)
func InvalidArgument(msg string, argumentNames ...string) error {
return fmt.Errorf("%w: %s", ErrInvalidArg, invalidArgumentMessage(msg, argumentNames...))
}
var ErrDataConflict = errors.New("DataConflict")
@@ -64,8 +65,8 @@ func NoMessagingTopic(topic string) error {
return fmt.Errorf("%w: messaging topic '%s' not found", ErrNoMessagingTopic, topic)
}
func InvalidArgumentWrap(err error, msg string) error {
return wrapError(ErrInvalidArg, msg, err)
func InvalidArgumentWrap(err error, msg string, argumentNames ...string) error {
return wrapError(ErrInvalidArg, invalidArgumentMessage(msg, argumentNames...), err)
}
func InternalWrap(err error, msg string) error {
@@ -79,3 +80,23 @@ func wrapError(base error, msg string, err error) error {
}
return errors.Join(baseErr, err)
}
func invalidArgumentMessage(msg string, argumentNames ...string) string {
names := make([]string, 0, len(argumentNames))
for _, name := range argumentNames {
name = strings.TrimSpace(name)
if name == "" {
continue
}
names = append(names, fmt.Sprintf("%q", name))
}
if len(names) == 0 {
return msg
}
prefix := "broken argument"
if len(names) > 1 {
prefix = "broken arguments"
}
return fmt.Sprintf("%s %s: %s", prefix, strings.Join(names, ", "), msg)
}

View File

@@ -0,0 +1,47 @@
package merrors
import (
"errors"
"strings"
"testing"
)
func TestInvalidArgumentSupportsBrokenArgumentName(t *testing.T) {
t.Run("without argument name keeps old behavior", func(t *testing.T) {
err := InvalidArgument("value is missing")
expected := "invalidArgError: value is missing"
if err.Error() != expected {
t.Fatalf("unexpected error message: %s", err)
}
if !errors.Is(err, ErrInvalidArg) {
t.Fatalf("error should wrap ErrInvalidArg")
}
})
t.Run("single argument name", func(t *testing.T) {
err := InvalidArgument("value is missing", "bot_token_env")
expected := `invalidArgError: broken argument "bot_token_env": value is missing`
if err.Error() != expected {
t.Fatalf("unexpected error message: %s", err)
}
})
t.Run("multiple argument names", func(t *testing.T) {
err := InvalidArgument("value mismatch", "bot_token_env", "chat_id_env", " ")
expected := `invalidArgError: broken arguments "bot_token_env", "chat_id_env": value mismatch`
if err.Error() != expected {
t.Fatalf("unexpected error message: %s", err)
}
})
}
func TestInvalidArgumentWrapSupportsBrokenArgumentName(t *testing.T) {
base := errors.New("root cause")
err := InvalidArgumentWrap(base, "value is missing", "bot_token_env")
if !strings.Contains(err.Error(), `invalidArgError: broken argument "bot_token_env": value is missing`) {
t.Fatalf("wrapped error should include broken argument name: %s", err)
}
if !errors.Is(err, ErrInvalidArg) || !errors.Is(err, base) {
t.Fatalf("wrapped error should preserve all error layers")
}
}

View File

@@ -76,7 +76,7 @@ func (b *MessageBroker) Unsubscribe(event model.NotificationEvent, subChan <-cha
func NewInProcessBroker(logger mlogger.Logger, bufferSize int) (*MessageBroker, error) {
if bufferSize < 1 {
return nil, merrors.InvalidArgument(fmt.Sprintf("Invelid buffer size %d. It must be greater than 1", bufferSize))
return nil, merrors.InvalidArgument(fmt.Sprintf("Invelid buffer size %d. It must be greater than 1", bufferSize), "bufferSize")
}
logger.Info("Created in-process logger", zap.Int("buffer_size", bufferSize))
return &MessageBroker{

View File

@@ -39,7 +39,7 @@ func loadEnv(settings *nc.Settings, l *zap.Logger) (*envConfig, error) {
return v, nil
}
l.Error(fmt.Sprintf("NATS %s not found in environment", label), zap.String("env_var", key))
return "", merrors.InvalidArgument(fmt.Sprintf("NATS %s not found in environment variable: %s", label, key))
return "", merrors.InvalidArgument(fmt.Sprintf("NATS %s not found in environment variable: %s", label, key), key)
}
user, err := get(settings.UsernameEnv, "user name")
@@ -65,7 +65,7 @@ func loadEnv(settings *nc.Settings, l *zap.Logger) (*envConfig, error) {
port, err := strconv.Atoi(portStr)
if err != nil || port <= 0 || port > 65535 {
l.Error("Invalid NATS port value", zap.String("port", portStr))
return nil, merrors.InvalidArgument("Invalid NATS port: " + portStr)
return nil, merrors.InvalidArgument("Invalid NATS port: "+portStr, settings.PortEnv)
}
return &envConfig{

View File

@@ -17,7 +17,7 @@ type DemoRequestNotification struct {
func (drn *DemoRequestNotification) Serialize() ([]byte, error) {
if drn.request == nil {
return nil, merrors.InvalidArgument("demo request payload is empty")
return nil, merrors.InvalidArgument("demo request payload is empty", "request")
}
msg := gmessaging.DemoRequestEvent{
Name: drn.request.Name,

View File

@@ -32,22 +32,22 @@ func (dr *DemoRequest) Normalize() {
// Validate ensures that all required fields are present.
func (dr *DemoRequest) Validate() error {
if dr == nil {
return merrors.InvalidArgument("request payload is empty")
return merrors.InvalidArgument("request payload is empty", "request")
}
if dr.Name == "" {
return merrors.InvalidArgument("name must not be empty")
return merrors.InvalidArgument("name must not be empty", "request.name")
}
if dr.OrganizationName == "" {
return merrors.InvalidArgument("organization name must not be empty")
return merrors.InvalidArgument("organization name must not be empty", "request.organizationName")
}
if dr.Phone == "" {
return merrors.InvalidArgument("phone must not be empty")
return merrors.InvalidArgument("phone must not be empty", "request.phone")
}
if dr.WorkEmail == "" {
return merrors.InvalidArgument("work email must not be empty")
return merrors.InvalidArgument("work email must not be empty", "request.workEmail")
}
if dr.PayoutVolume == "" {
return merrors.InvalidArgument("payout volume must not be empty")
return merrors.InvalidArgument("payout volume must not be empty", "request.payoutVolume")
}
return nil
}

View File

@@ -17,12 +17,12 @@ func IndexableRefs(items []model.IndexableRef, oldIndex, newIndex int) ([]model.
}
}
if targetIndex == -1 {
return nil, merrors.InvalidArgument("Item not found at specified index")
return nil, merrors.InvalidArgument("Item not found at specified index", "oldIndex")
}
// Validate new index bounds
if newIndex < 0 || newIndex >= len(items) {
return nil, merrors.InvalidArgument("Invalid new index for reorder")
return nil, merrors.InvalidArgument("Invalid new index for reorder", "newIndex")
}
// Remove the item from its current position

View File

@@ -53,13 +53,13 @@ type App[T any] struct {
func NewApp[T any](logger mlogger.Logger, name string, config *Config, debug bool, repoFactory RepositoryFactory[T], serviceFactory ServiceFactory[T], opts ...Option[T]) (*App[T], error) {
if logger == nil {
return nil, merrors.InvalidArgument("nil logger supplied")
return nil, merrors.InvalidArgument("nil logger supplied", "logger")
}
if config == nil {
return nil, merrors.InvalidArgument("nil config supplied")
return nil, merrors.InvalidArgument("nil config supplied", "config")
}
if serviceFactory == nil {
return nil, merrors.InvalidArgument("nil service factory supplied")
return nil, merrors.InvalidArgument("nil service factory supplied", "serviceFactory")
}
app := &App[T]{