Table of Contents generated with DocToc
- Author: @kdada
- Initial issue: caicloud#2
We need a uniform api framework to standarize our api project.
- API projects run in internal environment. They don't care about connection security.
- API projects are modularized and have low coupling with engaged framework.
- APIs are not performance sensitive.
- APIs are restful.
- No html render requirements.
- No session requirements.
- No streaming uploading requirements.
The core components of api framework should have a router and a handler specification. Router is responsible for dispatching requests to handlers. Then handlers validate requests and inject parameters to api methods defined by users. After the execution of api methods, handlers analyze the returned values and write corresponding data to responses.
The main components with dependencies:
- Router
- Log
- Error
- Middleware
- Handler
- Log
- Error
- Validator
- Injector
- Context
- Tracing
- Client
- Log
- Error
- Tracing
A standard logger interface:
type Verboser interface {
Info(...interface{})
Infof(...interface{})
Infoln(...interface{})
}
type Logger interface {
V(int) Verboser
Info(...interface{})
Infof(...interface{})
Infoln(...interface{})
Warning(...interface{})
Warningf(...interface{})
Warningln(...interface{})
Error(...interface{})
Errorf(...interface{})
Errorln(...interface{})
Fatal(...interface{})
Fatalf(...interface{})
Fatalln(...interface{})
}
A standard error interface:
type Error interface {
Code() int
Message() interface{}
}
Tracing system could be Zipkin or Jeager. The two implement opentracing. And we will integrate opentracing via opentracing-go.
URL path is a long continuous string. But we can convert it to an array of segments. For example, we have an instance of url path:
/collections/object/subresources/resource
We can split the path by '/', then we get an array:
[collections, object, subresources, resource]
Router is a tree. Like this:
/
|-- collections
|---- (object:*)
|------ subresources
|-------- (resource:*)
|-- others
Every node should match one segement. Pass the array to root node of router, it finds the leaf node matched the last segement. Then the handler of the leaf node handles the request.
In the example, It shows we need support two kinds of router nodes:
- String node A string router can match a fixed segment of url path.
- Regexp node A regexp router can match multiple forms of segment.
The handler of node should implement:
type Handler interface {
Handle(context.Context)
}
As saying above, a router node can have one handler. But it can have multiple middlewares, middlewares decides the execution of router.
type Middleware interface {
Prerouting(context.Context) bool
Postrouting(context.Context) bool
}
Middleware can cancel the routing process and return immediately.
Alternative Middleware
type RoutingChain interface {
Continue(context.Context)
}
type Middleware interface {
Handle(context.Context, RoutingChain) bool
}
type FromType string
const (
FromPath FromType = "FromPath"
FromHeader FromType = "FromHeader"
FromForm FromType = "FromForm"
FromBody FromType = "FromBody"
)
type ResultType string
const (
DataResult ResultType = "DataResult"
)
type Parameter struct {
From FromType
Name string
Document string
Type string
Required bool
Default interface{}
}
type Result struct {
Type ResultType
}
type Function struct {
Pointer interface{}
Parameters []Parameter
Results []Result
Errors []Error
}
type ContentType struct {
Acceptable []string
Generable []string
}
type Definition struct {
HTTPMethod string
Summary string
Document string
ContentType ContentType
Function Function
}
type Descriptor struct {
Path string
Middlewares []Middleware
Definitions []Definition
Children []Descriptor
}
log
and errors
are based on other packages, so we don't explain it in the diagram.
nirvana
is the root package in the framework. It implements a configurable server structure. All plugins
implement config interface:
// ConfigInstaller is used to install config to service builder.
type ConfigInstaller interface {
// Name is the external config name.
Name() string
// Install installs stuffs before server starting.
Install(builder service.Builder, config *Config) error
// Uninstall uninstalls stuffs after server terminating.
Uninstall(builder service.Builder, config *Config) error
}
The startup flow:
Definition modifier is special. It only works in service builder and is used to modify definitions. That means
if you install some API definitions into nirvana server, you have a chance to modify all definitions globally.
The most important usage of modifier is to inject context.Context
into Definition.Parameters
as first
parameter.
The shutdown flow:
All plugins are independent, so plugins are installed and uninstalled without order. In this stage, plugins can close file descriptors or close network connections.
The lifecycle of requests:
ParameterGenerators
are generated from Definition.Parameters
. A parameter corresponds to a ParameterGenerator
.
Every parameter should go through the flow. Operators execute one by one. The original data passes to first operator, then the result of first operator is as the parameter to pass to the second operator. The last operator's result is as the final data to user function. If there are no operators, typed data is as the final data.
The workflow of DestinationHandlers
is like ParameterGenerators
. The returned value of user function is
passed to operators. Then the returned value of last operator is as final data to client.