Skip to content

Add support for the business level input validation #34

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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