From 434d858718d87c9c434ae68dc9095f66fb161008 Mon Sep 17 00:00:00 2001 From: mogfee Date: Mon, 6 Mar 2023 21:25:47 +0800 Subject: [PATCH] x --- app.go | 11 +- cmd/kit/main.go | 8 +- cmd/ts/main.go | 2 +- encoding/form/proto_decode.go | 224 ++++++++++++ encoding/form/proto_encode.go | 333 ++++++++++++++++++ encoding/form/well_known_types.go | 94 +++++ encoding/json/json.go | 67 ++++ errors/errors.go | 101 ++++-- errors/errors.pb.go | 226 ++++++++++++ errors/errors.proto | 16 + errors/warp.go | 16 + internal/endpoint/endpoint.go | 34 ++ internal/host/host.go | 92 +++++ middleware/jwt.go | 2 +- middleware/logger.go | 3 +- options.go | 2 +- test/main.go | 28 +- third_party/errors/errors.proto | 16 + .../google/api/annotations.proto | 0 .../google/api/client.proto | 0 .../google/api/field_behavior.proto | 0 {proto => third_party}/google/api/http.proto | 0 .../google/api/httpbody.proto | 0 .../google/protobuf/any.proto | 0 .../google/protobuf/api.proto | 0 .../google/protobuf/compiler/plugin.proto | 0 .../google/protobuf/descriptor.proto | 0 .../google/protobuf/duration.proto | 0 .../google/protobuf/empty.proto | 0 .../google/protobuf/field_mask.proto | 0 .../google/protobuf/source_context.proto | 0 .../google/protobuf/struct.proto | 0 .../google/protobuf/timestamp.proto | 0 .../google/protobuf/type.proto | 0 .../google/protobuf/wrappers.proto | 0 .../validate/validate.proto | 0 transport/http/calloption.go | 109 ++++++ transport/http/codec.go | 2 +- transport/http/router.go | 2 +- transport/http/server.go | 89 +++-- transport/http/status/status.go | 96 +++++ transport/http/transport.go | 18 + 42 files changed, 1523 insertions(+), 68 deletions(-) create mode 100644 encoding/form/proto_decode.go create mode 100644 encoding/form/proto_encode.go create mode 100644 encoding/form/well_known_types.go create mode 100644 encoding/json/json.go create mode 100644 errors/errors.pb.go create mode 100644 errors/errors.proto create mode 100644 errors/warp.go create mode 100644 internal/endpoint/endpoint.go create mode 100644 internal/host/host.go create mode 100644 third_party/errors/errors.proto rename {proto => third_party}/google/api/annotations.proto (100%) rename {proto => third_party}/google/api/client.proto (100%) rename {proto => third_party}/google/api/field_behavior.proto (100%) rename {proto => third_party}/google/api/http.proto (100%) rename {proto => third_party}/google/api/httpbody.proto (100%) rename {proto => third_party}/google/protobuf/any.proto (100%) rename {proto => third_party}/google/protobuf/api.proto (100%) rename {proto => third_party}/google/protobuf/compiler/plugin.proto (100%) rename {proto => third_party}/google/protobuf/descriptor.proto (100%) rename {proto => third_party}/google/protobuf/duration.proto (100%) rename {proto => third_party}/google/protobuf/empty.proto (100%) rename {proto => third_party}/google/protobuf/field_mask.proto (100%) rename {proto => third_party}/google/protobuf/source_context.proto (100%) rename {proto => third_party}/google/protobuf/struct.proto (100%) rename {proto => third_party}/google/protobuf/timestamp.proto (100%) rename {proto => third_party}/google/protobuf/type.proto (100%) rename {proto => third_party}/google/protobuf/wrappers.proto (100%) rename {proto => third_party}/validate/validate.proto (100%) create mode 100644 transport/http/calloption.go create mode 100644 transport/http/status/status.go diff --git a/app.go b/app.go index 2820551..0b0ae69 100644 --- a/app.go +++ b/app.go @@ -1,13 +1,16 @@ -package protoc_gen_kit +package kit import ( "context" "errors" - "git.diulo.com/mogfee/protoc-gen-kit/log" - "git.diulo.com/mogfee/protoc-gen-kit/registry" - "git.diulo.com/mogfee/protoc-gen-kit/transport" + "git.diulo.com/mogfee/kit/log" + "git.diulo.com/mogfee/kit/registry" + "git.diulo.com/mogfee/kit/transport" "github.com/google/uuid" "golang.org/x/sync/errgroup" + + _ "git.diulo.com/mogfee/kit/encoding/form" + _ "git.diulo.com/mogfee/kit/encoding/json" "os" "os/signal" "sync" diff --git a/cmd/kit/main.go b/cmd/kit/main.go index 9ccb233..848fa91 100644 --- a/cmd/kit/main.go +++ b/cmd/kit/main.go @@ -1,7 +1,7 @@ package main import ( - protogen2 "git.diulo.com/mogfee/protoc-gen-kit/protogen" + protogen2 "git.diulo.com/mogfee/kit/protogen" "google.golang.org/protobuf/compiler/protogen" ) @@ -24,9 +24,9 @@ func (u *Kit) Generate(plugin *protogen.Plugin) error { return nil } u.addImports("context") - u.addImports("git.diulo.com/mogfee/protoc-gen-kit/middleware") - u.addImports("git.diulo.com/mogfee/protoc-gen-kit/pkg/response") - u.addImports("git.diulo.com/mogfee/protoc-gen-kit/pkg/errors") + u.addImports("git.diulo.com/mogfee/kit/middleware") + u.addImports("git.diulo.com/mogfee/kit/pkg/response") + u.addImports("git.diulo.com/mogfee/kit/pkg/errors") u.addImports("github.com/gin-gonic/gin") for _, f := range plugin.Files { if len(f.Services) == 0 { diff --git a/cmd/ts/main.go b/cmd/ts/main.go index e4a7a42..39a2bba 100644 --- a/cmd/ts/main.go +++ b/cmd/ts/main.go @@ -2,7 +2,7 @@ package main import ( "fmt" - protogen2 "git.diulo.com/mogfee/protoc-gen-kit/protogen" + protogen2 "git.diulo.com/mogfee/kit/protogen" "google.golang.org/protobuf/compiler/protogen" "strings" ) diff --git a/encoding/form/proto_decode.go b/encoding/form/proto_decode.go new file mode 100644 index 0000000..550648d --- /dev/null +++ b/encoding/form/proto_decode.go @@ -0,0 +1,224 @@ +package form + +import ( + "encoding/base64" + "fmt" + "net/url" + "reflect" + "strconv" + "strings" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/fieldmaskpb" +) + +// EncodeValues encode a message into url values. +func EncodeValues(msg interface{}) (url.Values, error) { + if msg == nil || (reflect.ValueOf(msg).Kind() == reflect.Ptr && reflect.ValueOf(msg).IsNil()) { + return url.Values{}, nil + } + if v, ok := msg.(proto.Message); ok { + u := make(url.Values) + err := encodeByField(u, "", v.ProtoReflect()) + if err != nil { + return nil, err + } + return u, nil + } + return encoder.Encode(msg) +} + +func encodeByField(u url.Values, path string, m protoreflect.Message) (finalErr error) { + m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { + var ( + key string + newPath string + ) + if fd.HasJSONName() { + key = fd.JSONName() + } else { + key = fd.TextName() + } + if path == "" { + newPath = key + } else { + newPath = path + "." + key + } + if of := fd.ContainingOneof(); of != nil { + if f := m.WhichOneof(of); f != nil && f != fd { + return true + } + } + switch { + case fd.IsList(): + if v.List().Len() > 0 { + list, err := encodeRepeatedField(fd, v.List()) + if err != nil { + finalErr = err + return false + } + for _, item := range list { + u.Add(newPath, item) + } + } + case fd.IsMap(): + if v.Map().Len() > 0 { + m, err := encodeMapField(fd, v.Map()) + if err != nil { + finalErr = err + return false + } + for k, value := range m { + u.Set(fmt.Sprintf("%s[%s]", newPath, k), value) + } + } + case (fd.Kind() == protoreflect.MessageKind) || (fd.Kind() == protoreflect.GroupKind): + value, err := encodeMessage(fd.Message(), v) + if err == nil { + u.Set(newPath, value) + return true + } + if err = encodeByField(u, newPath, v.Message()); err != nil { + finalErr = err + return false + } + default: + value, err := EncodeField(fd, v) + if err != nil { + finalErr = err + return false + } + u.Set(newPath, value) + } + return true + }) + return +} + +func encodeRepeatedField(fieldDescriptor protoreflect.FieldDescriptor, list protoreflect.List) ([]string, error) { + var values []string + for i := 0; i < list.Len(); i++ { + value, err := EncodeField(fieldDescriptor, list.Get(i)) + if err != nil { + return nil, err + } + values = append(values, value) + } + return values, nil +} + +func encodeMapField(fieldDescriptor protoreflect.FieldDescriptor, mp protoreflect.Map) (map[string]string, error) { + m := make(map[string]string) + mp.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool { + key, err := EncodeField(fieldDescriptor.MapValue(), k.Value()) + if err != nil { + return false + } + value, err := EncodeField(fieldDescriptor.MapValue(), v) + if err != nil { + return false + } + m[key] = value + return true + }) + + return m, nil +} + +// EncodeField encode proto message filed +func EncodeField(fieldDescriptor protoreflect.FieldDescriptor, value protoreflect.Value) (string, error) { + switch fieldDescriptor.Kind() { + case protoreflect.BoolKind: + return strconv.FormatBool(value.Bool()), nil + case protoreflect.EnumKind: + if fieldDescriptor.Enum().FullName() == "google.protobuf.NullValue" { + return nullStr, nil + } + desc := fieldDescriptor.Enum().Values().ByNumber(value.Enum()) + return string(desc.Name()), nil + case protoreflect.StringKind: + return value.String(), nil + case protoreflect.BytesKind: + return base64.URLEncoding.EncodeToString(value.Bytes()), nil + case protoreflect.MessageKind, protoreflect.GroupKind: + return encodeMessage(fieldDescriptor.Message(), value) + default: + return fmt.Sprint(value.Interface()), nil + } +} + +// encodeMessage marshals the fields in the given protoreflect.Message. +// If the typeURL is non-empty, then a synthetic "@type" field is injected +// containing the URL as the value. +func encodeMessage(msgDescriptor protoreflect.MessageDescriptor, value protoreflect.Value) (string, error) { + switch msgDescriptor.FullName() { + case timestampMessageFullname: + return marshalTimestamp(value.Message()) + case durationMessageFullname: + return marshalDuration(value.Message()) + case bytesMessageFullname: + return marshalBytes(value.Message()) + case "google.protobuf.DoubleValue", "google.protobuf.FloatValue", "google.protobuf.Int64Value", "google.protobuf.Int32Value", + "google.protobuf.UInt64Value", "google.protobuf.UInt32Value", "google.protobuf.BoolValue", "google.protobuf.StringValue": + fd := msgDescriptor.Fields() + v := value.Message().Get(fd.ByName("value")) + return fmt.Sprint(v.Interface()), nil + case fieldMaskFullName: + m, ok := value.Message().Interface().(*fieldmaskpb.FieldMask) + if !ok || m == nil { + return "", nil + } + for i, v := range m.Paths { + m.Paths[i] = jsonCamelCase(v) + } + return strings.Join(m.Paths, ","), nil + default: + return "", fmt.Errorf("unsupported message type: %q", string(msgDescriptor.FullName())) + } +} + +// EncodeFieldMask return field mask name=paths +func EncodeFieldMask(m protoreflect.Message) (query string) { + m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { + if fd.Kind() == protoreflect.MessageKind { + if msg := fd.Message(); msg.FullName() == fieldMaskFullName { + value, err := encodeMessage(msg, v) + if err != nil { + return false + } + if fd.HasJSONName() { + query = fd.JSONName() + "=" + value + } else { + query = fd.TextName() + "=" + value + } + return false + } + } + return true + }) + return +} + +// jsonCamelCase converts a snake_case identifier to a camelCase identifier, +// according to the protobuf JSON specification. +// references: https://github.com/protocolbuffers/protobuf-go/blob/master/encoding/protojson/well_known_types.go#L842 +func jsonCamelCase(s string) string { + var b []byte + var wasUnderscore bool + for i := 0; i < len(s); i++ { // proto identifiers are always ASCII + c := s[i] + if c != '_' { + if wasUnderscore && isASCIILower(c) { + c -= 'a' - 'A' // convert to uppercase + } + b = append(b, c) + } + wasUnderscore = c == '_' + } + return string(b) +} + +func isASCIILower(c byte) bool { + return 'a' <= c && c <= 'z' +} diff --git a/encoding/form/proto_encode.go b/encoding/form/proto_encode.go new file mode 100644 index 0000000..1620162 --- /dev/null +++ b/encoding/form/proto_encode.go @@ -0,0 +1,333 @@ +package form + +import ( + "encoding/base64" + "errors" + "fmt" + "net/url" + "strconv" + "strings" + "time" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/fieldmaskpb" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +// DecodeValues decode url value into proto message. +func DecodeValues(msg proto.Message, values url.Values) error { + for key, values := range values { + if err := populateFieldValues(msg.ProtoReflect(), strings.Split(key, "."), values); err != nil { + return err + } + } + return nil +} + +func populateFieldValues(v protoreflect.Message, fieldPath []string, values []string) error { + if len(fieldPath) < 1 { + return errors.New("no field path") + } + if len(values) < 1 { + return errors.New("no value provided") + } + + var fd protoreflect.FieldDescriptor + for i, fieldName := range fieldPath { + if fd = getFieldDescriptor(v, fieldName); fd == nil { + // ignore unexpected field. + return nil + } + + if i == len(fieldPath)-1 { + break + } + + if fd.Message() == nil || fd.Cardinality() == protoreflect.Repeated { + if fd.IsMap() && len(fieldPath) > 1 { + // post subfield + return populateMapField(fd, v.Mutable(fd).Map(), []string{fieldPath[1]}, values) + } + return fmt.Errorf("invalid path: %q is not a message", fieldName) + } + + v = v.Mutable(fd).Message() + } + if of := fd.ContainingOneof(); of != nil { + if f := v.WhichOneof(of); f != nil { + return fmt.Errorf("field already set for oneof %q", of.FullName().Name()) + } + } + switch { + case fd.IsList(): + return populateRepeatedField(fd, v.Mutable(fd).List(), values) + case fd.IsMap(): + return populateMapField(fd, v.Mutable(fd).Map(), fieldPath, values) + } + if len(values) > 1 { + return fmt.Errorf("too many values for field %q: %s", fd.FullName().Name(), strings.Join(values, ", ")) + } + return populateField(fd, v, values[0]) +} + +func getFieldDescriptor(v protoreflect.Message, fieldName string) protoreflect.FieldDescriptor { + fields := v.Descriptor().Fields() + var fd protoreflect.FieldDescriptor + if fd = getDescriptorByFieldAndName(fields, fieldName); fd == nil { + if v.Descriptor().FullName() == structMessageFullname { + fd = fields.ByNumber(structFieldsFieldNumber) + } else if len(fieldName) > 2 && strings.HasSuffix(fieldName, "[]") { + fd = getDescriptorByFieldAndName(fields, strings.TrimSuffix(fieldName, "[]")) + } + } + return fd +} + +func getDescriptorByFieldAndName(fields protoreflect.FieldDescriptors, fieldName string) protoreflect.FieldDescriptor { + var fd protoreflect.FieldDescriptor + if fd = fields.ByName(protoreflect.Name(fieldName)); fd == nil { + fd = fields.ByJSONName(fieldName) + } + return fd +} + +func populateField(fd protoreflect.FieldDescriptor, v protoreflect.Message, value string) error { + if value == "" { + return nil + } + val, err := parseField(fd, value) + if err != nil { + return fmt.Errorf("parsing field %q: %w", fd.FullName().Name(), err) + } + v.Set(fd, val) + return nil +} + +func populateRepeatedField(fd protoreflect.FieldDescriptor, list protoreflect.List, values []string) error { + for _, value := range values { + v, err := parseField(fd, value) + if err != nil { + return fmt.Errorf("parsing list %q: %w", fd.FullName().Name(), err) + } + list.Append(v) + } + return nil +} + +func populateMapField(fd protoreflect.FieldDescriptor, mp protoreflect.Map, fieldPath []string, values []string) error { + // post sub key. + nkey := len(fieldPath) - 1 + key, err := parseField(fd.MapKey(), fieldPath[nkey]) + if err != nil { + return fmt.Errorf("parsing map key %q: %w", fd.FullName().Name(), err) + } + vkey := len(values) - 1 + value, err := parseField(fd.MapValue(), values[vkey]) + if err != nil { + return fmt.Errorf("parsing map value %q: %w", fd.FullName().Name(), err) + } + mp.Set(key.MapKey(), value) + return nil +} + +func parseField(fd protoreflect.FieldDescriptor, value string) (protoreflect.Value, error) { + switch fd.Kind() { + case protoreflect.BoolKind: + v, err := strconv.ParseBool(value) + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfBool(v), nil + case protoreflect.EnumKind: + enum, err := protoregistry.GlobalTypes.FindEnumByName(fd.Enum().FullName()) + switch { + case errors.Is(err, protoregistry.NotFound): + return protoreflect.Value{}, fmt.Errorf("enum %q is not registered", fd.Enum().FullName()) + case err != nil: + return protoreflect.Value{}, fmt.Errorf("failed to look up enum: %w", err) + } + v := enum.Descriptor().Values().ByName(protoreflect.Name(value)) + if v == nil { + i, err := strconv.ParseInt(value, 10, 32) //nolint:gomnd + if err != nil { + return protoreflect.Value{}, fmt.Errorf("%q is not a valid value", value) + } + v = enum.Descriptor().Values().ByNumber(protoreflect.EnumNumber(i)) + if v == nil { + return protoreflect.Value{}, fmt.Errorf("%q is not a valid value", value) + } + } + return protoreflect.ValueOfEnum(v.Number()), nil + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: + v, err := strconv.ParseInt(value, 10, 32) //nolint:gomnd + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfInt32(int32(v)), nil + case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: + v, err := strconv.ParseInt(value, 10, 64) //nolint:gomnd + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfInt64(v), nil + case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: + v, err := strconv.ParseUint(value, 10, 32) //nolint:gomnd + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfUint32(uint32(v)), nil + case protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + v, err := strconv.ParseUint(value, 10, 64) //nolint:gomnd + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfUint64(v), nil + case protoreflect.FloatKind: + v, err := strconv.ParseFloat(value, 32) //nolint:gomnd + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfFloat32(float32(v)), nil + case protoreflect.DoubleKind: + v, err := strconv.ParseFloat(value, 64) //nolint:gomnd + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfFloat64(v), nil + case protoreflect.StringKind: + return protoreflect.ValueOfString(value), nil + case protoreflect.BytesKind: + v, err := base64.StdEncoding.DecodeString(value) + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfBytes(v), nil + case protoreflect.MessageKind, protoreflect.GroupKind: + return parseMessage(fd.Message(), value) + default: + panic(fmt.Sprintf("unknown field kind: %v", fd.Kind())) + } +} + +func parseMessage(md protoreflect.MessageDescriptor, value string) (protoreflect.Value, error) { + var msg proto.Message + switch md.FullName() { + case "google.protobuf.Timestamp": + if value == nullStr { + break + } + t, err := time.Parse(time.RFC3339Nano, value) + if err != nil { + return protoreflect.Value{}, err + } + msg = timestamppb.New(t) + case "google.protobuf.Duration": + if value == nullStr { + break + } + d, err := time.ParseDuration(value) + if err != nil { + return protoreflect.Value{}, err + } + msg = durationpb.New(d) + case "google.protobuf.DoubleValue": + v, err := strconv.ParseFloat(value, 64) //nolint:gomnd + if err != nil { + return protoreflect.Value{}, err + } + msg = wrapperspb.Double(v) + case "google.protobuf.FloatValue": + v, err := strconv.ParseFloat(value, 32) //nolint:gomnd + if err != nil { + return protoreflect.Value{}, err + } + msg = wrapperspb.Float(float32(v)) + case "google.protobuf.Int64Value": + v, err := strconv.ParseInt(value, 10, 64) //nolint:gomnd + if err != nil { + return protoreflect.Value{}, err + } + msg = wrapperspb.Int64(v) + case "google.protobuf.Int32Value": + v, err := strconv.ParseInt(value, 10, 32) //nolint:gomnd + if err != nil { + return protoreflect.Value{}, err + } + msg = wrapperspb.Int32(int32(v)) + case "google.protobuf.UInt64Value": + v, err := strconv.ParseUint(value, 10, 64) //nolint:gomnd + if err != nil { + return protoreflect.Value{}, err + } + msg = wrapperspb.UInt64(v) + case "google.protobuf.UInt32Value": + v, err := strconv.ParseUint(value, 10, 32) //nolint:gomnd + if err != nil { + return protoreflect.Value{}, err + } + msg = wrapperspb.UInt32(uint32(v)) + case "google.protobuf.BoolValue": + v, err := strconv.ParseBool(value) + if err != nil { + return protoreflect.Value{}, err + } + msg = wrapperspb.Bool(v) + case "google.protobuf.StringValue": + msg = wrapperspb.String(value) + case "google.protobuf.BytesValue": + v, err := base64.StdEncoding.DecodeString(value) + if err != nil { + if v, err = base64.URLEncoding.DecodeString(value); err != nil { + return protoreflect.Value{}, err + } + } + msg = wrapperspb.Bytes(v) + case "google.protobuf.FieldMask": + fm := &fieldmaskpb.FieldMask{} + for _, fv := range strings.Split(value, ",") { + fm.Paths = append(fm.Paths, jsonSnakeCase(fv)) + } + msg = fm + case "google.protobuf.Value": + fm, err := structpb.NewValue(value) + if err != nil { + return protoreflect.Value{}, err + } + msg = fm + case "google.protobuf.Struct": + var v structpb.Struct + if err := protojson.Unmarshal([]byte(value), &v); err != nil { + return protoreflect.Value{}, err + } + msg = &v + default: + return protoreflect.Value{}, fmt.Errorf("unsupported message type: %q", string(md.FullName())) + } + return protoreflect.ValueOfMessage(msg.ProtoReflect()), nil +} + +// jsonSnakeCase converts a camelCase identifier to a snake_case identifier, +// according to the protobuf JSON specification. +// references: https://github.com/protocolbuffers/protobuf-go/blob/master/encoding/protojson/well_known_types.go#L864 +func jsonSnakeCase(s string) string { + var b []byte + for i := 0; i < len(s); i++ { // proto identifiers are always ASCII + c := s[i] + if isASCIIUpper(c) { + b = append(b, '_') + c += 'a' - 'A' // convert to lowercase + } + b = append(b, c) + } + return string(b) +} + +func isASCIIUpper(c byte) bool { + return 'A' <= c && c <= 'Z' +} diff --git a/encoding/form/well_known_types.go b/encoding/form/well_known_types.go new file mode 100644 index 0000000..8b0894d --- /dev/null +++ b/encoding/form/well_known_types.go @@ -0,0 +1,94 @@ +package form + +import ( + "encoding/base64" + "fmt" + "math" + "strings" + "time" + + "google.golang.org/protobuf/reflect/protoreflect" +) + +const ( + // timestamp + timestampMessageFullname protoreflect.FullName = "google.protobuf.Timestamp" + maxTimestampSeconds = 253402300799 + minTimestampSeconds = -6213559680013 + timestampSecondsFieldNumber protoreflect.FieldNumber = 1 + timestampNanosFieldNumber protoreflect.FieldNumber = 2 + + // duration + durationMessageFullname protoreflect.FullName = "google.protobuf.Duration" + secondsInNanos = 999999999 + durationSecondsFieldNumber protoreflect.FieldNumber = 1 + durationNanosFieldNumber protoreflect.FieldNumber = 2 + + // bytes + bytesMessageFullname protoreflect.FullName = "google.protobuf.BytesValue" + bytesValueFieldNumber protoreflect.FieldNumber = 1 + + // google.protobuf.Struct. + structMessageFullname protoreflect.FullName = "google.protobuf.Struct" + structFieldsFieldNumber protoreflect.FieldNumber = 1 + + fieldMaskFullName protoreflect.FullName = "google.protobuf.FieldMask" +) + +func marshalTimestamp(m protoreflect.Message) (string, error) { + fds := m.Descriptor().Fields() + fdSeconds := fds.ByNumber(timestampSecondsFieldNumber) + fdNanos := fds.ByNumber(timestampNanosFieldNumber) + + secsVal := m.Get(fdSeconds) + nanosVal := m.Get(fdNanos) + secs := secsVal.Int() + nanos := nanosVal.Int() + if secs < minTimestampSeconds || secs > maxTimestampSeconds { + return "", fmt.Errorf("%s: seconds out of range %v", timestampMessageFullname, secs) + } + if nanos < 0 || nanos > secondsInNanos { + return "", fmt.Errorf("%s: nanos out of range %v", timestampMessageFullname, nanos) + } + // Uses RFC 3339, where generated output will be Z-normalized and uses 0, 3, + // 6 or 9 fractional digits. + t := time.Unix(secs, nanos).UTC() + x := t.Format("2006-01-02T15:04:05.000000000") + x = strings.TrimSuffix(x, "000") + x = strings.TrimSuffix(x, "000") + x = strings.TrimSuffix(x, ".000") + return x + "Z", nil +} + +func marshalDuration(m protoreflect.Message) (string, error) { + fds := m.Descriptor().Fields() + fdSeconds := fds.ByNumber(durationSecondsFieldNumber) + fdNanos := fds.ByNumber(durationNanosFieldNumber) + + secsVal := m.Get(fdSeconds) + nanosVal := m.Get(fdNanos) + secs := secsVal.Int() + nanos := nanosVal.Int() + d := time.Duration(secs) * time.Second + overflow := d/time.Second != time.Duration(secs) + d += time.Duration(nanos) * time.Nanosecond + overflow = overflow || (secs < 0 && nanos < 0 && d > 0) + overflow = overflow || (secs > 0 && nanos > 0 && d < 0) + if overflow { + switch { + case secs < 0: + return time.Duration(math.MinInt64).String(), nil + case secs > 0: + return time.Duration(math.MaxInt64).String(), nil + } + } + return d.String(), nil +} + +func marshalBytes(m protoreflect.Message) (string, error) { + fds := m.Descriptor().Fields() + fdBytes := fds.ByNumber(bytesValueFieldNumber) + bytesVal := m.Get(fdBytes) + val := bytesVal.Bytes() + return base64.StdEncoding.EncodeToString(val), nil +} diff --git a/encoding/json/json.go b/encoding/json/json.go new file mode 100644 index 0000000..f5b05a6 --- /dev/null +++ b/encoding/json/json.go @@ -0,0 +1,67 @@ +package json + +import ( + "encoding/json" + "git.diulo.com/mogfee/kit/encoding" + "reflect" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +// Name is the name registered for the json codec. +const Name = "json" + +var ( + // MarshalOptions is a configurable JSON format marshaller. + MarshalOptions = protojson.MarshalOptions{ + EmitUnpopulated: true, + } + // UnmarshalOptions is a configurable JSON format parser. + UnmarshalOptions = protojson.UnmarshalOptions{ + DiscardUnknown: true, + } +) + +func init() { + encoding.RegisterCodec(codec{}) +} + +// codec is a Codec implementation with json. +type codec struct{} + +func (codec) Marshal(v interface{}) ([]byte, error) { + switch m := v.(type) { + case json.Marshaler: + return m.MarshalJSON() + case proto.Message: + return MarshalOptions.Marshal(m) + default: + return json.Marshal(m) + } +} + +func (codec) Unmarshal(data []byte, v interface{}) error { + switch m := v.(type) { + case json.Unmarshaler: + return m.UnmarshalJSON(data) + case proto.Message: + return UnmarshalOptions.Unmarshal(data, m) + default: + rv := reflect.ValueOf(v) + for rv := rv; rv.Kind() == reflect.Ptr; { + if rv.IsNil() { + rv.Set(reflect.New(rv.Type().Elem())) + } + rv = rv.Elem() + } + if m, ok := reflect.Indirect(rv).Interface().(proto.Message); ok { + return UnmarshalOptions.Unmarshal(data, m) + } + return json.Unmarshal(data, m) + } +} + +func (codec) Name() string { + return Name +} diff --git a/errors/errors.go b/errors/errors.go index 9d5fe1a..8ae26d0 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -2,8 +2,9 @@ package errors import ( "errors" + "fmt" + httpstatus "git.diulo.com/mogfee/kit/transport/http/status" "google.golang.org/genproto/googleapis/rpc/errdetails" - "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -15,36 +16,69 @@ const ( ) type Error struct { - Status int - Message string - Reason string - Metadata map[string]string + Status + cause error } -func (e *Error) WithMetadata(data map[string]string) *Error { - err := Clone(e) - err.Metadata = data - return err +func (e *Error) Error() string { + return fmt.Sprintf("error: code = %d reason = %s message = %s metadata = %v cause = %v", e.Code, e.Reason, e.Message, e.Metadata, e.cause) } -func (s *Error) Error() string { - return s.Message +func (e *Error) Unwarp() error { + return e.cause } -func New(code int, reason, message string) *Error { - return &Error{ - Status: code, - Message: message, - Reason: reason, - Metadata: map[string]string{}, +func (e *Error) Is(err error) bool { + if se := new(Error); errors.As(err, &se) { + return se.Code == e.Code && se.Reason == e.Reason } + return false } +func (e *Error) WithCause(cause error) *Error { + err := Clone(e) + err.cause = cause + return err +} +func (e *Error) WithMetadata(md map[string]string) *Error { + err := Clone(e) + err.Metadata = md + return err +} + func (e *Error) GRPCStatus() *status.Status { - s, _ := status.New(codes.Code(e.Status), e.Message). + s, _ := status.New(httpstatus.ToGRPCCode(int(e.Code)), e.Message). WithDetails(&errdetails.ErrorInfo{ Reason: e.Reason, Metadata: e.Metadata, }) return s } +func New(code int, reason, message string) *Error { + return &Error{ + Status: Status{ + Code: int32(code), + Reason: reason, + Message: message, + }, + } +} +func Newf(code int, reason string, format string, a ...any) *Error { + return New(code, reason, fmt.Sprintf(format, a...)) +} +func Errorf(code int, reason string, format string, a ...any) *Error { + return New(code, reason, fmt.Sprintf(format, a...)) +} +func Code(err error) int { + if err == nil { + return 200 + } + return int(FromError(err).Code) +} +func Reason(err error) string { + if err == nil { + return UnknownReason + } + return FromError(err).Reason +} + func Clone(err *Error) *Error { if err == nil { return nil @@ -54,17 +88,14 @@ func Clone(err *Error) *Error { metadata[k] = v } return &Error{ - Status: err.Status, - Reason: err.Reason, - Message: err.Message, - Metadata: metadata, - } -} -func Code(err error) int { - if err == nil { - return 200 + cause: err.cause, + Status: Status{ + Code: err.Code, + Reason: err.Reason, + Message: err.Message, + Metadata: metadata, + }, } - return FromError(err).Status } func FromError(err error) *Error { @@ -74,5 +105,17 @@ func FromError(err error) *Error { if se := new(Error); errors.As(err, &se) { return se } - return New(UnknownCode, UnknownReason, err.Error()) + gs, ok := status.FromError(err) + if !ok { + return New(UnknownCode, UnknownReason, err.Error()) + } + ret := New(httpstatus.FromGRPCCode(gs.Code()), UnknownReason, gs.Message()) + for _, detail := range gs.Details() { + switch d := detail.(type) { + case *errdetails.ErrorInfo: + ret.Reason = d.Reason + return ret.WithMetadata(d.Metadata) + } + } + return ret } diff --git a/errors/errors.pb.go b/errors/errors.pb.go new file mode 100644 index 0000000..541600d --- /dev/null +++ b/errors/errors.pb.go @@ -0,0 +1,226 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v4.22.0 +// source: errors.proto + +package errors + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + descriptorpb "google.golang.org/protobuf/types/descriptorpb" + reflect "reflect" + sync "sync" +) + +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 Status struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` + Metadata map[string]string `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *Status) Reset() { + *x = Status{} + if protoimpl.UnsafeEnabled { + mi := &file_errors_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Status) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Status) ProtoMessage() {} + +func (x *Status) ProtoReflect() protoreflect.Message { + mi := &file_errors_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Status.ProtoReflect.Descriptor instead. +func (*Status) Descriptor() ([]byte, []int) { + return file_errors_proto_rawDescGZIP(), []int{0} +} + +func (x *Status) GetCode() int32 { + if x != nil { + return x.Code + } + return 0 +} + +func (x *Status) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *Status) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *Status) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +var file_errors_proto_extTypes = []protoimpl.ExtensionInfo{ + { + ExtendedType: (*descriptorpb.EnumOptions)(nil), + ExtensionType: (*int32)(nil), + Field: 1108, + Name: "errors.default_code", + Tag: "varint,1108,opt,name=default_code", + Filename: "errors.proto", + }, + { + ExtendedType: (*descriptorpb.EnumValueOptions)(nil), + ExtensionType: (*int32)(nil), + Field: 1109, + Name: "errors.code", + Tag: "varint,1109,opt,name=code", + Filename: "errors.proto", + }, +} + +// Extension fields to descriptorpb.EnumOptions. +var ( + // optional int32 default_code = 1108; + E_DefaultCode = &file_errors_proto_extTypes[0] +) + +// Extension fields to descriptorpb.EnumValueOptions. +var ( + // optional int32 code = 1109; + E_Code = &file_errors_proto_extTypes[1] +) + +var File_errors_proto protoreflect.FileDescriptor + +var file_errors_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc5, 0x01, 0x0a, 0x06, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, + 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x38, 0x0a, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x3a, 0x40, 0x0a, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x63, 0x6f, 0x64, 0x65, + 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xd4, + 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, + 0x64, 0x65, 0x3a, 0x36, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x21, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6e, 0x75, + 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xd5, 0x08, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x42, 0x28, 0x5a, 0x26, 0x67, 0x69, + 0x74, 0x2e, 0x64, 0x69, 0x75, 0x6c, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6d, 0x6f, 0x67, 0x66, + 0x65, 0x65, 0x2f, 0x6b, 0x69, 0x74, 0x2f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x3b, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_errors_proto_rawDescOnce sync.Once + file_errors_proto_rawDescData = file_errors_proto_rawDesc +) + +func file_errors_proto_rawDescGZIP() []byte { + file_errors_proto_rawDescOnce.Do(func() { + file_errors_proto_rawDescData = protoimpl.X.CompressGZIP(file_errors_proto_rawDescData) + }) + return file_errors_proto_rawDescData +} + +var file_errors_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_errors_proto_goTypes = []interface{}{ + (*Status)(nil), // 0: errors.Status + nil, // 1: errors.Status.MetadataEntry + (*descriptorpb.EnumOptions)(nil), // 2: google.protobuf.EnumOptions + (*descriptorpb.EnumValueOptions)(nil), // 3: google.protobuf.EnumValueOptions +} +var file_errors_proto_depIdxs = []int32{ + 1, // 0: errors.Status.metadata:type_name -> errors.Status.MetadataEntry + 2, // 1: errors.default_code:extendee -> google.protobuf.EnumOptions + 3, // 2: errors.code:extendee -> google.protobuf.EnumValueOptions + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 1, // [1:3] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_errors_proto_init() } +func file_errors_proto_init() { + if File_errors_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_errors_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Status); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_errors_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 2, + NumServices: 0, + }, + GoTypes: file_errors_proto_goTypes, + DependencyIndexes: file_errors_proto_depIdxs, + MessageInfos: file_errors_proto_msgTypes, + ExtensionInfos: file_errors_proto_extTypes, + }.Build() + File_errors_proto = out.File + file_errors_proto_rawDesc = nil + file_errors_proto_goTypes = nil + file_errors_proto_depIdxs = nil +} diff --git a/errors/errors.proto b/errors/errors.proto new file mode 100644 index 0000000..0572677 --- /dev/null +++ b/errors/errors.proto @@ -0,0 +1,16 @@ +syntax="proto3"; +package errors; +option go_package="git.diulo.com/mogfee/kit/errors;errors"; +import "google/protobuf/descriptor.proto"; +message Status{ + int32 code=1; + string reason=2; + string message=3; + mapmetadata=4; +} +extend google.protobuf.EnumOptions{ + int32 default_code=1108; +} +extend google.protobuf.EnumValueOptions{ + int32 code=1109; +} \ No newline at end of file diff --git a/errors/warp.go b/errors/warp.go new file mode 100644 index 0000000..24f56f0 --- /dev/null +++ b/errors/warp.go @@ -0,0 +1,16 @@ +package errors + +import ( + stderrors "errors" +) + +func Is(err, target error) bool { + return stderrors.Is(err, target) +} + +func As(err error, target any) bool { + return stderrors.As(err, target) +} +func Unwarp(err error) error { + return stderrors.Unwrap(err) +} diff --git a/internal/endpoint/endpoint.go b/internal/endpoint/endpoint.go new file mode 100644 index 0000000..160b694 --- /dev/null +++ b/internal/endpoint/endpoint.go @@ -0,0 +1,34 @@ +package endpoint + +import ( + "net/url" +) + +// NewEndpoint new an Endpoint URL. +func NewEndpoint(scheme, host string) *url.URL { + return &url.URL{Scheme: scheme, Host: host} +} + +// ParseEndpoint parses an Endpoint URL. +func ParseEndpoint(endpoints []string, scheme string) (string, error) { + for _, e := range endpoints { + u, err := url.Parse(e) + if err != nil { + return "", err + } + + if u.Scheme == scheme { + return u.Host, nil + } + } + return "", nil +} + +// Scheme is the scheme of endpoint url. +// examples: scheme="http",isSecure=true get "https" +func Scheme(scheme string, isSecure bool) string { + if isSecure { + return scheme + "s" + } + return scheme +} diff --git a/internal/host/host.go b/internal/host/host.go new file mode 100644 index 0000000..774f7ae --- /dev/null +++ b/internal/host/host.go @@ -0,0 +1,92 @@ +package host + +import ( + "fmt" + "net" + "strconv" +) + +// ExtractHostPort from address +func ExtractHostPort(addr string) (host string, port uint64, err error) { + var ports string + host, ports, err = net.SplitHostPort(addr) + if err != nil { + return + } + port, err = strconv.ParseUint(ports, 10, 16) //nolint:gomnd + return +} + +func isValidIP(addr string) bool { + ip := net.ParseIP(addr) + return ip.IsGlobalUnicast() && !ip.IsInterfaceLocalMulticast() +} + +// Port return a real port. +func Port(lis net.Listener) (int, bool) { + if addr, ok := lis.Addr().(*net.TCPAddr); ok { + return addr.Port, true + } + return 0, false +} + +// Extract returns a private addr and port. +func Extract(hostPort string, lis net.Listener) (string, error) { + addr, port, err := net.SplitHostPort(hostPort) + if err != nil && lis == nil { + return "", err + } + if lis != nil { + p, ok := Port(lis) + if !ok { + return "", fmt.Errorf("failed to extract port: %v", lis.Addr()) + } + port = strconv.Itoa(p) + } + if len(addr) > 0 && (addr != "0.0.0.0" && addr != "[::]" && addr != "::") { + return net.JoinHostPort(addr, port), nil + } + ifaces, err := net.Interfaces() + if err != nil { + return "", err + } + minIndex := int(^uint(0) >> 1) + ips := make([]net.IP, 0) + for _, iface := range ifaces { + if (iface.Flags & net.FlagUp) == 0 { + continue + } + if iface.Index >= minIndex && len(ips) != 0 { + continue + } + addrs, err := iface.Addrs() + if err != nil { + continue + } + for i, rawAddr := range addrs { + var ip net.IP + switch addr := rawAddr.(type) { + case *net.IPAddr: + ip = addr.IP + case *net.IPNet: + ip = addr.IP + default: + continue + } + if isValidIP(ip.String()) { + minIndex = iface.Index + if i == 0 { + ips = make([]net.IP, 0, 1) + } + ips = append(ips, ip) + if ip.To4() != nil { + break + } + } + } + } + if len(ips) != 0 { + return net.JoinHostPort(ips[len(ips)-1].String(), port), nil + } + return "", nil +} diff --git a/middleware/jwt.go b/middleware/jwt.go index 4fb3c9c..2c98aae 100644 --- a/middleware/jwt.go +++ b/middleware/jwt.go @@ -2,7 +2,7 @@ package middleware import ( "context" - "git.diulo.com/mogfee/protoc-gen-kit/constants" + "git.diulo.com/mogfee/kit/constants" "google.golang.org/grpc/metadata" ) diff --git a/middleware/logger.go b/middleware/logger.go index e5a290a..0d497d9 100644 --- a/middleware/logger.go +++ b/middleware/logger.go @@ -2,14 +2,13 @@ package middleware import ( "context" - "git.diulo.com/mogfee/protoc-gen-kit/log" + "git.diulo.com/mogfee/kit/log" ) func Logger(logger log.Logger) Middleware { //helper:=log.NewHelper(logger) return func(handler Handler) Handler { return func(ctx context.Context, a any) (any, error) { - //helper.Infof("") return handler(ctx, a) } } diff --git a/options.go b/options.go index c124b4b..40998fd 100644 --- a/options.go +++ b/options.go @@ -1,4 +1,4 @@ -package protoc_gen_kit +package kit import ( "context" diff --git a/test/main.go b/test/main.go index 0f1e71f..b06aecf 100644 --- a/test/main.go +++ b/test/main.go @@ -1,16 +1,36 @@ package main import ( + "errors" "fmt" - protoc_gen_kit "git.diulo.com/mogfee/protoc-gen-kit" + "git.diulo.com/mogfee/kit" + "git.diulo.com/mogfee/kit/transport/http" ) func main() { - app := protoc_gen_kit.New( - protoc_gen_kit.Name("user-server"), - protoc_gen_kit.Server()) + app := kit.New( + kit.Name("user-server"), + kit.Server(appServer())) fmt.Println("run start") app.Run() fmt.Println("run end") app.Stop() } +func appServer() *http.Server { + srv := http.NewServer(http.Address("localhost:9019")) + group := srv.Route("/") + group.Handle("GET", "/", func(ctx http.Context) error { + return errors.New("abc ") + }) + //group.GET("/", func(ctx http.Context) error { + // return ctx.String(200, "index page") + //}) + //group.GET("/hello", func(ctx http.Context) error { + // return ctx.String(200, "hello page") + //}) + //group.GET("/error", func(ctx http.Context) error { + // fmt.Println("err 1") + // return errors.New(400, "BAD_REQUEST", "") + //}) + return srv +} diff --git a/third_party/errors/errors.proto b/third_party/errors/errors.proto new file mode 100644 index 0000000..0572677 --- /dev/null +++ b/third_party/errors/errors.proto @@ -0,0 +1,16 @@ +syntax="proto3"; +package errors; +option go_package="git.diulo.com/mogfee/kit/errors;errors"; +import "google/protobuf/descriptor.proto"; +message Status{ + int32 code=1; + string reason=2; + string message=3; + mapmetadata=4; +} +extend google.protobuf.EnumOptions{ + int32 default_code=1108; +} +extend google.protobuf.EnumValueOptions{ + int32 code=1109; +} \ No newline at end of file diff --git a/proto/google/api/annotations.proto b/third_party/google/api/annotations.proto similarity index 100% rename from proto/google/api/annotations.proto rename to third_party/google/api/annotations.proto diff --git a/proto/google/api/client.proto b/third_party/google/api/client.proto similarity index 100% rename from proto/google/api/client.proto rename to third_party/google/api/client.proto diff --git a/proto/google/api/field_behavior.proto b/third_party/google/api/field_behavior.proto similarity index 100% rename from proto/google/api/field_behavior.proto rename to third_party/google/api/field_behavior.proto diff --git a/proto/google/api/http.proto b/third_party/google/api/http.proto similarity index 100% rename from proto/google/api/http.proto rename to third_party/google/api/http.proto diff --git a/proto/google/api/httpbody.proto b/third_party/google/api/httpbody.proto similarity index 100% rename from proto/google/api/httpbody.proto rename to third_party/google/api/httpbody.proto diff --git a/proto/google/protobuf/any.proto b/third_party/google/protobuf/any.proto similarity index 100% rename from proto/google/protobuf/any.proto rename to third_party/google/protobuf/any.proto diff --git a/proto/google/protobuf/api.proto b/third_party/google/protobuf/api.proto similarity index 100% rename from proto/google/protobuf/api.proto rename to third_party/google/protobuf/api.proto diff --git a/proto/google/protobuf/compiler/plugin.proto b/third_party/google/protobuf/compiler/plugin.proto similarity index 100% rename from proto/google/protobuf/compiler/plugin.proto rename to third_party/google/protobuf/compiler/plugin.proto diff --git a/proto/google/protobuf/descriptor.proto b/third_party/google/protobuf/descriptor.proto similarity index 100% rename from proto/google/protobuf/descriptor.proto rename to third_party/google/protobuf/descriptor.proto diff --git a/proto/google/protobuf/duration.proto b/third_party/google/protobuf/duration.proto similarity index 100% rename from proto/google/protobuf/duration.proto rename to third_party/google/protobuf/duration.proto diff --git a/proto/google/protobuf/empty.proto b/third_party/google/protobuf/empty.proto similarity index 100% rename from proto/google/protobuf/empty.proto rename to third_party/google/protobuf/empty.proto diff --git a/proto/google/protobuf/field_mask.proto b/third_party/google/protobuf/field_mask.proto similarity index 100% rename from proto/google/protobuf/field_mask.proto rename to third_party/google/protobuf/field_mask.proto diff --git a/proto/google/protobuf/source_context.proto b/third_party/google/protobuf/source_context.proto similarity index 100% rename from proto/google/protobuf/source_context.proto rename to third_party/google/protobuf/source_context.proto diff --git a/proto/google/protobuf/struct.proto b/third_party/google/protobuf/struct.proto similarity index 100% rename from proto/google/protobuf/struct.proto rename to third_party/google/protobuf/struct.proto diff --git a/proto/google/protobuf/timestamp.proto b/third_party/google/protobuf/timestamp.proto similarity index 100% rename from proto/google/protobuf/timestamp.proto rename to third_party/google/protobuf/timestamp.proto diff --git a/proto/google/protobuf/type.proto b/third_party/google/protobuf/type.proto similarity index 100% rename from proto/google/protobuf/type.proto rename to third_party/google/protobuf/type.proto diff --git a/proto/google/protobuf/wrappers.proto b/third_party/google/protobuf/wrappers.proto similarity index 100% rename from proto/google/protobuf/wrappers.proto rename to third_party/google/protobuf/wrappers.proto diff --git a/proto/validate/validate.proto b/third_party/validate/validate.proto similarity index 100% rename from proto/validate/validate.proto rename to third_party/validate/validate.proto diff --git a/transport/http/calloption.go b/transport/http/calloption.go new file mode 100644 index 0000000..7b0ed82 --- /dev/null +++ b/transport/http/calloption.go @@ -0,0 +1,109 @@ +package http + +import ( + "net/http" +) + +// CallOption configures a Call before it starts or extracts information from +// a Call after it completes. +type CallOption interface { + // before is called before the call is sent to any server. If before + // returns a non-nil error, the RPC fails with that error. + before(*callInfo) error + + // after is called after the call has completed. after cannot return an + // error, so any failures should be reported via output parameters. + after(*callInfo, *csAttempt) +} + +type callInfo struct { + contentType string + operation string + pathTemplate string +} + +// EmptyCallOption does not alter the Call configuration. +// It can be embedded in another structure to carry satellite data for use +// by interceptors. +type EmptyCallOption struct{} + +func (EmptyCallOption) before(*callInfo) error { return nil } +func (EmptyCallOption) after(*callInfo, *csAttempt) {} + +type csAttempt struct { + res *http.Response +} + +// ContentType with request content type. +func ContentType(contentType string) CallOption { + return ContentTypeCallOption{ContentType: contentType} +} + +// ContentTypeCallOption is BodyCallOption +type ContentTypeCallOption struct { + EmptyCallOption + ContentType string +} + +func (o ContentTypeCallOption) before(c *callInfo) error { + c.contentType = o.ContentType + return nil +} + +func defaultCallInfo(path string) callInfo { + return callInfo{ + contentType: "application/json", + operation: path, + pathTemplate: path, + } +} + +// Operation is serviceMethod call option +func Operation(operation string) CallOption { + return OperationCallOption{Operation: operation} +} + +// OperationCallOption is set ServiceMethod for client call +type OperationCallOption struct { + EmptyCallOption + Operation string +} + +func (o OperationCallOption) before(c *callInfo) error { + c.operation = o.Operation + return nil +} + +// PathTemplate is http path template +func PathTemplate(pattern string) CallOption { + return PathTemplateCallOption{Pattern: pattern} +} + +// PathTemplateCallOption is set path template for client call +type PathTemplateCallOption struct { + EmptyCallOption + Pattern string +} + +func (o PathTemplateCallOption) before(c *callInfo) error { + c.pathTemplate = o.Pattern + return nil +} + +// Header returns a CallOptions that retrieves the http response header +// from server reply. +func Header(header *http.Header) CallOption { + return HeaderCallOption{header: header} +} + +// HeaderCallOption is retrieve response header for client call +type HeaderCallOption struct { + EmptyCallOption + header *http.Header +} + +func (o HeaderCallOption) after(c *callInfo, cs *csAttempt) { + if cs.res != nil && cs.res.Header != nil { + *o.header = cs.res.Header + } +} diff --git a/transport/http/codec.go b/transport/http/codec.go index f89a29b..869e7b1 100644 --- a/transport/http/codec.go +++ b/transport/http/codec.go @@ -81,7 +81,7 @@ func DefaultErrorEncoder(w http.ResponseWriter, r *http.Request, err error) { return } w.Header().Set("Content-Type", httputil.ContentType(codec.Name())) - w.WriteHeader(int(se.Status)) + w.WriteHeader(int(se.Status.Code)) _, _ = w.Write(body) } func CodeForRequest(r *http.Request, name string) (encoding.Codec, bool) { diff --git a/transport/http/router.go b/transport/http/router.go index bc0a803..c1d1886 100644 --- a/transport/http/router.go +++ b/transport/http/router.go @@ -12,7 +12,7 @@ type RouteInfo struct { Path string Method string } -type HandlerFunc func(Context) error +type HandlerFunc func(ctx Context) error type Router struct { prefix string pool sync.Pool diff --git a/transport/http/server.go b/transport/http/server.go index 2b4e6ef..cccf280 100644 --- a/transport/http/server.go +++ b/transport/http/server.go @@ -3,7 +3,11 @@ package http import ( "context" "crypto/tls" + "errors" + "git.diulo.com/mogfee/kit/internal/endpoint" + "git.diulo.com/mogfee/kit/internal/host" "git.diulo.com/mogfee/kit/internal/matcher" + "git.diulo.com/mogfee/kit/log" "git.diulo.com/mogfee/kit/middleware" "git.diulo.com/mogfee/kit/transport" "github.com/gorilla/mux" @@ -156,6 +160,8 @@ func NewServer(opts ...ServerOption) *Server { decVars: DefaultRequestVars, decQuery: DefaultRequestQuery, decBody: DefaultRequestDecoder, + enc: DefaultResponseEncoder, + ene: DefaultErrorEncoder, strictSlash: true, router: mux.NewRouter(), } @@ -198,7 +204,7 @@ func (s *Server) WalkRoute(fn WalkRoutFunc) error { }) } func (s *Server) Kind() transport.Kind { - return transport.htt + return transport.KindHTTP } func (s *Server) Route(prefix string, filters ...FilterFunc) *Router { return newRouter(prefix, s, filters...) @@ -218,6 +224,38 @@ func (s *Server) HandleHeader(key, val string, h http.Handler) { func (s *Server) ServeHTTP(res http.ResponseWriter, req *http.Request) { s.Handler.ServeHTTP(res, req) } + +func (s *Server) Endpoint() (*url.URL, error) { + if err := s.listenAndEndpoint(); err != nil { + return nil, err + } + return s.endpoint, nil +} + +func (s *Server) Start(ctx context.Context) error { + if err := s.listenAndEndpoint(); err != nil { + return err + } + s.BaseContext = func(listener net.Listener) context.Context { + return ctx + } + log.Infof("[HTTP] server listening on: %s", s.endpoint) + var err error + if s.tlsConf != nil { + err = s.ServeTLS(s.lis, "", "") + } else { + err = s.Serve(s.lis) + } + if !errors.Is(err, http.ErrServerClosed) { + return err + } + return nil +} + +func (s *Server) Stop(ctx context.Context) error { + log.Info("[HTTP] server stopping") + return s.Shutdown(ctx) +} func (s *Server) filter() mux.MiddlewareFunc { return func(handler http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { @@ -237,27 +275,38 @@ func (s *Server) filter() mux.MiddlewareFunc { if route := mux.CurrentRoute(request); route != nil { pathTemplate, _ = route.GetPathTemplate() } - tr := &Transport{} + tr := &Transport{ + operation: pathTemplate, + pathTemplate: pathTemplate, + reqHeader: headerCarrier(request.Header), + replyHeader: headerCarrier(writer.Header()), + request: request, + } + if s.endpoint != nil { + tr.endpoint = s.endpoint.String() + } + tr.request = request.WithContext(transport.NewServerContext(ctx, tr)) + handler.ServeHTTP(writer, request) }) } } -func (s *Server) Endpoint() string { - //TODO implement me - panic("implement me") -} - -func (s *Server) Operation() string { - //TODO implement me - panic("implement me") -} - -func (s *Server) RequestHeader() transport.Header { - //TODO implement me - panic("implement me") -} - -func (s *Server) ReplyHeader() transport.Header { - //TODO implement me - panic("implement me") +func (s *Server) listenAndEndpoint() error { + if s.lis == nil { + lis, err := net.Listen(s.network, s.address) + if err != nil { + s.err = err + return err + } + s.lis = lis + } + if s.endpoint == nil { + addr, err := host.Extract(s.address, s.lis) + if err != nil { + s.err = err + return err + } + s.endpoint = endpoint.NewEndpoint(endpoint.Scheme("http", s.tlsConf != nil), addr) + } + return s.err } diff --git a/transport/http/status/status.go b/transport/http/status/status.go new file mode 100644 index 0000000..a9510cf --- /dev/null +++ b/transport/http/status/status.go @@ -0,0 +1,96 @@ +package status + +import ( + "google.golang.org/grpc/codes" + "net/http" +) + +const ( + ClientClosed = 499 +) + +type Converter interface { + ToGRPCCode(code int) codes.Code + FromGRPCCode(code codes.Code) int +} + +var DefaultConverter Converter = statusConverter{} + +type statusConverter struct { +} + +func (s statusConverter) ToGRPCCode(code int) codes.Code { + switch code { + case http.StatusOK: + return codes.OK + case http.StatusBadRequest: + return codes.InvalidArgument + case http.StatusUnauthorized: + return codes.Unauthenticated + case http.StatusForbidden: + return codes.PermissionDenied + case http.StatusNotFound: + return codes.NotFound + case http.StatusConflict: + return codes.Aborted + case http.StatusTooManyRequests: + return codes.ResourceExhausted + case http.StatusInternalServerError: + return codes.Internal + case http.StatusNotImplemented: + return codes.Unimplemented + case http.StatusServiceUnavailable: + return codes.Unavailable + case http.StatusGatewayTimeout: + return codes.DeadlineExceeded + case ClientClosed: + return codes.Canceled + } + return codes.Unknown +} + +func (s statusConverter) FromGRPCCode(code codes.Code) int { + switch code { + case codes.OK: + return http.StatusOK + case codes.Canceled: + return ClientClosed + case codes.Unknown: + return http.StatusInternalServerError + case codes.InvalidArgument: + return http.StatusBadRequest + case codes.DeadlineExceeded: + return http.StatusGatewayTimeout + case codes.NotFound: + return http.StatusNotFound + case codes.AlreadyExists: + return http.StatusConflict + case codes.PermissionDenied: + return http.StatusForbidden + case codes.Unauthenticated: + return http.StatusUnauthorized + case codes.ResourceExhausted: + return http.StatusTooManyRequests + case codes.FailedPrecondition: + return http.StatusBadRequest + case codes.Aborted: + return http.StatusConflict + case codes.OutOfRange: + return http.StatusBadRequest + case codes.Unimplemented: + return http.StatusNotImplemented + case codes.Internal: + return http.StatusInternalServerError + case codes.Unavailable: + return http.StatusServiceUnavailable + case codes.DataLoss: + return http.StatusInternalServerError + } + return http.StatusInternalServerError +} +func ToGRPCCode(code int) codes.Code { + return DefaultConverter.ToGRPCCode(code) +} +func FromGRPCCode(code codes.Code) int { + return DefaultConverter.FromGRPCCode(code) +} diff --git a/transport/http/transport.go b/transport/http/transport.go index 1a0654c..6a9f2bf 100644 --- a/transport/http/transport.go +++ b/transport/http/transport.go @@ -63,3 +63,21 @@ func RequestFromServerContext(ctx context.Context) (*http.Request, bool) { } return nil, false } + +type headerCarrier http.Header + +func (h headerCarrier) Get(key string) string { + return http.Header(h).Get(key) +} + +func (h headerCarrier) Set(key string, value string) { + http.Header(h).Set(key, value) +} + +func (h headerCarrier) Keys() []string { + kvs := make([]string, 0, len(h)) + for k := range http.Header(h) { + kvs = append(kvs, k) + } + return kvs +}