Repository: caicloud/nirvana
Pull request #392 (2020-11-23 06:16:57)

[release-0.3] feat(*): make multiple plugins support the RPC builder
This is an automated cherry-pick of #390

/assign iawia002

caicloud-bot: @caicloud-bot: Adding the "do-not-merge/release-note-label-needed" label because no release-note block was detected, please follow our [release note process](https://git.k8s.io/community/contributors/guide/release-notes.md) to remove it.

<details>

Instructions for interacting with me using PR comments are available [here](https://github.com/caicloud/engineering/blob/master/guidelines/caicloud_bot.md).  If you have questions or suggestions related to my behavior, please file an issue against the [caicloud/tools](https://github.com/caicloud/tools/issues/new?title=Prow%20issue:) repository.
</details>
caicloud-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: 
To fully approve this pull request, please assign additional approvers.
We suggest the following additional approver: **kdada**

Assign the PR to them by writing `/assign @kdada` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://github.com/caicloud/engineering/blob/master/guidelines/caicloud_bot.md#bot-commands).

The pull request process is described [here](https://github.com/caicloud/engineering/blob/master/guidelines/caicloud_bot.md#typical-workflow)

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/caicloud/nirvana/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["kdada"]} -->
iawia002: /release-note-none
/lgtm
caicloud-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/caicloud/nirvana/pull/392#issuecomment-731952301" title="LGTM">iawia002</a>*

The full list of commands accepted by this bot can be found [here](https://github.com/caicloud/engineering/blob/master/guidelines/caicloud_bot.md#bot-commands).

The pull request process is described [here](https://github.com/caicloud/engineering/blob/master/guidelines/caicloud_bot.md#typical-workflow)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/caicloud/nirvana/blob/master/OWNERS)~~ [iawia002]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/definition/helper.go b/definition/helper.go
index 55a63db..b9b17b4 100644
--- a/definition/helper.go
+++ b/definition/helper.go
@@ -259,5 +259,4 @@ func SimpleDescriptor(method Method, path string, f interface{}) Descriptor {
 			},
 		},
 	}
-
 }
diff --git a/examples/getting-started/rpc-style/apis/api.app.v1.json b/examples/getting-started/rpc-style/apis/api.app.v1.json
new file mode 100644
index 0000000..cd9f81a
--- /dev/null
+++ b/examples/getting-started/rpc-style/apis/api.app.v1.json
@@ -0,0 +1,116 @@
+{
+  "schemes": [
+    "http"
+  ],
+  "swagger": "2.0",
+  "info": {
+    "description": "Example module management RESTful APIs\u003cbr/\u003eThe app v1 version",
+    "title": "module-template APIs\n",
+    "contact": {
+      "name": "Xinzhao Xu",
+      "email": "xuxinzhao@caicloud.io"
+    },
+    "version": "v1"
+  },
+  "host": "localhost:8080",
+  "paths": {
+    "/apis/app/v1/workloads": {
+      "get": {
+        "description": "Query a specified number of workloads and returns an array",
+        "consumes": [
+          "application/json",
+          ""
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "summary": "List Workloads",
+        "parameters": [
+          {
+            "type": "number",
+            "format": "int",
+            "default": 10,
+            "description": "Number of workloads",
+            "name": "count",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "A list of workloads",
+            "schema": {
+              "type": "array",
+              "items": {
+                "title": "Workload",
+                "$ref": "#/definitions/github.com_caicloud_module-template_pkg_server_apis_app_v1.Workload"
+              }
+            }
+          }
+        }
+      }
+    },
+    "/apis/app/v1/workloads/{workload}": {
+      "get": {
+        "description": "Get a workload by id",
+        "consumes": [
+          "application/json",
+          ""
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "summary": "Get Workload",
+        "parameters": [
+          {
+            "type": "string",
+            "format": "string",
+            "description": "workload name",
+            "name": "workload",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "A workload",
+            "schema": {
+              "$ref": "#/definitions/github.com_caicloud_module-template_pkg_server_apis_app_v1.Workload"
+            }
+          }
+        }
+      }
+    }
+  },
+  "definitions": {
+    "github.com_caicloud_module-template_pkg_server_apis_addon_v1alpha1.Object": {
+      "title": "Object",
+      "properties": {
+        "id": {
+          "type": "number",
+          "format": "int",
+          "title": "int"
+        }
+      }
+    },
+    "github.com_caicloud_module-template_pkg_server_apis_app_v1.Workload": {
+      "title": "Workload",
+      "properties": {
+        "name": {
+          "type": "string",
+          "format": "string",
+          "title": "string"
+        }
+      }
+    },
+    "github.com_caicloud_module-template_pkg_server_apis_storage_v1alpha1.Object": {
+      "title": "Object",
+      "properties": {
+        "id": {
+          "type": "number",
+          "format": "int",
+          "title": "int"
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/examples/getting-started/rpc-style/apis/api.storage.v1alpha1.json b/examples/getting-started/rpc-style/apis/api.storage.v1alpha1.json
new file mode 100644
index 0000000..60f3e58
--- /dev/null
+++ b/examples/getting-started/rpc-style/apis/api.storage.v1alpha1.json
@@ -0,0 +1,116 @@
+{
+  "schemes": [
+    "http"
+  ],
+  "swagger": "2.0",
+  "info": {
+    "description": "Example module management RESTful APIs\u003cbr/\u003eThe storage v1alpha1 version",
+    "title": "module-template APIs\n",
+    "contact": {
+      "name": "Xinzhao Xu",
+      "email": "xuxinzhao@caicloud.io"
+    },
+    "version": "v1alpha1"
+  },
+  "host": "localhost:8080",
+  "paths": {
+    "/apis/storage/v1alpha1/objects": {
+      "get": {
+        "description": "Query a specified number of objects and returns an array",
+        "consumes": [
+          "application/json",
+          ""
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "summary": "List Objects",
+        "parameters": [
+          {
+            "type": "number",
+            "format": "int",
+            "default": 10,
+            "description": "Number of objects",
+            "name": "count",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "A list of objects",
+            "schema": {
+              "type": "array",
+              "items": {
+                "title": "Object",
+                "$ref": "#/definitions/github.com_caicloud_module-template_pkg_server_apis_storage_v1alpha1.Object"
+              }
+            }
+          }
+        }
+      }
+    },
+    "/apis/storage/v1alpha1/objects/{object}": {
+      "get": {
+        "description": "Get a object by id",
+        "consumes": [
+          "application/json",
+          ""
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "summary": "Get Object",
+        "parameters": [
+          {
+            "type": "number",
+            "format": "int",
+            "description": "object id",
+            "name": "object",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "A object",
+            "schema": {
+              "$ref": "#/definitions/github.com_caicloud_module-template_pkg_server_apis_storage_v1alpha1.Object"
+            }
+          }
+        }
+      }
+    }
+  },
+  "definitions": {
+    "github.com_caicloud_module-template_pkg_server_apis_addon_v1alpha1.Object": {
+      "title": "Object",
+      "properties": {
+        "id": {
+          "type": "number",
+          "format": "int",
+          "title": "int"
+        }
+      }
+    },
+    "github.com_caicloud_module-template_pkg_server_apis_app_v1.Workload": {
+      "title": "Workload",
+      "properties": {
+        "name": {
+          "type": "string",
+          "format": "string",
+          "title": "string"
+        }
+      }
+    },
+    "github.com_caicloud_module-template_pkg_server_apis_storage_v1alpha1.Object": {
+      "title": "Object",
+      "properties": {
+        "id": {
+          "type": "number",
+          "format": "int",
+          "title": "int"
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/examples/getting-started/rpc-style/main.go b/examples/getting-started/rpc-style/main.go
index a4585f4..84c7632 100644
--- a/examples/getting-started/rpc-style/main.go
+++ b/examples/getting-started/rpc-style/main.go
@@ -23,8 +23,12 @@ import (
 	"github.com/caicloud/nirvana/config"
 	"github.com/caicloud/nirvana/definition"
 	"github.com/caicloud/nirvana/log"
+	"github.com/caicloud/nirvana/middlewares/metrics"
 	"github.com/caicloud/nirvana/middlewares/reqlog"
-	"github.com/caicloud/nirvana/service/builder"
+	"github.com/caicloud/nirvana/plugins/apidocs"
+	"github.com/caicloud/nirvana/plugins/healthcheck"
+	"github.com/caicloud/nirvana/plugins/profiling"
+	"github.com/caicloud/nirvana/service"
 )
 
 var echo = definition.RPCDescriptor{
@@ -94,8 +98,25 @@ func main() {
 	cmd := config.NewDefaultNirvanaCommand()
 	conf := nirvana.NewDefaultConfig()
 	conf.Configure(
-		nirvana.APIStyle(builder.APIStyleRPC),
-		nirvana.Descriptor(echo, echo2),
+		nirvana.APIStyle(service.APIStyleRPC),
+		nirvana.Descriptor(echo, echo2, metrics.RPCDescriptor("/metrics")),
+
+		profiling.Path("/debug/pprof"),
+		healthcheck.CheckerWithType(func(ctx context.Context, checkType string) error {
+			switch checkType {
+			case healthcheck.LivenessCheck:
+				return nil
+			case healthcheck.ReadinessCheck:
+				// add specific check logic here if needed
+				return nil
+			}
+			return nil
+		}),
+		// enable the API docs plugin
+		func(c *nirvana.Config) error {
+			plugin := &apidocs.Option{FilesPath: "apis", Path: "/docs"}
+			return plugin.Configure(c)
+		},
 	)
 	if err := cmd.ExecuteWithConfig(conf); err != nil {
 		log.Fatal(err)
diff --git a/middlewares/metrics/metrics.go b/middlewares/metrics/metrics.go
index adea2d0..2dd35b2 100644
--- a/middlewares/metrics/metrics.go
+++ b/middlewares/metrics/metrics.go
@@ -69,3 +69,21 @@ func Descriptor(path string) definition.Descriptor {
 	}
 	return definition.SimpleDescriptor(definition.Get, path, service.WrapHTTPHandler(promhttp.Handler()))
 }
+
+// RPCDescriptor returns a rpc descriptor for the API; it must be configured to a server in order to serve the
+// metric API.
+func RPCDescriptor(path string) definition.RPCDescriptor {
+	if path == "" {
+		path = "/metrics"
+	}
+	return definition.RPCDescriptor{
+		Path: path,
+		Actions: []definition.RPCAction{
+			{
+				Function: service.WrapHTTPHandler(promhttp.Handler()),
+				Consumes: []string{definition.MIMEAll},
+				Produces: []string{definition.MIMEAll},
+			},
+		},
+	}
+}
diff --git a/nirvana.go b/nirvana.go
index 9f1d90e..8be8c8b 100644
--- a/nirvana.go
+++ b/nirvana.go
@@ -53,7 +53,7 @@ type Server interface {
 // Config describes configuration of server.
 type Config struct {
 	// apiStyle makes nirvana serve as REST or RPC style, default is REST.
-	apiStyle string
+	apiStyle service.APIStyle
 	// tls cert file
 	certFile string
 	// tls ket file
@@ -177,7 +177,7 @@ func NewDefaultConfig() *Config {
 // modifiers for specific scenario, please use NewDefaultConfig().
 func NewConfig() *Config {
 	return &Config{
-		apiStyle:  builderutil.APIStyleREST,
+		apiStyle:  service.APIStyleREST,
 		port:      8080,
 		logger:    &log.SilentLogger{},
 		configSet: make(map[string]interface{}),
@@ -332,7 +332,7 @@ func RegisterConfigInstaller(ci ConfigInstaller) {
 }
 
 // APIStyle returns a configurer to set api style into config.
-func APIStyle(apiStyle string) Configurer {
+func APIStyle(apiStyle service.APIStyle) Configurer {
 	return func(c *Config) error {
 		c.apiStyle = apiStyle
 		return nil
diff --git a/plugins/apidocs/apidocs.go b/plugins/apidocs/apidocs.go
index a6a6c51..2b35de3 100644
--- a/plugins/apidocs/apidocs.go
+++ b/plugins/apidocs/apidocs.go
@@ -63,16 +63,21 @@ func (i *apiDocsInstaller) Install(builder service.Builder, cfg *nirvana.Config)
 		versions := make([]string, 0, len(files))
 		for v, data := range files {
 			versions = append(versions, v)
-			apiDescriptor = append(apiDescriptor, api.DescriptorForData(api.PathForVersion(c.path, v), data, definition.MIMEJSON))
+			if builder.APIStyle() == service.APIStyleRPC {
+				apiDescriptor = append(apiDescriptor, api.RPCDescriptorForData(api.PathForVersion(c.path, v), data, definition.MIMEJSON))
+			} else {
+				apiDescriptor = append(apiDescriptor, api.DescriptorForData(api.PathForVersion(c.path, v), data, definition.MIMEJSON))
+			}
 		}
 		data, err := api.GenSwaggerPageData(c.path, versions)
 		if err != nil {
 			return err
 		}
-		apiDescriptor = append(
-			apiDescriptor,
-			api.DescriptorForData(c.path, data, definition.MIMEHTML),
-		)
+		if builder.APIStyle() == service.APIStyleRPC {
+			apiDescriptor = append(apiDescriptor, api.RPCDescriptorForData(c.path, data, definition.MIMEHTML))
+		} else {
+			apiDescriptor = append(apiDescriptor, api.DescriptorForData(c.path, data, definition.MIMEHTML))
+		}
 		return builder.AddDescriptor(apiDescriptor...)
 	})
 }
diff --git a/plugins/healthcheck/healthz.go b/plugins/healthcheck/healthz.go
index 18fedfe..83e2568 100644
--- a/plugins/healthcheck/healthz.go
+++ b/plugins/healthcheck/healthz.go
@@ -80,17 +80,30 @@ func (i *healthcheckInstaller) Install(builder service.Builder, cfg *nirvana.Con
 			function = c.checkerWithType
 		}
 
-		err = builder.AddDescriptor(definition.Descriptor{
-			Path:     c.path,
-			Consumes: []string{definition.MIMEAll},
-			Produces: []string{definition.MIMEAll},
-			Definitions: []definition.Definition{{
-				Method:     definition.Get,
-				Results:    []definition.Result{definition.ErrorResult()},
-				Parameters: parameters,
-				Function:   function,
-			}},
-		})
+		if builder.APIStyle() == service.APIStyleRPC {
+			err = builder.AddDescriptor(definition.RPCDescriptor{
+				Path:     c.path,
+				Consumes: []string{definition.MIMEAll},
+				Produces: []string{definition.MIMEAll},
+				Actions: []definition.RPCAction{{
+					Results:    []definition.Result{definition.ErrorResult()},
+					Parameters: parameters,
+					Function:   function,
+				}},
+			})
+		} else {
+			err = builder.AddDescriptor(definition.Descriptor{
+				Path:     c.path,
+				Consumes: []string{definition.MIMEAll},
+				Produces: []string{definition.MIMEAll},
+				Definitions: []definition.Definition{{
+					Method:     definition.Get,
+					Results:    []definition.Result{definition.ErrorResult()},
+					Parameters: parameters,
+					Function:   function,
+				}},
+			})
+		}
 	})
 	return err
 }
diff --git a/plugins/profiling/profiling.go b/plugins/profiling/profiling.go
index d72c8c3..91433b9 100644
--- a/plugins/profiling/profiling.go
+++ b/plugins/profiling/profiling.go
@@ -17,16 +17,14 @@ limitations under the License.
 package profiling
 
 import (
-	"fmt"
 	"html/template"
 	"log"
 	"net/http"
 	"net/http/pprof"
+	"path"
 	"path/filepath"
-	"runtime"
 	rpprof "runtime/pprof"
 	"sort"
-	"strconv"
 
 	"github.com/caicloud/nirvana"
 	"github.com/caicloud/nirvana/definition"
@@ -56,8 +54,14 @@ func (i *profilingInstaller) Name() string {
 func (i *profilingInstaller) Install(builder service.Builder, cfg *nirvana.Config) error {
 	var err error
 	wrapper(cfg, func(c *config) {
-		if err = builder.AddDescriptor(descriptor(c.path)); err != nil {
-			return
+		if builder.APIStyle() == service.APIStyleRPC {
+			if err = builder.AddDescriptor(rpcDescriptors(c.path)...); err != nil {
+				return
+			}
+		} else {
+			if err = builder.AddDescriptor(descriptor(c.path)); err != nil {
+				return
+			}
 		}
 	})
 	return err
@@ -156,26 +160,7 @@ func descriptor(path string) definition.Descriptor {
 				Definitions: []definition.Definition{{
 					Method: definition.Get,
 					Function: service.WrapHTTPHandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-						name := filepath.Base(r.URL.Path)
-						w.Header().Set("X-Content-Type-Options", "nosniff")
-						p := rpprof.Lookup(name)
-						if p == nil {
-							w.WriteHeader(http.StatusNotFound)
-							_, _ = w.Write([]byte("Unknown profile"))
-							return
-						}
-						gc, _ := strconv.Atoi(r.FormValue("gc"))
-						if name == "heap" && gc > 0 {
-							runtime.GC()
-						}
-						debug, _ := strconv.Atoi(r.FormValue("debug"))
-						if debug != 0 {
-							w.Header().Set("Content-Type", "text/plain; charset=utf-8")
-						} else {
-							w.Header().Set("Content-Type", "application/octet-stream")
-							w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
-						}
-						_ = p.WriteTo(w, debug)
+						pprof.Handler(filepath.Base(r.URL.Path)).ServeHTTP(w, r)
 					}),
 				}},
 			},
@@ -183,6 +168,99 @@ func descriptor(path string) definition.Descriptor {
 	}
 }
 
+func rpcDescriptors(root string) []interface{} {
+	return []interface{}{
+		definition.RPCDescriptor{
+			Path:     root,
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandlerFunc(index),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "cmdline"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandlerFunc(pprof.Cmdline),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "profile"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandlerFunc(pprof.Profile),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "symbol"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandlerFunc(pprof.Symbol),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "trace"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandlerFunc(pprof.Trace),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "goroutine"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandler(pprof.Handler("goroutine")),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "threadcreate"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandler(pprof.Handler("threadcreate")),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "heap"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandler(pprof.Handler("heap")),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "allocs"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandler(pprof.Handler("allocs")),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "block"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandler(pprof.Handler("block")),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "mutex"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandler(pprof.Handler("mutex")),
+			}},
+		},
+	}
+}
+
 var profileDescriptions = map[string]string{
 	"allocs":    "A sampling of all past memory allocations",
 	"block":     "Stack traces that led to blocking on synchronization primitives",
diff --git a/plugins/version/version.go b/plugins/version/version.go
index 83ad822..2712737 100644
--- a/plugins/version/version.go
+++ b/plugins/version/version.go
@@ -64,18 +64,32 @@ func (i *versionInstaller) Install(builder service.Builder, cfg *nirvana.Config)
 			Hash:        c.hash,
 			Description: c.description,
 		}
-		err = builder.AddDescriptor(definition.Descriptor{
-			Path:     c.path,
-			Consumes: []string{definition.MIMEAll},
-			Produces: []string{definition.MIMEAll},
-			Definitions: []definition.Definition{{
-				Method:  definition.Get,
-				Results: definition.DataErrorResults(""),
-				Function: func(ctx context.Context) (*version, error) {
-					return v, nil
-				},
-			}},
-		})
+		if builder.APIStyle() == service.APIStyleRPC {
+			err = builder.AddDescriptor(definition.RPCDescriptor{
+				Path:     c.path,
+				Consumes: []string{definition.MIMEAll},
+				Produces: []string{definition.MIMEAll},
+				Actions: []definition.RPCAction{{
+					Results: definition.DataErrorResults(""),
+					Function: func(ctx context.Context) (*version, error) {
+						return v, nil
+					},
+				}},
+			})
+		} else {
+			err = builder.AddDescriptor(definition.Descriptor{
+				Path:     c.path,
+				Consumes: []string{definition.MIMEAll},
+				Produces: []string{definition.MIMEAll},
+				Definitions: []definition.Definition{{
+					Method:  definition.Get,
+					Results: definition.DataErrorResults(""),
+					Function: func(ctx context.Context) (*version, error) {
+						return v, nil
+					},
+				}},
+			})
+		}
 	})
 	return err
 }
diff --git a/service/builder/builder.go b/service/builder/builder.go
index 5be4b0d..5667b18 100644
--- a/service/builder/builder.go
+++ b/service/builder/builder.go
@@ -22,17 +22,10 @@ import (
 	"github.com/caicloud/nirvana/service/rpc"
 )
 
-const (
-	// APIStyleREST represents the RESTful API style.
-	APIStyleREST = "rest"
-	// APIStyleRPC represents the RPC API style.
-	APIStyleRPC = "rpc"
-)
-
 // New creates a service builder based on the given API style.
-func New(apiStyle string) service.Builder {
+func New(apiStyle service.APIStyle) service.Builder {
 	switch apiStyle {
-	case APIStyleRPC:
+	case service.APIStyleRPC:
 		return rpc.NewBuilder()
 	default:
 		return rest.NewBuilder()
diff --git a/service/rest/builder.go b/service/rest/builder.go
index 783c939..6f3e5f1 100644
--- a/service/rest/builder.go
+++ b/service/rest/builder.go
@@ -199,6 +199,11 @@ func (b *builder) Definitions() map[string][]definition.Definition {
 	return result
 }
 
+// APIStyle returns the API style of this builder.
+func (b *builder) APIStyle() service.APIStyle {
+	return service.APIStyleREST
+}
+
 // Build builds a service to handle request.
 func (b *builder) Build() (service.Service, error) {
 	if len(b.bindings) <= 0 {
diff --git a/service/rpc/builder.go b/service/rpc/builder.go
index 9df2d4a..ddc6263 100644
--- a/service/rpc/builder.go
+++ b/service/rpc/builder.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"net/http"
 	"reflect"
+	"sort"
 
 	"github.com/caicloud/nirvana/definition"
 	"github.com/caicloud/nirvana/log"
@@ -88,6 +89,9 @@ func (b *builder) SetModifier(m service.DefinitionModifier) {
 }
 
 func genRPCPath(path, version, action string) string {
+	if version == "" && action == "" {
+		return path
+	}
 	return fmt.Sprintf("%s?Version=%s&Action=%s", path, version, action)
 }
 
@@ -157,14 +161,26 @@ func (b *builder) Definitions() map[string][]definition.Definition {
 	return result
 }
 
+// APIStyle returns the API style of this builder.
+func (b *builder) APIStyle() service.APIStyle {
+	return service.APIStyleRPC
+}
+
 // Build builds a service to handle request.
 func (b *builder) Build() (service.Service, error) {
 	if len(b.bindings) <= 0 {
 		return nil, fmt.Errorf("no router")
 	}
 
+	paths := make([]string, 0, len(b.bindings))
+	for path := range b.bindings {
+		paths = append(paths, path)
+	}
+	sort.Strings(paths)
+
 	var err error
-	for path, bd := range b.bindings {
+	for _, path := range paths {
+		bd := b.bindings[path]
 		b.logger.V(log.LevelDebug).Infof("Path: %s, Consumes: %v, Produces: %v", path, bd.definition.Consumes, bd.definition.Produces)
 		if b.modifier != nil {
 			b.modifier(&bd.definition)
diff --git a/service/types.go b/service/types.go
index 98b3238..f8aab79 100644
--- a/service/types.go
+++ b/service/types.go
@@ -23,6 +23,16 @@ import (
 	"github.com/caicloud/nirvana/log"
 )
 
+// APIStyle defines the API style type of the builder.
+type APIStyle string
+
+const (
+	// APIStyleREST represents the RESTful API style.
+	APIStyleREST APIStyle = "rest"
+	// APIStyleRPC represents the RPC API style.
+	APIStyleRPC APIStyle = "rpc"
+)
+
 // Builder builds service.
 type Builder interface {
 	// Logger returns logger of builder.
@@ -41,6 +51,8 @@ type Builder interface {
 	AddDescriptor(descriptors ...interface{}) error
 	// Definitions returns all definitions. If a modifier exists, it will be executed.
 	Definitions() map[string][]definition.Definition
+	// APIStyle returns the API style of this builder.
+	APIStyle() APIStyle
 	// Build builds a service to handle request.
 	Build() (Service, error)
 }
diff --git a/utils/api/api.go b/utils/api/api.go
index 394c802..929919c 100644
--- a/utils/api/api.go
+++ b/utils/api/api.go
@@ -135,13 +135,13 @@ func (ac *Container) AddDescriptor(descriptors ...interface{}) {
 
 // Generate generates API definitions.
 func (ac *Container) Generate(apiStyle string) (*Definitions, error) {
-	builder := builderutil.New(apiStyle)
+	builder := builderutil.New(service.APIStyle(apiStyle))
 	builder.SetModifier(ac.modifiers.Combine())
 	if err := builder.AddDescriptor(ac.descriptors...); err != nil {
 		return nil, err
 	}
 	definitions := builder.Definitions()
-	result, err := NewPathDefinitions(ac.typeContainer, definitions, apiStyle)
+	result, err := NewPathDefinitions(ac.typeContainer, definitions, service.APIStyle(apiStyle))
 	if err != nil {
 		return nil, err
 	}
diff --git a/utils/api/definitions.go b/utils/api/definitions.go
index c49dd2a..92021a4 100644
--- a/utils/api/definitions.go
+++ b/utils/api/definitions.go
@@ -22,7 +22,6 @@ import (
 
 	"github.com/caicloud/nirvana/definition"
 	"github.com/caicloud/nirvana/service"
-	"github.com/caicloud/nirvana/service/builder"
 )
 
 // Parameter describes a function parameter.
@@ -98,9 +97,9 @@ type Definition struct {
 }
 
 // NewDefinition creates openapi.Definition from definition.Definition.
-func NewDefinition(tc *TypeContainer, d *definition.Definition, apiStyle string) (*Definition, error) {
+func NewDefinition(tc *TypeContainer, d *definition.Definition, apiStyle service.APIStyle) (*Definition, error) {
 	code := service.HTTPCodeFor(d.Method)
-	if apiStyle == builder.APIStyleRPC {
+	if apiStyle == service.APIStyleRPC {
 		code = http.StatusOK
 	}
 
@@ -170,7 +169,7 @@ func NewDefinition(tc *TypeContainer, d *definition.Definition, apiStyle string)
 }
 
 // NewDefinitions creates a list of definitions.
-func NewDefinitions(tc *TypeContainer, definitions []definition.Definition, apiStyle string) ([]Definition, error) {
+func NewDefinitions(tc *TypeContainer, definitions []definition.Definition, apiStyle service.APIStyle) ([]Definition, error) {
 	result := make([]Definition, len(definitions))
 	for i, d := range definitions {
 		cd, err := NewDefinition(tc, &d, apiStyle)
@@ -183,7 +182,7 @@ func NewDefinitions(tc *TypeContainer, definitions []definition.Definition, apiS
 }
 
 // NewPathDefinitions creates a list of definitions with path.
-func NewPathDefinitions(tc *TypeContainer, definitions map[string][]definition.Definition, apiStyle string) (map[string][]Definition, error) {
+func NewPathDefinitions(tc *TypeContainer, definitions map[string][]definition.Definition, apiStyle service.APIStyle) (map[string][]Definition, error) {
 	result := make(map[string][]Definition)
 	for path, defs := range definitions {
 		cds, err := NewDefinitions(tc, defs, apiStyle)
diff --git a/utils/api/utils.go b/utils/api/utils.go
index 020df3f..94352f1 100644
--- a/utils/api/utils.go
+++ b/utils/api/utils.go
@@ -150,11 +150,27 @@ func DescriptorForData(path string, data []byte, contentType string) definition.
 				Method:   definition.Get,
 				Consumes: []string{definition.MIMENone},
 				Produces: []string{contentType},
-				Function: func(context.Context) ([]byte, error) {
-					return data, nil
+				Function: func(context.Context) []byte {
+					return data
 				},
-				Parameters: []definition.Parameter{},
-				Results:    definition.DataErrorResults(""),
+				Results: []definition.Result{definition.DataResultFor("")},
+			},
+		},
+	}
+}
+
+// RPCDescriptorForData generates a RPCDescriptor for API docs page.
+func RPCDescriptorForData(path string, data []byte, contentType string) definition.RPCDescriptor {
+	return definition.RPCDescriptor{
+		Path: path,
+		Actions: []definition.RPCAction{
+			{
+				Consumes: []string{definition.MIMENone},
+				Produces: []string{contentType},
+				Function: func(context.Context) []byte {
+					return data
+				},
+				Results: []definition.Result{definition.DataResultFor("")},
 			},
 		},
 	}
diff --git a/utils/builder/builder.go b/utils/builder/builder.go
index 04371fd..ff522f3 100644
--- a/utils/builder/builder.go
+++ b/utils/builder/builder.go
@@ -30,7 +30,7 @@ import (
 	"strconv"
 	"text/template"
 
-	builderutil "github.com/caicloud/nirvana/service/builder"
+	"github.com/caicloud/nirvana/service"
 	"github.com/caicloud/nirvana/utils/api"
 )
 
@@ -60,7 +60,7 @@ func (b *APIBuilder) Build() (*api.Definitions, error) {
 	descriptors := make([]function, 0)
 	modifiers := make([]function, 0)
 
-	apiStyle := builderutil.APIStyleREST
+	apiStyle := string(service.APIStyleREST)
 
 	for _, pkg := range analyzer.Paths() {
 		groups := analyzer.PackageComments(pkg)


Test patch:
diff --git a/examples/api-basic/api/v2/app_test.go b/examples/api-basic/api/v2/app_test.go
index 1ac8046..5d6c5d1 100644
--- a/examples/api-basic/api/v2/app_test.go
+++ b/examples/api-basic/api/v2/app_test.go
@@ -22,12 +22,12 @@ import (
 	"testing"
 
 	"github.com/caicloud/nirvana/examples/api-basic/application"
-	"github.com/caicloud/nirvana/service/builder"
+	"github.com/caicloud/nirvana/service"
 	"github.com/caicloud/nirvana/utils/unittest"
 )
 
 func TestCreate(t *testing.T) {
-	s, err := unittest.NewTestService(builder.APIStyleREST, Descriptor())
+	s, err := unittest.NewTestService(service.APIStyleREST, Descriptor())
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/utils/unittest/unittest.go b/utils/unittest/unittest.go
index bba6ffe..25c345b 100644
--- a/utils/unittest/unittest.go
+++ b/utils/unittest/unittest.go
@@ -71,7 +71,7 @@ func (r *responseWriter) Bytes() []byte {
 }
 
 // NewTestService creates a service.Service for testing.
-func NewTestService(apiStyle string, desc ...interface{}) (service.Service, error) {
+func NewTestService(apiStyle service.APIStyle, desc ...interface{}) (service.Service, error) {
 	builder := builderutil.New(apiStyle)
 	builder.SetModifier(service.FirstContextParameter())
 	builder.AddFilter(service.RedirectTrailingSlash(), service.FillLeadingSlash(), service.ParseRequestForm())
@@ -83,7 +83,7 @@ func NewTestService(apiStyle string, desc ...interface{}) (service.Service, erro
 
 // NewTestServiceWithConfig creates a service.Service for testing with user specified modifier and
 // filters. If modifier or filters is nil, default option will be used.
-func NewTestServiceWithConfig(apiStyle string, desc []interface{}, modifier service.DefinitionModifier,
+func NewTestServiceWithConfig(apiStyle service.APIStyle, desc []interface{}, modifier service.DefinitionModifier,
 	filters []service.Filter) (service.Service, error) {
 	builder := builderutil.New(apiStyle)
 	if modifier == nil {