Add support for patching existing codecs
RussellLuo opened this issue · 0 comments
RussellLuo commented
Motivation
Take the IP decoding
mentioned in README.md as an example:
type Service interface {
// @kok(op): POST /logs
// @kok(param): ip < in:header,name:X-Forwarded-For
// @kok(param): ip < in:request,name:RemoteAddr
Log(ctx context.Context, ip net.IP) (err error)
}
// The equivalent annotations.
type Service interface {
// @kok(op): POST /logs
// @kok(param): ip < in:header,name:X-Forwarded-For
// @kok(param): < in:request,name:RemoteAddr
Log(ctx context.Context, ip net.IP) (err error)
}
// You must customize the decoding of `ip` later (conventionally in another file named `codec.go`).
// See examples in the `Encoding and decoding` section.
// HTTP request:
// $ http POST /logs
The existing solution is to implement a new codec:
// codec.go
import (
"fmt"
"net"
"strings"
"github.com/RussellLuo/kok/pkg/codec/httpcodec"
)
type Codec struct {
httpcodec.JSON
}
func (c Codec) DecodeRequestParams(name string, values map[string][]string, out interface{}) error {
switch name {
case "ip":
// We are decoding the "ip" argument.
remote := values["request.RemoteAddr"][0]
if fwdFor := values["header.X-Forwarded-For"][0]; fwdFor != "" {
remote = strings.TrimSpace(strings.Split(fwdFor, ",")[0])
}
ipStr, _, err := net.SplitHostPort(remote)
if err != nil {
ipStr = remote // OK; probably didn't have a port
}
ip := net.ParseIP(ipStr)
if ip == nil {
return fmt.Errorf("invalid client IP address: %s", ipStr)
}
outIP := out.(*net.IP)
*outIP = ip
return nil
default:
// Use the JSON codec for other arguments.
return c.JSON.DecodeRequestParams(name, values, out)
}
}
func NewCodecs() *httpcodec.DefaultCodecs {
return httpcodec.NewDefaultCodecs(Codec{})
}
While the above solution is feasible, the custom encoding and decoding behavior here is so common that we should provide:
- an easier way to customize codecs for request parameters
- and better code reusability for custom codecs
Proposed Solution
Add support for patching existing codecs, which is shown as below:
// codec.go
import (
"fmt"
"net"
"strings"
"github.com/RussellLuo/kok/pkg/codec/httpcodec"
)
// IPCodec is used to encode and decode an IP. It can be reused wherever needed.
type IPCodec struct{}
func (c IPCodec) Decode(in map[string][]string, out interface{}) error {
remote := in["request.RemoteAddr"][0]
if fwdFor := in["header.X-Forwarded-For"][0]; fwdFor != "" {
remote = strings.TrimSpace(strings.Split(fwdFor, ",")[0])
}
ipStr, _, err := net.SplitHostPort(remote)
if err != nil {
ipStr = remote // OK; probably didn't have a port
}
ip := net.ParseIP(ipStr)
if ip == nil {
return fmt.Errorf("invalid client IP address: %s", ipStr)
}
outIP := out.(*net.IP)
*outIP = ip
return nil
}
func (c IPCodec) Encode(in interface{}) (out map[string][]string) {
return nil
}
func NewCodecs() *httpcodec.DefaultCodecs {
// Use IPCodec to encode and decode the argument named "ip", if exists,
// for the operation named "Log".
return httpcodec.NewDefaultCodecs(nil,
httpcodec.Op("Log", httpcodec.NewPatcher(httpcodec.JSON{}).Params("ip", IPCodec{})))
}
// Another way to create the codecs.
func NewCodecs2() *httpcodec.DefaultCodecs {
// Use IPCodec to encode and decode the argument named "ip", if exists,
// for all the operations.
return httpcodec.NewDefaultCodecs(nil).
PatchAll(func(c httpcodec.Codec) *httpcodec.Patcher {
return httpcodec.NewPatcher(c).Params("ip", IPCodec{})
})
}