diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c6210a2 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +.ONESHELL: + +generate: generate-proto + +generate-proto: + cd ./schemas/proto + buf generate diff --git a/cmd/zen/main.go b/cmd/zen/main.go index fb56211..866b01b 100644 --- a/cmd/zen/main.go +++ b/cmd/zen/main.go @@ -1,4 +1,8 @@ package main +import "gitlab.bareksa.com/backend/zen/cmd/zen/serve" + // This is the main entry point for CLI interface. -func main() {} +func main() { + serve.Serve() +} diff --git a/cmd/zen/serve/serve.go b/cmd/zen/serve/serve.go new file mode 100644 index 0000000..3659286 --- /dev/null +++ b/cmd/zen/serve/serve.go @@ -0,0 +1,33 @@ +package serve + +import ( + "log/slog" + "net/http" + + "connectrpc.com/grpcreflect" + "gitlab.bareksa.com/backend/zen/internal/gen/proto/notify/v1/notifyv1connect" + "gitlab.bareksa.com/backend/zen/internal/rpchandler" + + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +func Serve() { + mux := http.NewServeMux() + reflector := grpcreflect.NewStaticReflector( + notifyv1connect.NotifyServiceName, + ) + + mux.Handle(grpcreflect.NewHandlerV1(reflector)) + mux.Handle(grpcreflect.NewHandlerV1Alpha(reflector)) + mux.Handle(notifyv1connect.NewNotifyServiceHandler(rpchandler.NotifyServiceHandler{})) + + slog.Info("Starting server on :8080") + err := http.ListenAndServe( + ":8080", + h2c.NewHandler(mux, &http2.Server{}), + ) + if err != nil { + panic(err) + } +} diff --git a/core/zerr/error.go b/core/zerr/error.go new file mode 100644 index 0000000..fd086a9 --- /dev/null +++ b/core/zerr/error.go @@ -0,0 +1,64 @@ +package zerr + +import ( + "context" + "log/slog" + "time" + + "connectrpc.com/connect" + "gitlab.bareksa.com/backend/zen/core/zoptions" +) + +type Error interface { + error + // LogValue returns log fields to be consumed by logger. + LogValue() slog.Value + + // Code sets error code. + Code(code connect.Code) Error + GetCode() connect.Code + + // Message sets error message. It uses fmt.Sprintf to format the message. + Message(msg string, args ...any) Error + GetMessage() string + + PublicMessage(msg string, args ...any) Error + GetPublicMessage() string + + Caller(pc uintptr) Error + GetCaller() uintptr + + // Details sets error details. It's a key-value pair where the odd index is the key and the even index is the value. + // + // Invalid key-value pair number and format will cause the misplaced value to be paired up with "!BADKEY". + // + // Example: + // + // err.Details( + // "key1", 12345, + // "key2", float64(12345.67), + // ) + Details(fields ...any) Error + GetDetails() []any + + // Time sets the error time. + // + // Time is already set to current when the error is created or wrapped, + // so this method is only useful when you want to set a different time. + Time(t time.Time) Error + GetTime() time.Time + + // ID sets the error ID. + // + // ID is used to identify the error. Used by Zen to consider if an error is the same as another. + // + // Current implementation + ID(msg string, args ...any) Error + GetID() string + + Log(ctx context.Context) Error + Notify(ctx context.Context, opts ...zoptions.NotifyOption) Error + + // Sequence returns the state of the error sequence. + Sequence() *Sequence +} diff --git a/core/zerr/implementation.go b/core/zerr/implementation.go new file mode 100644 index 0000000..9a75d95 --- /dev/null +++ b/core/zerr/implementation.go @@ -0,0 +1,212 @@ +package zerr + +import ( + "context" + "fmt" + "log/slog" + "strconv" + "strings" + "time" + + "connectrpc.com/connect" + "gitlab.bareksa.com/backend/zen/core/zoptions" +) + +type Err struct { + message string + publicMessage string + code connect.Code + errs []error + caller uintptr + details []any + time time.Time + id string + logger Logger + notifier Notifier + sequence *Sequence +} + +func (mu *Err) Error() string { + if len(mu.errs) == 0 { + return "[nil]" + } + s := strings.Builder{} + if len(mu.errs) == 1 { + str := getNestedErrorString(mu.errs[0]) + str, found := strings.CutPrefix(str, mu.message) + if found { + str, _ = strings.CutPrefix(str, ": ") + } + s.WriteString(mu.message) + s.WriteString(": ") + s.WriteString(str) + return s.String() + } + s.WriteString(mu.message) + for i, e := range mu.errs { + if i > 0 { + s.WriteString("\n") + } + s.WriteString(strconv.Itoa(i + 1)) + s.WriteString(". ") + s.WriteString(getNestedErrorString(e)) + } + return s.String() +} + +func getNestedErrorString(err error) string { + if err == nil { + return "[nil]" + } + s := strings.Builder{} + if e, ok := err.(interface{ Unwrap() error }); ok { + current := err.Error() + if err := e.Unwrap(); err != nil { + next := getNestedErrorString(err) + hasPrefix := strings.HasPrefix(next, current) + if !hasPrefix { + s.WriteString(current) + s.WriteString(": ") + } + s.WriteString(next) + } + } + + if errs, ok := err.(interface{ Unwrap() []error }); ok { + for i, e := range errs.Unwrap() { + if i > 0 { + s.WriteString("\n") + } + s.WriteString(strconv.Itoa(i + 1)) + s.WriteString(". ") + s.WriteString(getNestedErrorString(e)) + } + } + + if s.Len() == 0 { + s.WriteString(err.Error()) + } + + return s.String() +} + +// LogValue returns log fields to be consumed by logger. +func (mu *Err) LogValue() slog.Value { + attrs := make([]slog.Attr, 0, len(mu.errs)) + i := 0 + for _, err := range mu.errs { + if err == nil { + continue + } + i++ + attrs = append(attrs, slog.Attr{ + Key: strconv.Itoa(i), + Value: errorValuer{err}.LogValue(), + }) + } + return slog.GroupValue(attrs...) +} + +// Code sets error code. Ignored in Multi. +func (mu *Err) Code(code connect.Code) Error { + mu.code = code + return mu +} + +// GetCode Always returns 0 in Multi. +func (mu *Err) GetCode() connect.Code { + return mu.code +} + +// Message sets error message. It uses fmt.Sprintf to format the message. +func (mu *Err) Message(msg string, args ...any) Error { + mu.message = fmt.Sprintf(msg, args...) + return mu +} + +func (mu *Err) GetMessage() string { + return mu.message +} + +func (mu *Err) PublicMessage(msg string, args ...any) Error { + mu.publicMessage = fmt.Sprintf(msg, args...) + return mu +} + +func (mu *Err) GetPublicMessage() string { + return mu.publicMessage +} + +func (mu *Err) Caller(pc uintptr) Error { + mu.caller = pc + return mu +} + +func (mu *Err) GetCaller() uintptr { + return mu.caller +} + +// Details sets error details. It's a key-value pair where the odd index is the key and the even index is the value. +// +// Invalid key-value pair number and format will cause the misplaced value to be paired up with "!BADKEY". +// +// Example: +// +// err.Details( +// "key1", 12345, +// "key2", float64(12345.67), +// ) +func (mu *Err) Details(fields ...any) Error { + mu.details = fields + return mu +} + +func (mu *Err) GetDetails() []any { + return mu.details +} + +// Time sets the error time. +// +// Time is already set to current when the error is created or wrapped, +// so this method is only useful when you want to set a different time. +func (mu *Err) Time(t time.Time) Error { + mu.time = t + return mu +} + +func (mu *Err) GetTime() time.Time { + return mu.time +} + +// ID sets the error ID. +// +// ID is used to identify the error. Used by Zen to consider if an error is the same as another. +// +// Current implementation +func (mu *Err) ID(msg string, args ...any) Error { + mu.id = fmt.Sprintf(msg, args...) + return mu +} + +func (mu *Err) GetID() string { + return mu.id +} + +func (mu *Err) Log(ctx context.Context) Error { + mu.logger.Log(ctx, mu) + return mu +} + +func (mu *Err) Notify(ctx context.Context, opts ...zoptions.NotifyOption) Error { + mu.notifier.Notify(ctx, mu, opts...) + return mu +} + +// Sequence returns the state of the error sequence. +func (mu *Err) Sequence() *Sequence { + return mu.sequence +} + +func (mu *Err) Unwrap() []error { + return mu.errs +} diff --git a/core/zerr/sequence.go b/core/zerr/sequence.go new file mode 100644 index 0000000..d6d4c38 --- /dev/null +++ b/core/zerr/sequence.go @@ -0,0 +1,98 @@ +package zerr + +import "iter" + +// Sequence is the implementation of zerr.Sequence. +// +// The implementation is reversed linked list. +// +// New Sequence value will be added to the start of the list. +type Sequence struct { + prev *Sequence + next *Sequence + err Error + index int +} + +// Set prepends the next Sequence to the current Sequence. +func (se *Sequence) Set(next *Sequence) { + next.next = se + se.prev = next + se.index = next.index + 1 + if next.next != nil { + next.next.Set(se) + } +} + +// Outer returns the error that wraps this Error. +// Return nil if this Error is not wrapped +// or wrapping outer is not a zerr.Error. +func (se *Sequence) Outer() Error { + if se.prev != nil { + return se.prev.err + } + return nil +} + +// Inner returns the inner zerr.Error in the sequence. +// +// if there is no inner zerr.Error, it returns nil. +// +// if the inner error is not a zerr.Error, this returns nil. +func (se *Sequence) Inner() Error { + if se.next != nil { + return se.next.err + } + return nil +} + +// Index returns the index of this Error in the sequence. +func (se *Sequence) Index() int { + return se.index +} + +// Root returns the outermost Error in the sequence. +// +// Returns self if there is no outermost. +func (se *Sequence) Root() *Sequence { + root := se + for root.prev != nil { + root = root.prev + } + return root +} + +func (se *Sequence) IsRoot() bool { + return se.prev == nil +} + +// Iter returns an iterator to traverse the sequence. +// +// The iterator will start from outermost zerr.Error (root) to the last zerr.Error +// in depth-first order. +// +// Iterator will skip any nil values in the sequence. +// +// Iterator does not guarantee idempotent behavior. +// Next call to iterator may start from different root +// because the parent in the tree may be wrapped +// by another zerr.Error. +// +// Example: +// +// for _, e := range err.Sequence().Iter() { +// // e is zerr.Error +// // do something with e +// } +func (se *Sequence) Iter() iter.Seq[Error] { + root := se.Root() + return iter.Seq[Error](func(yield func(V Error) bool) { + for next := root; next != nil; next = next.next { + if next.err != nil { + if !yield(next.err) { + return + } + } + } + }) +} diff --git a/core/zerr/valuer.go b/core/zerr/valuer.go new file mode 100644 index 0000000..14d7314 --- /dev/null +++ b/core/zerr/valuer.go @@ -0,0 +1,31 @@ +package zerr + +import ( + "log/slog" + "reflect" +) + +type errorValuer struct { + error +} + +func (ev errorValuer) LogValue() slog.Value { + if ev.error == nil { + return slog.AnyValue(nil) + } + if lv, ok := ev.error.(slog.LogValuer); ok { + return lv.LogValue() + } + if unwrap, ok := ev.error.(interface{ Unwrap() []error }); ok { + return (&Err{errs: unwrap.Unwrap()}).LogValue() + } + attrs := make([]slog.Attr, 0, 3) + typ := reflect.TypeOf(ev.error).String() + attrs = append(attrs, slog.String("type", typ)) + attrs = append(attrs, slog.String("error", ev.error.Error())) + if typ == "*errors.errorString" { + return slog.GroupValue(attrs...) + } + attrs = append(attrs, slog.Any("details", ev.error)) + return slog.GroupValue(attrs...) +} diff --git a/core/zerr/wrap.go b/core/zerr/wrap.go new file mode 100644 index 0000000..d9306f2 --- /dev/null +++ b/core/zerr/wrap.go @@ -0,0 +1,41 @@ +package zerr + +import ( + "context" + "time" + + "gitlab.bareksa.com/backend/zen/core/zoptions" +) + +type WrapInitialInput struct { + Error error + Message string + PC uintptr + Time time.Time + Logger Logger + Notifier Notifier +} + +type Wrapper interface { + // Wrap creates a zerr.Error with inputs + // generated by global entry points. + Wrap(input WrapInitialInput) Error +} + +type Logger interface { + Log(ctx context.Context, err Error) +} + +type Notifier interface { + Notify(ctx context.Context, err Error, opts ...zoptions.NotifyOption) +} + +type WrapperFunc func(input WrapInitialInput) Error + +func (wr WrapperFunc) Wrap(input WrapInitialInput) Error { + return wr(input) +} + +var DefaultWrapper Wrapper = WrapperFunc(func(input WrapInitialInput) Error { + panic("not implemented") +}) diff --git a/core/zlog/zlog.go b/core/zlog/zlog.go new file mode 100644 index 0000000..ade69da --- /dev/null +++ b/core/zlog/zlog.go @@ -0,0 +1,7 @@ +package zlog + +import notifyv1 "gitlab.bareksa.com/backend/zen/internal/gen/proto/notify/v1" + +func log() { + notifyv1.Payload +} diff --git a/zcore/zoptions/notify.go b/core/zoptions/notify.go similarity index 100% rename from zcore/zoptions/notify.go rename to core/zoptions/notify.go diff --git a/go.mod b/go.mod index 000d784..e1dd7b6 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,12 @@ module gitlab.bareksa.com/backend/zen -go 1.22.5 +go 1.23 + +require ( + connectrpc.com/connect v1.16.2 + connectrpc.com/grpcreflect v1.2.0 + golang.org/x/net v0.23.0 + google.golang.org/protobuf v1.34.2 +) + +require golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e0f11ee --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +connectrpc.com/connect v1.16.2 h1:ybd6y+ls7GOlb7Bh5C8+ghA6SvCBajHwxssO2CGFjqE= +connectrpc.com/connect v1.16.2/go.mod h1:n2kgwskMHXC+lVqb18wngEpF95ldBHXjZYJussz5FRc= +connectrpc.com/grpcreflect v1.2.0 h1:Q6og1S7HinmtbEuBvARLNwYmTbhEGRpHDhqrPNlmK+U= +connectrpc.com/grpcreflect v1.2.0/go.mod h1:nwSOKmE8nU5u/CidgHtPYk1PFI3U9ignz7iDMxOYkSY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= diff --git a/internal/rpchandler/rpc_handler.go b/internal/rpchandler/rpc_handler.go new file mode 100644 index 0000000..a2b292f --- /dev/null +++ b/internal/rpchandler/rpc_handler.go @@ -0,0 +1,17 @@ +package rpchandler + +import ( + "gitlab.bareksa.com/backend/zen/internal/gen/proto/notify/v1/notifyv1connect" +) + +type NotifyServiceHandler struct { + notifyv1connect.UnimplementedNotifyServiceHandler +} + +// func (no *NotifyServiceHandler) SendNotification(_ context.Context, _ *connect.Request[notifyv1.SendNotificationRequest]) (*connect.Response[notifyv1.SendNotificationResponse], error) { +// panic("not implemented") // TODO: Implement +// } +// +// func (no *NotifyServiceHandler) SendAttachment(_ context.Context, _ *connect.ClientStream[notifyv1.SendAttachmentRequest]) (*connect.Response[notifyv1.SendAttachmentResponse], error) { +// panic("not implemented") // TODO: Implement +// } diff --git a/schemas/proto/buf.gen.yaml b/schemas/proto/buf.gen.yaml index 4e69c53..c61e81e 100644 --- a/schemas/proto/buf.gen.yaml +++ b/schemas/proto/buf.gen.yaml @@ -7,18 +7,18 @@ managed: override: - file_option: go_package_prefix - value: gitlab.bareksa.com/backend/zen/zcore/internal/gen/proto + value: gitlab.bareksa.com/backend/zen/internal/gen/proto plugins: - remote: buf.build/protocolbuffers/go:v1.34.2 - out: ../../zcore/internal/gen/proto + out: ../../internal/gen/proto opt: - paths=source_relative - remote: buf.build/connectrpc/go - out: ../../zcore/internal/gen/proto + out: ../../internal/gen/proto opt: - paths=source_relative - remote: buf.build/community/pseudomuto-doc:v1.5.1 - out: ../../zcore/internal/gen/proto + out: ../../internal/gen/proto diff --git a/schemas/proto/notify/v1/send_notification.proto b/schemas/proto/notify/v1/send_notification.proto index df2957f..20b3d49 100644 --- a/schemas/proto/notify/v1/send_notification.proto +++ b/schemas/proto/notify/v1/send_notification.proto @@ -47,6 +47,7 @@ message Payload { bytes e_json = 6; string e_text = 7; } + string id = 8; } message Service { diff --git a/zcore/internal/server/server.go b/zcore/internal/server/server.go deleted file mode 100644 index abb4e43..0000000 --- a/zcore/internal/server/server.go +++ /dev/null @@ -1 +0,0 @@ -package server diff --git a/zcore/zerr/zerr.go b/zcore/zerr/zerr.go deleted file mode 100644 index 07b86ba..0000000 --- a/zcore/zerr/zerr.go +++ /dev/null @@ -1,33 +0,0 @@ -package zerr - -import ( - "context" - "time" - - "gitlab.bareksa.com/backend/zen/zcore/zoptions" -) - -type Error interface { - error - - Code(code int) Error - GetCode() int - - Message(msg string, args ...any) Error - GetMessage() string - - PublicMessage(msg string, args ...any) Error - GetPublicMessage() string - - Caller(pc uintptr) Error - GetCaller() uintptr - - Time(t time.Time) Error - GetTime() time.Time - - Key(msg string, args ...any) Error - GetKey() string - - Log(ctx context.Context) Error - Notify(ctx context.Context, opts ...zoptions.NotifyOption) Error -} diff --git a/zcore/zlog/zlog.go b/zcore/zlog/zlog.go deleted file mode 100644 index d3e6811..0000000 --- a/zcore/zlog/zlog.go +++ /dev/null @@ -1 +0,0 @@ -package zlog