RussellLuo/kun

Add support for patching existing codecs

RussellLuo opened this issue · 0 comments

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{})
		})
}