Skip to content

Commit

Permalink
add API mock feature
Browse files Browse the repository at this point in the history
  • Loading branch information
zhangxu committed Nov 4, 2016
1 parent 9cce2bf commit 7794deb
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 33 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Gateway is a API gateway based on http. It works at 7 layer.
* Routing based on URL
* API aggregation(support url rewrite)
* Validation
* API Mock
* Backend Server heath check
* Use [fasthttp](https://github.com/valyala/fasthttp)
* Admin WEBUI
Expand Down
4 changes: 4 additions & 0 deletions cmd/admin/public/assets/js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ function APICreateController($scope, $routeParams, $http, $location, $route) {
$scope.newUrl = "";
$scope.newDesc = "";
$scope.newName = "";
$scope.newMock = {};
$scope.newMockContentType = "";
$scope.newNodes = [];


Expand Down Expand Up @@ -120,6 +122,8 @@ function APICreateController($scope, $routeParams, $http, $location, $route) {
"url": $scope.newUrl,
"method": $scope.newMethod,
"desc": $scope.newDesc,
"mock": $scope.newMock,
"mockContentType": $scope.newMockContentType,
"nodes": $scope.newNodes,
}

Expand Down
4 changes: 4 additions & 0 deletions cmd/admin/public/html/api/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ <h1 class="page-header">API</h1>
<th>Method</th>
<th>Desc</th>
<th>Dispatch Nodes</th>
<th>Mock</th>
<th>Opts</th>
</tr>
</thead>
Expand Down Expand Up @@ -58,6 +59,9 @@ <h1 class="page-header">API</h1>
</tbody>
</table>
</td>
<td>
{{api.mock | json}}
</td>
<td>
<button type="button" class="btn btn-success btn-sm" ng-click="delete(api.encodeURL, api.method)">Delete</button>
</td>
Expand Down
8 changes: 6 additions & 2 deletions cmd/admin/public/html/api/new.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
<label for="desc">Desc</label>
<textarea class="form-control" ng-model="newDesc" id="desc" rows="4"></textarea>
</div>

<div class="form-group">
<label for="mock">Mock</label>
<textarea json-text class="form-control" ng-model="newMock" id="mock" rows="15"></textarea>
</div>
</div>
</div>

Expand All @@ -52,7 +57,6 @@
<label for="validation">Validation</label>
<textarea json-text class="form-control" ng-model="newValidation" id="validation" rows="15"></textarea>
</div>

<button type="button" class="btn btn-default" ng-click="addNode()">Add</button>
</form>
</div>
Expand Down Expand Up @@ -80,7 +84,7 @@
<td>
<input type="text" size="10" ng-model="n.rewrite" />
</td>
<td>
<td>
<textarea json-text class="form-control" ng-model="n.validations" rows="15"></textarea>
</td>
<td>
Expand Down
6 changes: 5 additions & 1 deletion cmd/admin/public/html/api/update.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
<label for="desc">Desc</label>
<textarea class="form-control" ng-model="api.desc" id="desc" rows="4"></textarea>
</div>

<div class="form-group">
<label for="mock">Mock</label>
<textarea json-text class="form-control" ng-model="api.mock" id="mock" rows="15"></textarea>
</div>
</div>
</div>

Expand All @@ -52,7 +57,6 @@
<label for="validation">Validation</label>
<textarea json-text class="form-control" ng-model="newValidation" id="validation" rows="15"></textarea>
</div>

<button type="button" class="btn btn-default" ng-click="addNode()">Add</button>
</form>
</div>
Expand Down
28 changes: 28 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,37 @@ API
API is the core concept in gateway. You can use admin to manage your APIs.

# API fields
* Name
The API Name

* URL
URL is a regex pattern for match request url. If a origin request url matches this value, proxy dispatch request to nodes which is defined in this api.

* Method
API Http method, the request must match both URL and method. `*` is match all http method(GET,PUT,POST,DELETE)

* Mock
A mock json configuration like this
```json
{
"value": "{\"abc\":\"hello\"}",
"contentType": "application/json; charset=utf-8",
"headers": [
{
"name": "header1",
"value": "value1"
}
],
"cookies": [
"test-c=1", // it's a set-cookie header string format
"test-c2=2" // it's a set-cookie header string format
]
}
```
value is required, contentType, headers and cookies are optional.

Note. If proxy get any error(e.g. has no backend server, backend return a error code) by this API, proxy will use mock to response.

* Nodes
API nodes is a list infomation. Every Node has 4 attrbutes: cluster, attrbute name, rewrite. Proxy will dispatch origin request to these nodes, and wait for all response, than merge to response to client.

Expand Down
75 changes: 69 additions & 6 deletions pkg/model/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,27 @@ type Node struct {
Validations []*Validation `json:"validations, omitempty"`
}

// Mock mock
type Mock struct {
Value string `json:"value"`
ContentType string `json:"contentType, omitempty"`
Headers []*MockHeader `json:"headers, omitempty"`
Cookies []string `json:"cookies, omitempty"`
ParsedCookies []*fasthttp.Cookie `json:"-"`
}

// MockHeader header
type MockHeader struct {
Name string `json:"name"`
Value string `json:"value"`
}

// API a api define
type API struct {
Name string `json:"name, omitempty"`
URL string `json:"url"`
Method string `json:"method"`
Alias string `json:"alias, omitempty"`
Mock *Mock `json:"mock, omitempty"`
Nodes []*Node `json:"nodes"`
Desc string `json:"desc, omitempty"`
Pattern *regexp.Regexp `json:"-"`
Expand All @@ -32,6 +47,11 @@ type API struct {
func UnMarshalAPI(data []byte) *API {
v := &API{}
json.Unmarshal(data, v)

if v.Mock != nil && v.Mock.Value == "" {
v.Mock = nil
}

return v
}

Expand All @@ -42,6 +62,10 @@ func UnMarshalAPIFromReader(r io.Reader) (*API, error) {
decoder := json.NewDecoder(r)
err := decoder.Decode(v)

if v.Mock != nil && v.Mock.Value == "" {
v.Mock = nil
}

return v, err
}

Expand All @@ -53,6 +77,50 @@ func NewAPI(url string, nodes []*Node) *API {
}
}

// Parse parse
func (a *API) Parse() {
a.Pattern = regexp.MustCompile(a.URL)
for _, n := range a.Nodes {
if nil != n.Validations {
for _, v := range n.Validations {
v.ParseValidation()
}
}
}

if nil != a.Mock && nil != a.Mock.Cookies && len(a.Mock.Cookies) > 0 {
a.Mock.ParsedCookies = make([]*fasthttp.Cookie, len(a.Mock.Cookies))
for index, c := range a.Mock.Cookies {
ck := &fasthttp.Cookie{}
ck.Parse(c)
a.Mock.ParsedCookies[index] = ck
}
}
}

// RenderMock dender mock response
func (a *API) RenderMock(ctx *fasthttp.RequestCtx) {
if a.Mock == nil {
return
}

ctx.Response.Header.SetContentType(a.Mock.ContentType)

if a.Mock.Headers != nil && len(a.Mock.Headers) > 0 {
for _, header := range a.Mock.Headers {
ctx.Response.Header.Add(header.Name, header.Value)
}
}

if a.Mock.ParsedCookies != nil && len(a.Mock.ParsedCookies) > 0 {
for _, ck := range a.Mock.ParsedCookies {
ctx.Response.Header.SetCookie(ck)
}
}

ctx.WriteString(a.Mock.Value)
}

// Marshal marshal
func (a *API) Marshal() []byte {
v, _ := json.Marshal(a)
Expand All @@ -70,8 +138,3 @@ func (a *API) getNodeURL(req *fasthttp.Request, node *Node) string {
func (a *API) matches(req *fasthttp.Request) bool {
return (a.Method == "*" || strings.ToUpper(string(req.Header.Method())) == a.Method) && a.Pattern.Match(req.URI().RequestURI())
}

func (a *API) updateFrom(api *API) {
a.URL = api.URL
a.Nodes = api.Nodes
}
14 changes: 14 additions & 0 deletions pkg/model/mock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"value": "{\"abc\":\"hello\"}",
"contentType": "application/json; charset=utf-8",
"headers": [
{
"name": "header1",
"value": "value1"
}
],
"cookies": [
"test-c=1",
"test-c2=2"
]
}
25 changes: 4 additions & 21 deletions pkg/model/ruletable.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package model

import (
"errors"
"regexp"
"sync"
"time"

Expand Down Expand Up @@ -172,14 +171,7 @@ func (r *RouteTable) AddNewAPI(api *API) error {
return ErrAPIExists
}

api.Pattern = regexp.MustCompile(api.URL)
for _, n := range api.Nodes {
if nil != n.Validations {
for _, v := range n.Validations {
v.ParseValidation()
}
}
}
api.Parse()

r.apis[getAPIKey(api.URL, api.Method)] = api

Expand All @@ -193,21 +185,12 @@ func (r *RouteTable) UpdateAPI(api *API) error {
r.rwLock.Lock()
defer r.rwLock.Unlock()

old, ok := r.apis[getAPIKey(api.URL, api.Method)]

if !ok {
if _, ok := r.apis[getAPIKey(api.URL, api.Method)]; !ok {
return ErrAPINotFound
}

old.updateFrom(api)

for _, n := range old.Nodes {
if nil != n.Validations {
for _, v := range n.Validations {
v.ParseValidation()
}
}
}
r.apis[getAPIKey(api.URL, api.Method)] = api
api.Parse()

log.Infof("API <%s-%s> updated", api.Method, api.URL)

Expand Down
10 changes: 7 additions & 3 deletions proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ var (
)

var (
// HeaderContentType content-type header
HeaderContentType = "Content-Type"
// MergeContentType merge operation using content-type
MergeContentType = "application/json; charset=utf-8"
// MergeRemoveHeaders merge operation need to remove headers
Expand Down Expand Up @@ -109,6 +107,12 @@ func (p *Proxy) ReverseProxyHandler(ctx *fasthttp.RequestCtx) {

for _, result := range results {
if result.Err != nil {
if result.API.Mock != nil {
result.API.RenderMock(ctx)
result.Release()
return
}

ctx.SetStatusCode(result.Code)
result.Release()
return
Expand All @@ -128,7 +132,7 @@ func (p *Proxy) ReverseProxyHandler(ctx *fasthttp.RequestCtx) {
result.Res.Header.CopyTo(&ctx.Response.Header)
}

ctx.Response.Header.Add(HeaderContentType, MergeContentType)
ctx.Response.Header.SetContentType(MergeContentType)
ctx.SetStatusCode(fasthttp.StatusOK)

ctx.WriteString("{")
Expand Down

0 comments on commit 7794deb

Please sign in to comment.