Skip to content

Add support for the business level input validation #34

@RussellLuo

Description

@RussellLuo

Problem

kun has already supported request validation since #10. However, there are some disadvantages:

  • The request validation is done at the transport layer, while the request fields to validate are actually designed at the service layer
  • Input validation of pure services (i.e. without the transport layer) are not supported, which is a critical problem since "In-process function call" is the communication type kun must support.
  • Validation schemas are not visible from the documentation (i.e. Godoc for pure services, OAS for HTTP services, Protobuf for gRPC services, etc)

Validation Grammar

Validation grammars from the current leading Go frameworks:

Other interesting references:

Proposed Solution

  • Define validation schemas in the comments of interface methods.
  • Generate the validation code, in the form of a service middleware (i.e. at the service layer), per the schema definitions.

Example

helloworld

Take the helloworld service as an example, the validation schemas become (see previous schema definition):

// Service is used for saying hello.
type Service interface {
	// SayHello says hello to the given name.
	//
	// @schema:
	//   name: len(0, 10).msg("bad length") && match(`^\w+$`)
	//
	//kun:op POST /messages
	SayHello(ctx context.Context, name string) (message string, err error)
}

and this is the corresponding generated service middleware:

import (
	v "github.com/RussellLuo/validating/v3"
)

func ValidatingMiddleware() func(next Service) Service {
	return func(next Service) Service {
		return &validatingMiddleware{
			next: next,
		}
	}
}

type validatingMiddleware struct {
	next Service
}

func (mw *validatingMiddleware) SayHello(ctx context.Context, name string) (string, error) {
	schema := v.Schema{
		v.F("name", name): v.All(
			v.LenString(0, 10).Msg("bad length"),
			v.Match(regexp.MustCompile(`^\w+$`)),
		),
	}
	if err := v.Validate(schema); err != nil {
		return "", werror.Wrap(gcode.ErrInvalidArgument, err)
	}

	return mw.next.SayHello(ctx, name)
}

usersvc

Take the usersvc an example, the schema definition will be:

type User struct {
	Name string
	Age  int
	IP   net.IP `kun:"in=header name=X-Forwarded-For, in=request name=RemoteAddr"`
}

func (u User) Schema() v.Schema {
	return v.Schema{
		v.F("name", u.Name): v.All(
			v.LenString(0, 10),
			v.Match(regexp.MustCompile(`^\w+$`)),
		),
		v.F("age", u.Age): v.Range(0, 100),
		v.F("ip", user.IP): vext.IP(),
	}
}

type Service interface {
	// CreateUser creates a user with the given attributes.
	//
	//kun:op POST /users
	//kun:param user
	//kun:success body=result
	CreateUser(ctx context.Context, user User) (result User, err error)
}

and this is the corresponding generated service middleware:

import (
	v "github.com/RussellLuo/validating/v3"
)

func ValidatingMiddleware() func(next Service) Service {
	return func(next Service) Service {
		return &validatingMiddleware{
			next: next,
		}
	}
}

type validatingMiddleware struct {
	next Service
}

func (mw *validatingMiddleware) CreateUser(ctx context.Context, user User) (result User, err error) {
	schema := v.Schema{
		v.F("user", user): user.Schema(),
	}
	if err := v.Validate(schema); err != nil {
		return User{}, werror.Wrap(gcode.ErrInvalidArgument, err)
	}

	return mw.next.CreateUser(ctx, user)
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions