From f906c0ebddb139e9fb539baf798a5280f83b6009 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 5 May 2026 23:03:17 -0400 Subject: [PATCH 1/4] feat(template): derive ErrorTemplate from proto Template option Signed-off-by: Yordis Prieto --- error.go | 5 + go.mod | 6 +- go.sum | 4 + template_proto.go | 233 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 template_proto.go diff --git a/error.go b/error.go index b76f30e..84af3d6 100644 --- a/error.go +++ b/error.go @@ -795,6 +795,8 @@ type ErrorTemplate struct { message string // empty string means use code's default message visibility Visibility help *Help + metadata Metadata + fields []protoFieldSpec } // TemplateOption represents options that can be applied to ErrorTemplate @@ -866,6 +868,9 @@ func (et *ErrorTemplate) NewError(options ...ErrorOption) *TrogonError { if et.help != nil { baseOptions = append(baseOptions, WithHelp(*et.help)) } + for key, value := range et.metadata { + baseOptions = append(baseOptions, WithMetadataValue(value.visibility, key, value.value)) + } return NewError(et.domain, et.reason, append(baseOptions, options...)...) } diff --git a/go.mod b/go.mod index 4a6ffec..e13033e 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,17 @@ module github.com/TrogonStack/trogonerror -go 1.24.2 +go 1.26 require ( connectrpc.com/connect v1.19.2 + github.com/TrogonStack/trogonproto v0.1.0 github.com/stretchr/testify v1.11.1 + google.golang.org/protobuf v1.36.11 ) require ( + buf.build/gen/go/elixir-protobuf/protobuf/protocolbuffers/go v1.36.11-20260114033128-d590dff383e9.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 171f90f..b663fb7 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ +buf.build/gen/go/elixir-protobuf/protobuf/protocolbuffers/go v1.36.11-20260114033128-d590dff383e9.1 h1:YyaDi4kIyhOWTWYN0OzoKC+Wo+DpSC5aQxfe69fVUWA= +buf.build/gen/go/elixir-protobuf/protobuf/protocolbuffers/go v1.36.11-20260114033128-d590dff383e9.1/go.mod h1:fQtoq8UKgFiCFI3I7uyYxA2iVU1CplNN0PJpdFLdXHI= connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo= connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= +github.com/TrogonStack/trogonproto v0.1.0 h1:l3Skhw6rY4zskjS2d8GfXK9fKB6n5lIKObksAjknpUg= +github.com/TrogonStack/trogonproto v0.1.0/go.mod h1:RfEvGMmPYqH8RLE1I63nJZ3OmLUbBa4XSjya06aPLVE= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= diff --git a/template_proto.go b/template_proto.go new file mode 100644 index 0000000..fdf82d0 --- /dev/null +++ b/template_proto.go @@ -0,0 +1,233 @@ +package trogonerror + +import ( + "fmt" + + errpb "github.com/TrogonStack/trogonproto/gen/trogon/error/v1alpha1" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" +) + +type protoFieldSpec struct { + key string + number protoreflect.FieldNumber + visibility Visibility + hasFixedValue bool + fixedValue string + hasDefault bool + defaultValue string +} + +// NewErrorTemplateFromProto builds an ErrorTemplate from a proto message type +// that carries the trogon.error.v1alpha1.Template message option. +// +// The descriptor is read once at template-construction time. Per-field +// FieldOptions are cached so subsequent FromProto calls do not re-walk the +// descriptor. +func NewErrorTemplateFromProto[T proto.Message](options ...TemplateOption) *ErrorTemplate { + var zero T + desc := zero.ProtoReflect().Descriptor() + + template := &ErrorTemplate{ + code: CodeUnknown, + visibility: VisibilityInternal, + } + + if msgOpts, ok := proto.GetExtension(desc.Options(), errpb.E_Message).(*errpb.MessageOptions); ok && msgOpts != nil { + applyProtoTemplate(template, msgOpts.GetTemplate()) + } + + template.fields = collectFieldSpecs(desc) + for _, spec := range template.fields { + if spec.hasFixedValue { + if template.metadata == nil { + template.metadata = Metadata{} + } + template.metadata[spec.key] = MetadataValue{ + value: spec.fixedValue, + visibility: spec.visibility, + } + } + } + + for _, option := range options { + option(template) + } + + return template +} + +// FromProto creates a new error instance, deriving metadata from the proto +// message's populated fields according to their FieldOptions annotations. +// +// Fields with value_policy = value contribute their fixed literal (already +// baked into the template). Fields with value_policy = default_value use the +// runtime field value when set, falling back to default_value otherwise. +// Fields without a value policy use the runtime field value as-is. +// +// Caller-supplied options apply last and override anything derived from the +// proto instance. +func (et *ErrorTemplate) FromProto(m proto.Message, options ...ErrorOption) *TrogonError { + derived := make([]ErrorOption, 0, len(et.fields)) + reflected := m.ProtoReflect() + + for _, spec := range et.fields { + if spec.hasFixedValue { + continue + } + + field := reflected.Descriptor().Fields().ByNumber(spec.number) + if field == nil { + continue + } + + value := protoFieldString(reflected, field) + if value == "" && spec.hasDefault { + value = spec.defaultValue + } + if value == "" { + continue + } + + derived = append(derived, WithMetadataValue(spec.visibility, spec.key, value)) + } + + return et.NewError(append(derived, options...)...) +} + +func applyProtoTemplate(template *ErrorTemplate, t *errpb.MessageOptions_Template) { + if t == nil { + return + } + if d := t.GetDomain(); d != "" { + template.domain = d + } + if r := t.GetReason(); r != "" { + template.reason = r + } + if m := t.GetMessage(); m != "" { + template.message = m + } + if c := t.GetCode(); c != errpb.Code_UNSPECIFIED { + template.code = mapProtoCode(c) + } + if v := t.GetVisibility(); v != errpb.Visibility_VISIBILITY_UNSPECIFIED { + template.visibility = mapProtoVisibility(v) + } + for _, link := range t.GetHelpLinks() { + if template.help == nil { + template.help = &Help{} + } + template.help.links = append(template.help.links, HelpLink{ + description: link.GetDescription(), + url: link.GetUrl(), + }) + } + for _, entry := range t.GetMetadata() { + if entry.GetKey() == "" { + continue + } + if template.metadata == nil { + template.metadata = Metadata{} + } + template.metadata[entry.GetKey()] = MetadataValue{ + value: entry.GetValue(), + visibility: mapProtoVisibility(entry.GetVisibility()), + } + } +} + +func collectFieldSpecs(desc protoreflect.MessageDescriptor) []protoFieldSpec { + fields := desc.Fields() + specs := make([]protoFieldSpec, 0, fields.Len()) + for i := 0; i < fields.Len(); i++ { + field := fields.Get(i) + fopts, ok := proto.GetExtension(field.Options(), errpb.E_Field).(*errpb.FieldOptions) + if !ok || fopts == nil { + continue + } + spec := protoFieldSpec{ + key: field.JSONName(), + number: field.Number(), + visibility: mapProtoVisibility(fopts.GetVisibility()), + } + switch policy := fopts.GetValuePolicy().(type) { + case *errpb.FieldOptions_Value: + spec.hasFixedValue = true + spec.fixedValue = policy.Value + case *errpb.FieldOptions_DefaultValue: + spec.hasDefault = true + spec.defaultValue = policy.DefaultValue + } + specs = append(specs, spec) + } + return specs +} + +func protoFieldString(m protoreflect.Message, field protoreflect.FieldDescriptor) string { + if !m.Has(field) { + return "" + } + v := m.Get(field) + switch field.Kind() { + case protoreflect.StringKind: + return v.String() + case protoreflect.EnumKind: + if ev := field.Enum().Values().ByNumber(v.Enum()); ev != nil { + return string(ev.Name()) + } + return fmt.Sprint(int32(v.Enum())) + default: + return fmt.Sprint(v.Interface()) + } +} + +func mapProtoCode(c errpb.Code) Code { + switch c { + case errpb.Code_CANCELLED: + return CodeCancelled + case errpb.Code_UNKNOWN: + return CodeUnknown + case errpb.Code_INVALID_ARGUMENT: + return CodeInvalidArgument + case errpb.Code_DEADLINE_EXCEEDED: + return CodeDeadlineExceeded + case errpb.Code_NOT_FOUND: + return CodeNotFound + case errpb.Code_ALREADY_EXISTS: + return CodeAlreadyExists + case errpb.Code_PERMISSION_DENIED: + return CodePermissionDenied + case errpb.Code_RESOURCE_EXHAUSTED: + return CodeResourceExhausted + case errpb.Code_FAILED_PRECONDITION: + return CodeFailedPrecondition + case errpb.Code_ABORTED: + return CodeAborted + case errpb.Code_OUT_OF_RANGE: + return CodeOutOfRange + case errpb.Code_UNIMPLEMENTED: + return CodeUnimplemented + case errpb.Code_INTERNAL: + return CodeInternal + case errpb.Code_UNAVAILABLE: + return CodeUnavailable + case errpb.Code_DATA_LOSS: + return CodeDataLoss + case errpb.Code_UNAUTHENTICATED: + return CodeUnauthenticated + default: + return CodeUnknown + } +} + +func mapProtoVisibility(v errpb.Visibility) Visibility { + switch v { + case errpb.Visibility_VISIBILITY_PUBLIC: + return VisibilityPublic + case errpb.Visibility_VISIBILITY_PRIVATE: + return VisibilityPrivate + default: + return VisibilityInternal + } +} From b21328fd4301f8fd0ac4d9bb41ea180fba7d3e40 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 5 May 2026 23:08:12 -0400 Subject: [PATCH 2/4] test(template): cover proto-derived template with sample fixture Signed-off-by: Yordis Prieto --- .gitattributes | 3 + .github/workflows/ci.yml | 20 +- buf.gen.yaml | 25 +++ example_test.go | 25 +++ .../gen/trogonerror/testdata/v1/errors.pb.go | 184 ++++++++++++++++++ internal/testdata/proto/buf.lock | 12 ++ internal/testdata/proto/buf.yaml | 6 + .../trogonerror/testdata/v1/errors.proto | 34 ++++ template_proto_test.go | 91 +++++++++ 9 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 buf.gen.yaml create mode 100644 internal/testdata/gen/trogonerror/testdata/v1/errors.pb.go create mode 100644 internal/testdata/proto/buf.lock create mode 100644 internal/testdata/proto/buf.yaml create mode 100644 internal/testdata/proto/trogonerror/testdata/v1/errors.proto create mode 100644 template_proto_test.go diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c85eac3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto eol=lf +internal/testdata/gen/** linguist-generated=true +*.pb.go linguist-generated=true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37dc982..ef26bd7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,12 +12,30 @@ permissions: pull-requests: read jobs: + generated-code-up-to-date: + name: Generated Code Up-To-Date + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5.0.0 + - uses: bufbuild/buf-setup-action@v1.50.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Regenerate Go bindings + run: buf generate + - name: Verify clean working tree + run: | + if ! git diff --quiet --exit-code; then + echo "Generated code is out of date. Run 'buf generate' and commit the result." + git diff + exit 1 + fi + quality-assurance: name: Quality Assurance runs-on: ubuntu-latest strategy: matrix: - go-version: [1.24.x] + go-version: [1.26.x] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..d1a136f --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,25 @@ +version: v2 +managed: + enabled: true + override: + - file_option: go_package_prefix + value: github.com/TrogonStack/trogonerror/internal/testdata/gen + - file_option: go_package + path: trogon/error/v1alpha1/code.proto + value: github.com/TrogonStack/trogonproto/gen/trogon/error/v1alpha1 + - file_option: go_package + path: trogon/error/v1alpha1/options.proto + value: github.com/TrogonStack/trogonproto/gen/trogon/error/v1alpha1 + - file_option: go_package + path: trogon/error/v1alpha1/visibility.proto + value: github.com/TrogonStack/trogonproto/gen/trogon/error/v1alpha1 + - file_option: go_package + path: elixirpb.proto + value: buf.build/gen/go/elixir-protobuf/protobuf/protocolbuffers/go +plugins: + - remote: buf.build/protocolbuffers/go:v1.36.5 + out: internal/testdata/gen + opt: + - paths=source_relative +inputs: + - directory: internal/testdata/proto diff --git a/example_test.go b/example_test.go index 84be37f..2920865 100644 --- a/example_test.go +++ b/example_test.go @@ -5,8 +5,33 @@ import ( "time" "github.com/TrogonStack/trogonerror" + testdatav1 "github.com/TrogonStack/trogonerror/internal/testdata/gen/trogonerror/testdata/v1" ) +var userNotFoundTpl = trogonerror.NewErrorTemplateFromProto[*testdatav1.UserNotFound]() + +func ExampleNewErrorTemplateFromProto() { + err := userNotFoundTpl.FromProto(&testdatav1.UserNotFound{ + UserId: "gid://shopify/Customer/1234567890", + }) + + fmt.Println(err.Error()) + // Output: + // User does not exist. + // visibility: PUBLIC + // domain: shopify.users + // reason: USER_NOT_FOUND + // code: NOT_FOUND + // metadata: + // - component: users visibility=PUBLIC + // - region: us-east-1 visibility=PUBLIC + // - team: platform-identity visibility=INTERNAL + // - tenantId: default-tenant visibility=PUBLIC + // - userId: gid://shopify/Customer/1234567890 visibility=PUBLIC + // + // - User Docs: https://docs.shopify.com/users +} + func ExampleNewError() { err := trogonerror.NewError("shopify.users", "NOT_FOUND", trogonerror.WithCode(trogonerror.CodeNotFound), diff --git a/internal/testdata/gen/trogonerror/testdata/v1/errors.pb.go b/internal/testdata/gen/trogonerror/testdata/v1/errors.pb.go new file mode 100644 index 0000000..6c51380 --- /dev/null +++ b/internal/testdata/gen/trogonerror/testdata/v1/errors.pb.go @@ -0,0 +1,184 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.5 +// protoc (unknown) +// source: trogonerror/testdata/v1/errors.proto + +package testdatav1 + +import ( + _ "github.com/TrogonStack/trogonproto/gen/trogon/error/v1alpha1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type UserNotFound struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + TenantId string `protobuf:"bytes,2,opt,name=tenant_id,json=tenantId,proto3" json:"tenant_id,omitempty"` + Region string `protobuf:"bytes,3,opt,name=region,proto3" json:"region,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserNotFound) Reset() { + *x = UserNotFound{} + mi := &file_trogonerror_testdata_v1_errors_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserNotFound) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserNotFound) ProtoMessage() {} + +func (x *UserNotFound) ProtoReflect() protoreflect.Message { + mi := &file_trogonerror_testdata_v1_errors_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserNotFound.ProtoReflect.Descriptor instead. +func (*UserNotFound) Descriptor() ([]byte, []int) { + return file_trogonerror_testdata_v1_errors_proto_rawDescGZIP(), []int{0} +} + +func (x *UserNotFound) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *UserNotFound) GetTenantId() string { + if x != nil { + return x.TenantId + } + return "" +} + +func (x *UserNotFound) GetRegion() string { + if x != nil { + return x.Region + } + return "" +} + +var File_trogonerror_testdata_v1_errors_proto protoreflect.FileDescriptor + +var file_trogonerror_testdata_v1_errors_proto_rawDesc = string([]byte{ + 0x0a, 0x24, 0x74, 0x72, 0x6f, 0x67, 0x6f, 0x6e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x2f, 0x74, 0x65, + 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x17, 0x74, 0x72, 0x6f, 0x67, 0x6f, 0x6e, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x76, 0x31, 0x1a, + 0x20, 0x74, 0x72, 0x6f, 0x67, 0x6f, 0x6e, 0x2f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x2f, 0x76, 0x31, + 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x1a, 0x23, 0x74, 0x72, 0x6f, 0x67, 0x6f, 0x6e, 0x2f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x2f, + 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x26, 0x74, 0x72, 0x6f, 0x67, 0x6f, 0x6e, 0x2f, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x76, 0x69, + 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb7, + 0x02, 0x0a, 0x0c, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x12, + 0x20, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x07, 0xea, 0xe7, 0xa8, 0x03, 0x02, 0x08, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, + 0x64, 0x12, 0x34, 0x0a, 0x09, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x17, 0xea, 0xe7, 0xa8, 0x03, 0x12, 0x08, 0x03, 0x12, 0x0e, 0x64, + 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x2d, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x52, 0x08, 0x74, + 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, + 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x12, 0xea, 0xe7, 0xa8, 0x03, 0x0d, 0x08, 0x03, + 0x1a, 0x09, 0x75, 0x73, 0x2d, 0x65, 0x61, 0x73, 0x74, 0x2d, 0x31, 0x52, 0x06, 0x72, 0x65, 0x67, + 0x69, 0x6f, 0x6e, 0x3a, 0xa2, 0x01, 0xe2, 0xe7, 0xa8, 0x03, 0x9c, 0x01, 0x0a, 0x99, 0x01, 0x0a, + 0x0d, 0x73, 0x68, 0x6f, 0x70, 0x69, 0x66, 0x79, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x12, 0x0e, + 0x55, 0x53, 0x45, 0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x46, 0x4f, 0x55, 0x4e, 0x44, 0x1a, 0x14, + 0x55, 0x73, 0x65, 0x72, 0x20, 0x64, 0x6f, 0x65, 0x73, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x65, 0x78, + 0x69, 0x73, 0x74, 0x2e, 0x20, 0x05, 0x28, 0x03, 0x32, 0x2b, 0x0a, 0x1e, 0x68, 0x74, 0x74, 0x70, + 0x73, 0x3a, 0x2f, 0x2f, 0x64, 0x6f, 0x63, 0x73, 0x2e, 0x73, 0x68, 0x6f, 0x70, 0x69, 0x66, 0x79, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x12, 0x09, 0x55, 0x73, 0x65, 0x72, + 0x20, 0x44, 0x6f, 0x63, 0x73, 0x3a, 0x14, 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, + 0x6e, 0x74, 0x12, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x03, 0x3a, 0x1b, 0x0a, 0x04, 0x74, + 0x65, 0x61, 0x6d, 0x12, 0x11, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2d, 0x69, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x42, 0x85, 0x02, 0x0a, 0x1b, 0x63, 0x6f, 0x6d, + 0x2e, 0x74, 0x72, 0x6f, 0x67, 0x6f, 0x6e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x2e, 0x74, 0x65, 0x73, + 0x74, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x76, 0x31, 0x42, 0x0b, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x5b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x54, 0x72, 0x6f, 0x67, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x2f, + 0x74, 0x72, 0x6f, 0x67, 0x6f, 0x6e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x67, 0x65, + 0x6e, 0x2f, 0x74, 0x72, 0x6f, 0x67, 0x6f, 0x6e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x2f, 0x74, 0x65, + 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x76, 0x31, 0x3b, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, + 0x74, 0x61, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x54, 0x54, 0x58, 0xaa, 0x02, 0x17, 0x54, 0x72, 0x6f, + 0x67, 0x6f, 0x6e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x2e, 0x54, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, + 0x61, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x17, 0x54, 0x72, 0x6f, 0x67, 0x6f, 0x6e, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x5c, 0x54, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x5c, 0x56, 0x31, 0xe2, 0x02, + 0x23, 0x54, 0x72, 0x6f, 0x67, 0x6f, 0x6e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5c, 0x54, 0x65, 0x73, + 0x74, 0x64, 0x61, 0x74, 0x61, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x19, 0x54, 0x72, 0x6f, 0x67, 0x6f, 0x6e, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x3a, 0x3a, 0x54, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x3a, 0x3a, 0x56, 0x31, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) + +var ( + file_trogonerror_testdata_v1_errors_proto_rawDescOnce sync.Once + file_trogonerror_testdata_v1_errors_proto_rawDescData []byte +) + +func file_trogonerror_testdata_v1_errors_proto_rawDescGZIP() []byte { + file_trogonerror_testdata_v1_errors_proto_rawDescOnce.Do(func() { + file_trogonerror_testdata_v1_errors_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_trogonerror_testdata_v1_errors_proto_rawDesc), len(file_trogonerror_testdata_v1_errors_proto_rawDesc))) + }) + return file_trogonerror_testdata_v1_errors_proto_rawDescData +} + +var file_trogonerror_testdata_v1_errors_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_trogonerror_testdata_v1_errors_proto_goTypes = []any{ + (*UserNotFound)(nil), // 0: trogonerror.testdata.v1.UserNotFound +} +var file_trogonerror_testdata_v1_errors_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_trogonerror_testdata_v1_errors_proto_init() } +func file_trogonerror_testdata_v1_errors_proto_init() { + if File_trogonerror_testdata_v1_errors_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_trogonerror_testdata_v1_errors_proto_rawDesc), len(file_trogonerror_testdata_v1_errors_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_trogonerror_testdata_v1_errors_proto_goTypes, + DependencyIndexes: file_trogonerror_testdata_v1_errors_proto_depIdxs, + MessageInfos: file_trogonerror_testdata_v1_errors_proto_msgTypes, + }.Build() + File_trogonerror_testdata_v1_errors_proto = out.File + file_trogonerror_testdata_v1_errors_proto_goTypes = nil + file_trogonerror_testdata_v1_errors_proto_depIdxs = nil +} diff --git a/internal/testdata/proto/buf.lock b/internal/testdata/proto/buf.lock new file mode 100644 index 0000000..9114efb --- /dev/null +++ b/internal/testdata/proto/buf.lock @@ -0,0 +1,12 @@ +# Generated by buf. DO NOT EDIT. +version: v2 +deps: + - name: buf.build/elixir-protobuf/protobuf + commit: d590dff383e94f28bacec0e8015c4daa + digest: b5:fa4a17975e0ed6b54e3e7637d659a6358fb18ebd8a931462003cea62d43336fb02c8685076e4cb5129875b13f13c2c6dfe7205f2215d7666efdfc5cce836a2de + - name: buf.build/protocolbuffers/wellknowntypes + commit: 4e1ccfa6827947beb55974645a315b8d + digest: b5:eb5228b1abd02064d6ff0248918500c1ec1ce7df69126af3f220c0b67d81ff45bdf9f016a8e66cd9c1e534f18afc6d8e090d400604c5331d551a68d05f7e7be9 + - name: buf.build/trogonstack/trogon-proto + commit: e7442b2c978348c9864565ec086db7a5 + digest: b5:2dba0ce198c5981e5d870610f5a5eaadf499d28bffb9d34fd4df5decc35ac5cbcedb670eeaf431312ab59e6d4050f7eb5559a5715e3db06322033658269468ef diff --git a/internal/testdata/proto/buf.yaml b/internal/testdata/proto/buf.yaml new file mode 100644 index 0000000..2b937ab --- /dev/null +++ b/internal/testdata/proto/buf.yaml @@ -0,0 +1,6 @@ +version: v2 +modules: + - path: . +deps: + - buf.build/trogonstack/trogon-proto + - buf.build/elixir-protobuf/protobuf diff --git a/internal/testdata/proto/trogonerror/testdata/v1/errors.proto b/internal/testdata/proto/trogonerror/testdata/v1/errors.proto new file mode 100644 index 0000000..79bd77a --- /dev/null +++ b/internal/testdata/proto/trogonerror/testdata/v1/errors.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +package trogonerror.testdata.v1; + +import "trogon/error/v1alpha1/code.proto"; +import "trogon/error/v1alpha1/options.proto"; +import "trogon/error/v1alpha1/visibility.proto"; + +message UserNotFound { + option (trogon.error.v1alpha1.message).template = { + domain: "shopify.users", + reason: "USER_NOT_FOUND", + message: "User does not exist.", + code: NOT_FOUND, + visibility: VISIBILITY_PUBLIC, + help_links: [{url: "https://docs.shopify.com/users", description: "User Docs"}], + metadata: [ + {key: "component", value: "users", visibility: VISIBILITY_PUBLIC}, + {key: "team", value: "platform-identity", visibility: VISIBILITY_INTERNAL} + ] + }; + + string user_id = 1 [(trogon.error.v1alpha1.field) = { + visibility: VISIBILITY_PUBLIC + }]; + string tenant_id = 2 [(trogon.error.v1alpha1.field) = { + visibility: VISIBILITY_PUBLIC, + default_value: "default-tenant" + }]; + string region = 3 [(trogon.error.v1alpha1.field) = { + visibility: VISIBILITY_PUBLIC, + value: "us-east-1" + }]; +} diff --git a/template_proto_test.go b/template_proto_test.go new file mode 100644 index 0000000..69dfb1e --- /dev/null +++ b/template_proto_test.go @@ -0,0 +1,91 @@ +package trogonerror_test + +import ( + "testing" + + "github.com/TrogonStack/trogonerror" + testdatav1 "github.com/TrogonStack/trogonerror/internal/testdata/gen/trogonerror/testdata/v1" + "github.com/stretchr/testify/assert" +) + +var userNotFoundTemplate = trogonerror.NewErrorTemplateFromProto[*testdatav1.UserNotFound]() + +func TestNewErrorTemplateFromProto_TemplateLevel(t *testing.T) { + err := userNotFoundTemplate.NewError() + + assert.Equal(t, "shopify.users", err.Domain()) + assert.Equal(t, "USER_NOT_FOUND", err.Reason()) + assert.Equal(t, trogonerror.CodeNotFound, err.Code()) + assert.Equal(t, "User does not exist.", err.Message()) + assert.Equal(t, trogonerror.VisibilityPublic, err.Visibility()) + + help := err.Help() + if assert.NotNil(t, help) { + assert.Equal(t, "https://docs.shopify.com/users", help.Links()[0].URL()) + } +} + +func TestNewErrorTemplateFromProto_TemplateMetadata(t *testing.T) { + md := userNotFoundTemplate.NewError().Metadata() + + assert.Equal(t, "users", md["component"].Value()) + assert.Equal(t, trogonerror.VisibilityPublic, md["component"].Visibility()) + assert.Equal(t, "platform-identity", md["team"].Value()) + assert.Equal(t, trogonerror.VisibilityInternal, md["team"].Visibility()) +} + +func TestNewErrorTemplateFromProto_FieldFixedValueBaked(t *testing.T) { + md := userNotFoundTemplate.NewError().Metadata() + + assert.Equal(t, "us-east-1", md["region"].Value(), "value_policy=value should bake into template") + assert.Equal(t, trogonerror.VisibilityPublic, md["region"].Visibility()) +} + +func TestErrorTemplate_FromProto_RuntimeFieldValues(t *testing.T) { + err := userNotFoundTemplate.FromProto(&testdatav1.UserNotFound{ + UserId: "gid://shopify/Customer/1234", + TenantId: "acme", + }) + + md := err.Metadata() + assert.Equal(t, "gid://shopify/Customer/1234", md["userId"].Value()) + assert.Equal(t, "acme", md["tenantId"].Value()) + assert.Equal(t, "us-east-1", md["region"].Value()) +} + +func TestErrorTemplate_FromProto_DefaultValueFallback(t *testing.T) { + err := userNotFoundTemplate.FromProto(&testdatav1.UserNotFound{ + UserId: "gid://shopify/Customer/1234", + }) + + md := err.Metadata() + assert.Equal(t, "default-tenant", md["tenantId"].Value(), "empty field should fall back to default_value") +} + +func TestErrorTemplate_FromProto_FixedValueWinsOverRuntime(t *testing.T) { + err := userNotFoundTemplate.FromProto(&testdatav1.UserNotFound{ + UserId: "gid://shopify/Customer/1234", + Region: "ignored", + }) + + assert.Equal(t, "us-east-1", err.Metadata()["region"].Value(), + "value_policy=value must ignore runtime field") +} + +func TestErrorTemplate_FromProto_OptionsOverride(t *testing.T) { + err := userNotFoundTemplate.FromProto( + &testdatav1.UserNotFound{UserId: "u1"}, + trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "userId", "u2"), + ) + + assert.Equal(t, "u2", err.Metadata()["userId"].Value(), + "caller options must win over proto-derived metadata") +} + +func TestNewErrorTemplateFromProto_TemplateOptionOverride(t *testing.T) { + template := trogonerror.NewErrorTemplateFromProto[*testdatav1.UserNotFound]( + trogonerror.TemplateWithMessage("custom message"), + ) + + assert.Equal(t, "custom message", template.NewError().Message()) +} From d25ae7225989826051f71d0b43b3ceb98a8750dc Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 5 May 2026 23:09:57 -0400 Subject: [PATCH 3/4] chore(buf): collapse trogon-proto overrides and drop unused elixirpb mapping Signed-off-by: Yordis Prieto --- buf.gen.yaml | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/buf.gen.yaml b/buf.gen.yaml index d1a136f..d326f2c 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -4,18 +4,9 @@ managed: override: - file_option: go_package_prefix value: github.com/TrogonStack/trogonerror/internal/testdata/gen - - file_option: go_package - path: trogon/error/v1alpha1/code.proto - value: github.com/TrogonStack/trogonproto/gen/trogon/error/v1alpha1 - - file_option: go_package - path: trogon/error/v1alpha1/options.proto - value: github.com/TrogonStack/trogonproto/gen/trogon/error/v1alpha1 - - file_option: go_package - path: trogon/error/v1alpha1/visibility.proto - value: github.com/TrogonStack/trogonproto/gen/trogon/error/v1alpha1 - - file_option: go_package - path: elixirpb.proto - value: buf.build/gen/go/elixir-protobuf/protobuf/protocolbuffers/go + - file_option: go_package_prefix + module: buf.build/trogonstack/trogon-proto + value: github.com/TrogonStack/trogonproto/gen plugins: - remote: buf.build/protocolbuffers/go:v1.36.5 out: internal/testdata/gen From 2462f11fc324f9f1853dea0f2e5f7bfd20397e87 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Wed, 7 Oct 2026 13:03:21 -0400 Subject: [PATCH 4/4] refactor(errproto): move proto template derivation into its own package Keeps the core trogonerror package free of a hard protobuf dependency so consumers that do not use proto-declared errors avoid pulling it in. Signed-off-by: Yordis Prieto --- error.go | 11 +- errproto/example_test.go | 32 ++++ .../template_proto.go | 152 +++++++++--------- .../template_proto_test.go | 7 +- example_test.go | 25 --- 5 files changed, 118 insertions(+), 109 deletions(-) create mode 100644 errproto/example_test.go rename template_proto.go => errproto/template_proto.go (52%) rename template_proto_test.go => errproto/template_proto_test.go (92%) diff --git a/error.go b/error.go index 84af3d6..3171b4d 100644 --- a/error.go +++ b/error.go @@ -796,7 +796,6 @@ type ErrorTemplate struct { visibility Visibility help *Help metadata Metadata - fields []protoFieldSpec } // TemplateOption represents options that can be applied to ErrorTemplate @@ -856,6 +855,16 @@ func TemplateWithHelpLink(description, url string) TemplateOption { } } +// TemplateWithMetadataValue sets a single metadata entry with specific visibility +func TemplateWithMetadataValue(visibility Visibility, key, value string) TemplateOption { + return func(t *ErrorTemplate) { + if t.metadata == nil { + t.metadata = Metadata{} + } + t.metadata[key] = MetadataValue{value: value, visibility: visibility} + } +} + // NewError creates a new error instance from the template func (et *ErrorTemplate) NewError(options ...ErrorOption) *TrogonError { baseOptions := []ErrorOption{ diff --git a/errproto/example_test.go b/errproto/example_test.go new file mode 100644 index 0000000..75d8c0d --- /dev/null +++ b/errproto/example_test.go @@ -0,0 +1,32 @@ +package errproto_test + +import ( + "fmt" + + "github.com/TrogonStack/trogonerror/errproto" + testdatav1 "github.com/TrogonStack/trogonerror/internal/testdata/gen/trogonerror/testdata/v1" +) + +var userNotFoundTpl = errproto.NewErrorTemplateFromProto[*testdatav1.UserNotFound]() + +func ExampleNewErrorTemplateFromProto() { + err := userNotFoundTpl.FromProto(&testdatav1.UserNotFound{ + UserId: "gid://shopify/Customer/1234567890", + }) + + fmt.Println(err.Error()) + // Output: + // User does not exist. + // visibility: PUBLIC + // domain: shopify.users + // reason: USER_NOT_FOUND + // code: NOT_FOUND + // metadata: + // - component: users visibility=PUBLIC + // - region: us-east-1 visibility=PUBLIC + // - team: platform-identity visibility=INTERNAL + // - tenantId: default-tenant visibility=PUBLIC + // - userId: gid://shopify/Customer/1234567890 visibility=PUBLIC + // + // - User Docs: https://docs.shopify.com/users +} diff --git a/template_proto.go b/errproto/template_proto.go similarity index 52% rename from template_proto.go rename to errproto/template_proto.go index fdf82d0..25087ce 100644 --- a/template_proto.go +++ b/errproto/template_proto.go @@ -1,60 +1,64 @@ -package trogonerror +// Package errproto derives trogonerror.ErrorTemplate values from proto +// messages that carry the trogon.error.v1alpha1 Template and FieldOptions +// message/field options, so services can declare their error contract once +// in proto and pick it up in Go without hand-rewriting the same +// domain/reason/code/visibility/help/metadata in two places. +package errproto import ( "fmt" + "github.com/TrogonStack/trogonerror" errpb "github.com/TrogonStack/trogonproto/gen/trogon/error/v1alpha1" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" ) type protoFieldSpec struct { - key string - number protoreflect.FieldNumber - visibility Visibility - hasFixedValue bool - fixedValue string - hasDefault bool - defaultValue string + key string + number protoreflect.FieldNumber + visibility trogonerror.Visibility + hasFixedValue bool + fixedValue string + hasDefault bool + defaultValue string } -// NewErrorTemplateFromProto builds an ErrorTemplate from a proto message type +// Template wraps a trogonerror.ErrorTemplate with the proto field +// specifications needed to derive per-instance metadata from a populated +// proto message. +type Template struct { + *trogonerror.ErrorTemplate + fields []protoFieldSpec +} + +// NewErrorTemplateFromProto builds a Template from a proto message type // that carries the trogon.error.v1alpha1.Template message option. // // The descriptor is read once at template-construction time. Per-field // FieldOptions are cached so subsequent FromProto calls do not re-walk the // descriptor. -func NewErrorTemplateFromProto[T proto.Message](options ...TemplateOption) *ErrorTemplate { +func NewErrorTemplateFromProto[T proto.Message](options ...trogonerror.TemplateOption) *Template { var zero T desc := zero.ProtoReflect().Descriptor() - template := &ErrorTemplate{ - code: CodeUnknown, - visibility: VisibilityInternal, - } + var domain, reason string + var derived []trogonerror.TemplateOption if msgOpts, ok := proto.GetExtension(desc.Options(), errpb.E_Message).(*errpb.MessageOptions); ok && msgOpts != nil { - applyProtoTemplate(template, msgOpts.GetTemplate()) + domain, reason, derived = templateOptionsFromProto(msgOpts.GetTemplate()) } - template.fields = collectFieldSpecs(desc) - for _, spec := range template.fields { + fields := collectFieldSpecs(desc) + for _, spec := range fields { if spec.hasFixedValue { - if template.metadata == nil { - template.metadata = Metadata{} - } - template.metadata[spec.key] = MetadataValue{ - value: spec.fixedValue, - visibility: spec.visibility, - } + derived = append(derived, trogonerror.TemplateWithMetadataValue(spec.visibility, spec.key, spec.fixedValue)) } } - for _, option := range options { - option(template) - } + et := trogonerror.NewErrorTemplate(domain, reason, append(derived, options...)...) - return template + return &Template{ErrorTemplate: et, fields: fields} } // FromProto creates a new error instance, deriving metadata from the proto @@ -67,11 +71,11 @@ func NewErrorTemplateFromProto[T proto.Message](options ...TemplateOption) *Erro // // Caller-supplied options apply last and override anything derived from the // proto instance. -func (et *ErrorTemplate) FromProto(m proto.Message, options ...ErrorOption) *TrogonError { - derived := make([]ErrorOption, 0, len(et.fields)) +func (t *Template) FromProto(m proto.Message, options ...trogonerror.ErrorOption) *trogonerror.TrogonError { + derived := make([]trogonerror.ErrorOption, 0, len(t.fields)) reflected := m.ProtoReflect() - for _, spec := range et.fields { + for _, spec := range t.fields { if spec.hasFixedValue { continue } @@ -89,52 +93,40 @@ func (et *ErrorTemplate) FromProto(m proto.Message, options ...ErrorOption) *Tro continue } - derived = append(derived, WithMetadataValue(spec.visibility, spec.key, value)) + derived = append(derived, trogonerror.WithMetadataValue(spec.visibility, spec.key, value)) } - return et.NewError(append(derived, options...)...) + return t.NewError(append(derived, options...)...) } -func applyProtoTemplate(template *ErrorTemplate, t *errpb.MessageOptions_Template) { +func templateOptionsFromProto(t *errpb.MessageOptions_Template) (domain, reason string, opts []trogonerror.TemplateOption) { if t == nil { - return - } - if d := t.GetDomain(); d != "" { - template.domain = d - } - if r := t.GetReason(); r != "" { - template.reason = r + return "", "", nil } + + domain = t.GetDomain() + reason = t.GetReason() + if m := t.GetMessage(); m != "" { - template.message = m + opts = append(opts, trogonerror.TemplateWithMessage(m)) } if c := t.GetCode(); c != errpb.Code_UNSPECIFIED { - template.code = mapProtoCode(c) + opts = append(opts, trogonerror.TemplateWithCode(mapProtoCode(c))) } if v := t.GetVisibility(); v != errpb.Visibility_VISIBILITY_UNSPECIFIED { - template.visibility = mapProtoVisibility(v) + opts = append(opts, trogonerror.TemplateWithVisibility(mapProtoVisibility(v))) } for _, link := range t.GetHelpLinks() { - if template.help == nil { - template.help = &Help{} - } - template.help.links = append(template.help.links, HelpLink{ - description: link.GetDescription(), - url: link.GetUrl(), - }) + opts = append(opts, trogonerror.TemplateWithHelpLink(link.GetDescription(), link.GetUrl())) } for _, entry := range t.GetMetadata() { if entry.GetKey() == "" { continue } - if template.metadata == nil { - template.metadata = Metadata{} - } - template.metadata[entry.GetKey()] = MetadataValue{ - value: entry.GetValue(), - visibility: mapProtoVisibility(entry.GetVisibility()), - } + opts = append(opts, trogonerror.TemplateWithMetadataValue(mapProtoVisibility(entry.GetVisibility()), entry.GetKey(), entry.GetValue())) } + + return domain, reason, opts } func collectFieldSpecs(desc protoreflect.MessageDescriptor) []protoFieldSpec { @@ -182,52 +174,52 @@ func protoFieldString(m protoreflect.Message, field protoreflect.FieldDescriptor } } -func mapProtoCode(c errpb.Code) Code { +func mapProtoCode(c errpb.Code) trogonerror.Code { switch c { case errpb.Code_CANCELLED: - return CodeCancelled + return trogonerror.CodeCancelled case errpb.Code_UNKNOWN: - return CodeUnknown + return trogonerror.CodeUnknown case errpb.Code_INVALID_ARGUMENT: - return CodeInvalidArgument + return trogonerror.CodeInvalidArgument case errpb.Code_DEADLINE_EXCEEDED: - return CodeDeadlineExceeded + return trogonerror.CodeDeadlineExceeded case errpb.Code_NOT_FOUND: - return CodeNotFound + return trogonerror.CodeNotFound case errpb.Code_ALREADY_EXISTS: - return CodeAlreadyExists + return trogonerror.CodeAlreadyExists case errpb.Code_PERMISSION_DENIED: - return CodePermissionDenied + return trogonerror.CodePermissionDenied case errpb.Code_RESOURCE_EXHAUSTED: - return CodeResourceExhausted + return trogonerror.CodeResourceExhausted case errpb.Code_FAILED_PRECONDITION: - return CodeFailedPrecondition + return trogonerror.CodeFailedPrecondition case errpb.Code_ABORTED: - return CodeAborted + return trogonerror.CodeAborted case errpb.Code_OUT_OF_RANGE: - return CodeOutOfRange + return trogonerror.CodeOutOfRange case errpb.Code_UNIMPLEMENTED: - return CodeUnimplemented + return trogonerror.CodeUnimplemented case errpb.Code_INTERNAL: - return CodeInternal + return trogonerror.CodeInternal case errpb.Code_UNAVAILABLE: - return CodeUnavailable + return trogonerror.CodeUnavailable case errpb.Code_DATA_LOSS: - return CodeDataLoss + return trogonerror.CodeDataLoss case errpb.Code_UNAUTHENTICATED: - return CodeUnauthenticated + return trogonerror.CodeUnauthenticated default: - return CodeUnknown + return trogonerror.CodeUnknown } } -func mapProtoVisibility(v errpb.Visibility) Visibility { +func mapProtoVisibility(v errpb.Visibility) trogonerror.Visibility { switch v { case errpb.Visibility_VISIBILITY_PUBLIC: - return VisibilityPublic + return trogonerror.VisibilityPublic case errpb.Visibility_VISIBILITY_PRIVATE: - return VisibilityPrivate + return trogonerror.VisibilityPrivate default: - return VisibilityInternal + return trogonerror.VisibilityInternal } } diff --git a/template_proto_test.go b/errproto/template_proto_test.go similarity index 92% rename from template_proto_test.go rename to errproto/template_proto_test.go index 69dfb1e..cf0355e 100644 --- a/template_proto_test.go +++ b/errproto/template_proto_test.go @@ -1,14 +1,15 @@ -package trogonerror_test +package errproto_test import ( "testing" "github.com/TrogonStack/trogonerror" + "github.com/TrogonStack/trogonerror/errproto" testdatav1 "github.com/TrogonStack/trogonerror/internal/testdata/gen/trogonerror/testdata/v1" "github.com/stretchr/testify/assert" ) -var userNotFoundTemplate = trogonerror.NewErrorTemplateFromProto[*testdatav1.UserNotFound]() +var userNotFoundTemplate = errproto.NewErrorTemplateFromProto[*testdatav1.UserNotFound]() func TestNewErrorTemplateFromProto_TemplateLevel(t *testing.T) { err := userNotFoundTemplate.NewError() @@ -83,7 +84,7 @@ func TestErrorTemplate_FromProto_OptionsOverride(t *testing.T) { } func TestNewErrorTemplateFromProto_TemplateOptionOverride(t *testing.T) { - template := trogonerror.NewErrorTemplateFromProto[*testdatav1.UserNotFound]( + template := errproto.NewErrorTemplateFromProto[*testdatav1.UserNotFound]( trogonerror.TemplateWithMessage("custom message"), ) diff --git a/example_test.go b/example_test.go index 2920865..84be37f 100644 --- a/example_test.go +++ b/example_test.go @@ -5,33 +5,8 @@ import ( "time" "github.com/TrogonStack/trogonerror" - testdatav1 "github.com/TrogonStack/trogonerror/internal/testdata/gen/trogonerror/testdata/v1" ) -var userNotFoundTpl = trogonerror.NewErrorTemplateFromProto[*testdatav1.UserNotFound]() - -func ExampleNewErrorTemplateFromProto() { - err := userNotFoundTpl.FromProto(&testdatav1.UserNotFound{ - UserId: "gid://shopify/Customer/1234567890", - }) - - fmt.Println(err.Error()) - // Output: - // User does not exist. - // visibility: PUBLIC - // domain: shopify.users - // reason: USER_NOT_FOUND - // code: NOT_FOUND - // metadata: - // - component: users visibility=PUBLIC - // - region: us-east-1 visibility=PUBLIC - // - team: platform-identity visibility=INTERNAL - // - tenantId: default-tenant visibility=PUBLIC - // - userId: gid://shopify/Customer/1234567890 visibility=PUBLIC - // - // - User Docs: https://docs.shopify.com/users -} - func ExampleNewError() { err := trogonerror.NewError("shopify.users", "NOT_FOUND", trogonerror.WithCode(trogonerror.CodeNotFound),