API

概念

Nirvana Definition

在 Nirvana 中,所有的 API 都是通过 Descriptor 和 Definition 进行描述的。首先看一个 List Messages 的 API 定义:

  1. // 在使用 nirvana init 创建的标准项目结构中,这个文件位于 pkg/apis/v1/descriptors/message.go
  2. func init() {
  3. register([]def.Descriptor{{
  4. // Path 定义了 API 路径
  5. Path: "/messages",
  6. // Definitions 数组包含了这个路径下的所有定义。
  7. Definitions: []def.Definition{listMessages},
  8. },
  9. }...)
  10. }
  11. // listMessages 定义了一个返回 Message 列表的 API
  12. var listMessages = def.Definition{
  13. // 这个 API 返回的是资源数组,所以使用 List 方法。
  14. Method: def.List,
  15. // Summary 是一个短语,用于描述这个 API 的用途。这个短语在生成文档和客户端的时候用于区分 API。
  16. // 这个字符串去掉空格后会作为生成客户端时的函数名,因此请确保这个字符串是有意义的。
  17. Summary: "List Messages",
  18. // 详细描述这个 API 的用途。
  19. Description: "Query a specified number of messages and returns an array",
  20. // 业务函数
  21. Function: message.ListMessages,
  22. // 对应业务函数的参数信息。用于告知 Nirvana 从请求的那一部分取得数据,然后传递给业务函数。
  23. Parameters: []def.Parameter{
  24. {
  25. // 参数来源
  26. Source: def.Query,
  27. // 参数名称,作为 key 从 Source 里取值。
  28. // 与业务函数的参数名称无关。
  29. Name: "count",
  30. // 默认值
  31. Default: 10,
  32. // 参数描述
  33. Description: "Number of messages",
  34. },
  35. },
  36. // 对应业务函数的返回结果。用于告知 Nirvana 业务函数返回结果如何放到请求的响应中。
  37. Results: def.DataErrorResults("A list of messages"),
  38. }

根据上面的 API 定义,再对应业务函数:

  1. // 在使用 nirvana init 创建的标准项目结构中,这个文件位于 pkg/message/message.go
  2. // Message describes a message entry.
  3. type Message struct {
  4. ID int `json:"id"`
  5. Title string `json:"title"`
  6. Content string `json:"content"`
  7. }
  8. // ListMessages returns all messages.
  9. func ListMessages(ctx context.Context, count int) ([]Message, error) {
  10. messages := make([]Message, count)
  11. for i := 0; i < count; i++ {
  12. messages[i].ID = i
  13. messages[i].Title = fmt.Sprintf("Example %d", i)
  14. messages[i].Content = fmt.Sprintf("Content of example %d", i)
  15. }
  16. return messages, nil
  17. }

可以看到,业务函数既不关心参数的来源和类型转换,也不关心如何将返回值写到响应里,只是按照业务需求实现逻辑。

Definition Method

在 Nirvana 中,我们建议所有的 API 都遵守 RESTful 风格,并且在 URL 中携带 API 的版本号。下表中展示了 Nirvana 中定义的动作以及对应的 API 定义。

Nirvana 方法HTTP 方法HTTP 成功状态码URL描述
ListGET200/apis/v1/resources获取资源列表
GetGET200/apis/v1/resources/{resource}根据资源唯一 ID/Name 获取资源
CreatePOST201/apis/v1/resources创建一个资源(非幂等)
UpdatePUT200/apis/v1/resources/{resource}更新一个资源(幂等)
PatchPATCH200/apis/v1/resources/{resource}修改一个资源的部分内容
DeleteDELETE204/apis/v1/resources/{resource}删除一个资源
AsyncCreatePOST202/apis/v1/resources异步创建资源
AsyncUpdatePUT202/apis/v1/resources/{resource}异步更新资源
AsyncPatchPATCH202/apis/v1/resources/{resource}异步修改资源部分内容
AsyncDeleteDELETE202/apis/v1/resources/{resource}异步删除资源

对于 Nirvana 异步方法,发出后服务端应当只是将请求加入执行队列,然后立刻返回一个关联的对象或者链接,供客户端后续查询请求执行状态。

所有的 Nirvana 方法都是语义层面的,为的是提高 API 定义的可读性。也就是说 List 和 Get 在一个 HTTP 请求中使用的都是 GET,两者没有区别。 但是为了使 API 定义更加明确,我们应该根据场景确定使用哪个 Nirvana 方法。比如某个 API 是返回一个资源列表的,那么 Nirvana 方法就应该是 List 而不是 Get。

Definition Source

Definition Source 用于描述一个业务函数的参数的来源和默认值。

参数来源名称描述
Path参数值来源于 API Path
Query参数值来源于 URL Query
Header参数值来源于 Request Header
Form参数值来源于 Request Body,但是 Content-Type 必须是 application/x-www-form-urlencoded 或 multipart/form-data
File参数值来源于 Request Body,但是 Content-Type 必须是 multipart/form-data
Body参数值来源于 Request Body
AutoAuto 类型对应的参数必须是一个结构体,通过结构体的 tag 定义来确定每个字段的来源
Prefab参数值来源于当前 server 内部,比如一个 DB 链接

Auto 类型的 tag 范例如下:

  1. type Example struct {
  2. Start int `source:"Query,start,default=100"`
  3. ContentType string `source:"Header,Content-Type"`
  4. }

tag 名称为 source。值使用逗号分隔,第一个参数表示参数来源,第二个表示名称。如果是 Body 类型名称可以为空。 如果需要给字段设置默认值,则需要使用 default={value} 的形式。

如果有多个 Auto 结构体,可以组合成一个:

  1. type AnotherAutoStruct struct {
  2. ...
  3. }
  4. type Example struct {
  5. Start int `source:"Query,start,default=100"`
  6. ContentType string `source:"Header,Content-Type"`
  7. AnotherAutoStruct
  8. }

对于没有 source 的结构体类型,会递归遍历以寻找带有 source 的字段。忽略所有没有 source 的字段。

Definition Destination

Definition Destination 用于描述一个业务函数的参数的来源和默认值。

返回值目标描述
Meta这个返回值类型必须是 map[string]string,会写入 Response Header
Data返回值可以是任意结构,自动转换并写入到 Response Body
Error错误类型,必须是 error

给项目添加一个 API

接下来我们给项目增加一个 API,用于获取一条消息:

  1. func init() {
  2. register([]def.Descriptor{{
  3. Path: "/messages",
  4. Definitions: []def.Definition{listMessages},
  5. }, {
  6. // 获取一条消息的 Descriptor。
  7. Path: "/messages/{message}",
  8. Definitions: []def.Definition{getMessage},
  9. },
  10. }...)
  11. }
  12. // 获取一条消息的 API 定义。
  13. var getMessage = def.Definition{
  14. // 因为只获取一条消息,此处为 Get。
  15. Method: def.Get,
  16. Summary: "Get Message",
  17. Description: "Get a message by id",
  18. // 业务函数
  19. Function: message.GetMessage,
  20. Parameters: []def.Parameter{
  21. // 这是一个工具方法,用于快速生成一个参数结构。
  22. // message 是从 API Path 里获取的。
  23. def.PathParameterFor("message", "Message id"),
  24. },
  25. Results: def.DataErrorResults("A message"),
  26. }

对应的业务函数如下:

  1. // GetMessage return a message by id.
  2. func GetMessage(ctx context.Context, id int) (*Message, error) {
  3. return &Message{
  4. ID: id,
  5. Title: "This is an example",
  6. Content: "Example content",
  7. }, nil
  8. }

添加 API 之后,编译运行。然后访问 http://localhost:8080/apis/v1/messages/100,即可获得结果(默认情况下都是 json 类型):

  1. {"id":100,"title":"This is an example","content":"Example content"}

这里添加的业务函数都是以 Golang 函数的方式呈现的。如果希望使用实例方法作为业务的处理函数,请参考 方法包