meta-api 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3fc58de696caf4cf3abade846de9af53bb6491bda575b361461aa67f07d2fb9
4
- data.tar.gz: 68d00c2d43086af73a10bbe6bfc103a5ee8611751b01ad2b9c811edf9197438a
3
+ metadata.gz: b4385e82bdaec80e15adc4b62b1259f30125720452e579d84a45c79471c2d9cb
4
+ data.tar.gz: f3b9baa683d4a757eabbcc9c13e86f133741f941f75c92bf8e28999d17f29653
5
5
  SHA512:
6
- metadata.gz: 18cf46619c9f286fe01fbc3b941a6ca5a5e380740ad94e4d6827040cd9c6a014f6249eb13bc609a9f56fe2c15da6e044250f610b0eafdf6202aa9b6a16a0f15b
7
- data.tar.gz: edc1532a86fe8776f061cfd1f550392ba280a4e693437b6d6260bfcfce8f0a33d7ae66e6f95250fb8530751307751a53857c0fd2a32a57ec9f8764bf6a9037da
6
+ metadata.gz: 29e7fa33cae933c185cad5ea6bc5e0a9e460f549b5ea8bd12a6217b93e473d61a9cd5eaba2f803c28a5a55710a3668db80c42ad656b526cfb8604e50cdb42721
7
+ data.tar.gz: c7774b6feb64e9646017a7034082ab81b9fcf9a3928450edd725192671126a472941da314f64eb5c798a253eec6c82a99ff0701db6eacbda31cff42b6a605de0
data/CHANGELOG.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # 更新日志
2
2
 
3
- ## 0.0.5(2023 年 418 日)
3
+ ## 0.0.6(2023 年 526 日)
4
+
5
+ 1. 添加了 Meta::Execution#abort_execution! 方法。
6
+ 2. 重新规范响应体的 application/json 设定,尽可能不过分设定。
7
+ 3. 修复了若干实现上和文档的 bug.
8
+
9
+ ## 0.0.5(2023 年 4 月 27 日)
4
10
 
5
11
  1. 调整了 `around` 钩子的执行顺序,它与 `before` 钩子共同定义顺序。
6
12
  2. 修复了若干 bug.
data/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # Meta 框架
2
2
 
3
- Meta 框架是一个适用于 Web API 的后端框架,采用 Ruby 语言编写。正如它的名字,它是用定义“元”信息的方式实现 API,同时一份符合 Open API 语义的文档也能同步生成。
3
+ Meta 框架是一个用于开发 Web API 的后端框架,采用 Ruby 语言编写。正如它的名字,它是用定义“元”信息的方式实现 API,同时生成一份符合 Open API 规格的接口文档。
4
4
 
5
5
  > 有关框架名称的由来,阅读[框架的名称由来](docs/名称由来.md)。
6
6
 
7
7
  ## 脚手架
8
8
 
9
- 你可直接使用我的脚手架项目上手体验:
9
+ 可直接使用我的[脚手架](https://github.com/yetrun/web-frame-example)项目上手体验:
10
10
 
11
11
  ```bash
12
12
  $ git clone https://github.com/yetrun/web-frame-example.git
@@ -26,9 +26,9 @@ gem 'meta-api', '~> 0.0.5' # Meta 框架处于快速开发阶段,引入时应
26
26
  require 'meta/api'
27
27
  ```
28
28
 
29
- > 或者可嵌入到 Rails 项目中使用,参见[为 Rails 项目带来参数验证效果](docs/Rails.md)。
29
+ > 或者可嵌入到 Rails 项目中使用,参考[为 Rails 项目带来参数验证效果](docs/Rails.md)。
30
30
 
31
- ## 快速上手
31
+ ## 快速一览
32
32
 
33
33
  ### 定义 API
34
34
 
@@ -52,12 +52,12 @@ class NotesAPI < Meta::Application
52
52
  param :note, type: 'object', ref: NoteEntity
53
53
  end
54
54
  status 201 do
55
- expose :note, type: 'object', ref: NoteEntity
55
+ expose :note, type: 'object', ref: NoteEntity.lock_scope('full')
56
56
  end
57
57
  action do
58
58
  note = Note.create!(params[:note])
59
59
  response.status = 201
60
- render :note, note, scope: 'full'
60
+ render :note, note
61
61
  end
62
62
  end
63
63
 
@@ -67,7 +67,7 @@ class NotesAPI < Meta::Application
67
67
  param :id, type: 'integer'
68
68
  end
69
69
  status 200 do
70
- expose :note, type: 'object', ref: NoteEntity
70
+ expose :note, type: 'object', ref: NoteEntity.lock_scope('full')
71
71
  end
72
72
  action do
73
73
  note = Note.find(params[:id])
@@ -81,7 +81,7 @@ class NotesAPI < Meta::Application
81
81
  param :note, type: 'object', ref: NoteEntity
82
82
  end
83
83
  status 200 do
84
- expose :note, type: 'object', ref: NoteEntity
84
+ expose :note, type: 'object', ref: NoteEntity.lock_scope('full')
85
85
  end
86
86
  action do
87
87
  note = Note.find(params[:id])
@@ -92,6 +92,7 @@ class NotesAPI < Meta::Application
92
92
 
93
93
  delete '/notes/:id' do
94
94
  title '删除笔记'
95
+ status 204
95
96
  action do
96
97
  note = Note.find(params[:id])
97
98
  note.destroy!
@@ -107,18 +108,12 @@ end
107
108
 
108
109
  ```ruby
109
110
  class NoteEntity < Meta::Entity
110
- property :id, type: 'integer', param: false
111
+ property :id, type: 'integer', param: false # 不作为参数传递
111
112
  property :title, type: 'string'
112
- property :content, type: 'string', render: { scope: 'full' }
113
+ property :content, type: 'string', render: { scope: 'full' } # 列表页接口不返回此字段
113
114
  end
114
115
  ```
115
116
 
116
- 我们发现了一些特殊的定义:
117
-
118
- - 标记 `id` 的 `param` 选项为 `false`,它不作为参数传递。
119
-
120
- - 标记 `content` 在 `render` 下的 `scope`,当且仅当显示传递 `scope` 为 `false` 时才会渲染此字段。(对比 *查看笔记列表* 和 *查看笔记* 接口)
121
-
122
117
  ### 生成 API 文档
123
118
 
124
119
  通过主动调用以下的方法可以生成 Open API 的规格文档:
@@ -127,20 +122,32 @@ end
127
122
  NotesAPI.to_swagger_doc
128
123
  ```
129
124
 
130
- 该 Open API 文档是 JSON 格式,可以在 Swagger UI 下预览效果。如果你不想寻找提供 Swagger UI 服务的站点,也不想自己搭建,可以直接使用我的:
125
+ 该 Open API 文档是 JSON 格式,可以在 Swagger UI 下预览效果。如果也不乐意自己搭建 Swagger UI,可以直接使用在线的:
131
126
 
132
127
  > http://openapi.yet.run/playground
133
128
 
134
129
  ### 将模块挂载在 Rack 下运行
135
130
 
136
- API 模块同时也是一个 Rack 中间件,它可以挂载在 Rack 下运行:
131
+ API 模块同时也是一个 Rack 中间件,它可以挂载在 Rack 下运行。假设以上文件分别位于 `notes_api.rb` 和 `note_entity.rb`,在项目下新建文件 `config.ru`,并写入:
137
132
 
138
133
  ```ruby
139
- # config.ru
134
+ require 'meta/api'
135
+ require_relative 'notes_api'
136
+ require_relative 'note_entity'
137
+
138
+ # 将文档挂载到 /api_spec 锚点下
139
+ map '/api_spec' do
140
+ run ->(env) {
141
+ [200, { 'CONTENT_TYPE' => 'application/json' }, [JSON.generate(NotesAPI.to_swagger_doc)]]
142
+ }
143
+ end
140
144
 
145
+ # 启动 NotesAPI 中定义的接口
141
146
  run NotesAPI
142
147
  ```
143
148
 
149
+ 然后执行命令:`bundle exec rackup`,接口即可启动。
150
+
144
151
  ## 文档
145
152
 
146
153
  - [教程](docs/教程.md)
@@ -148,7 +155,11 @@ run NotesAPI
148
155
 
149
156
  ## 支持
150
157
 
151
- 加 QQ 群(489579810)可获得实时答疑。
158
+ 你可以通过以下途径获得支持:
159
+
160
+ 1. 通过 GitHub 提交 [ISSUE](https://github.com/yetrun/web-frame/issues)
161
+ 2. 通过 QQ 群(489579810)获得实时答疑
162
+ 3. 对本项目提交 [Pull Request](https://github.com/yetrun/web-frame/pulls)
152
163
 
153
164
  ## License
154
165
 
@@ -1,6 +1,6 @@
1
1
  # 教程
2
2
 
3
- 现有的 Web API 框架并不关注文档的问题,文档往往是作为插件挂载到框架上的。但是,文档和业务实现并不需要割裂开,它们在很大程度上应该是耦合在一起的。比方说,某个接口我定义了参数如此,就该自动生成一致的文档向前端告知;同样,当我提供了文档是如此后,我的接口实现就改自动地约束为这样实现。
3
+ 现有的 Web API 框架并不关注文档的问题,文档往往是作为插件挂载到框架上的。但是,文档和业务实现并不需要割裂开,它们在很大程度上应该是耦合在一起的。比方说,某个接口我定义了参数如此,就该自动生成一致的文档向前端告知;同样,当我提供了文档是如此后,我的接口实现就该自动地约束为这样实现。
4
4
 
5
5
  Meta 框架天生就是将文档和实现统一起来的,并始终致力于此(如果真的有什么不一致或者不到位的地方,那只能说框架实现上尚有欠缺,并不能从思想上说本该如此)。Meta 框架与 Swagger 合作,致力于产生符合 Restful 和社区规范的文档格式。它提供了几乎完整的描述接口信息的宏命令,并且在描述接口的同时就能基本实现接口的一些约束,其中最重要的莫过于对参数和返回值的声明。
6
6
 
@@ -8,340 +8,483 @@ Meta 框架天生就是将文档和实现统一起来的,并始终致力于此
8
8
 
9
9
  在正式阅读本教程之前,有一些*准备工作*需要提前了解的。
10
10
 
11
- ### 一些限制
11
+ ### 只接受 JSON
12
12
 
13
- 在正式介绍框架能力之前,我先声明一下框架的约束。设定这些约束,主要是因为框架开发尚处于初期阶段,而且精力有限,因此将框架的完成度限定在更小的范围内。
13
+ **只接受格式为 `application/json` 的请求体参数,并且响应实体的格式一律为 `application/json`.**
14
14
 
15
- 1. **只接受格式为 `application/json` 的请求参数,并且响应实体的格式一律为 `application/json`.**
16
-
17
- 这在当前的环境下并不算太大的限制,如果你是致力于新项目的开发的话。但是,如果你处理旧项目,并且要求格式为 `application/json` 之外的格式,如 `application/xml`,则框架目前是无能为力的。
18
-
19
- 这中限制只包括请求参数的实体,诸如路径里的参数、或 query 中的参数依然可用,不受该限制。
15
+ 这在当前的环境下并不算太大的限制,如果你是致力于新项目的开发的话。但是,如果你处理旧项目,并且要求格式为 `application/json` 之外的格式,如 `application/xml`,则框架目前是不能自动处理的。
20
16
 
21
- 2. **由于只能接受 `application/json` 的请求格式,因此你无法使用传统的表单上传方式。**
22
-
23
- 表单上传有它特有的格式名,为 `multipart/form-data`. 因此,用框架提供的行为无法完成表单上传的格式。这并不是说无法完成表单上传的功能,你可能需要处理原生的 `Request` 对象(它是 Rack 架构提供的一个对象,参考 [Rack::Request](https://www.rubydoc.info/gems/rack/Rack/Request))。同样的,你如果要返回 `application/json` 之外的格式,你需要手动地处理原生的 `Response` 对象(同样也是 Rack 架构提供的一个对象,参考 [Rack::Response](https://www.rubydoc.info/gems/rack/Rack/Response))。
17
+ 这种限制只存在于通过 `params` 宏和 `status` 宏设定了请求体和响应体格式的情况。如果你没有用到这两个宏,那么你还是可以自由定义请求体和响应体的格式,只不过缺少了文档化的支持。自由实现需要用到 [`Rack::Request`](https://www.rubydoc.info/gems/rack/Rack/Request) 类和 [`Rack::Response`](https://www.rubydoc.info/gems/rack/Rack/Response) 类提供的方法,这是 Rack 架构提供的两个包装类,用于简化 HTTP 请求和响应操作。
24
18
 
25
19
  ### 教程脉络
26
20
 
27
- 首先,你将了解定义路由的全部知识。你从其他框架学习的经验也同样适用于本框架,如嵌套路由、before/after 钩子、异常拦截等、以及模块共享和复用等。
21
+ 首先,你将学到定义路由的全部知识。换句话说,你该如何具体地*描述*一个接口。一般来说,我们需要描述接口的标题、详述、标签、参数和返回值。
28
22
 
29
- 然后,你将了解路由的内部如何定义。换句话说,你该如何具体地*描述*一个接口。一般来说,我们需要描述接口的标题、详述、标签、参数和返回值。
23
+ 然后,你将学到命名空间的概念。命名空间用来组织接口的层级结构,并且会用到诸如如路由嵌套、before/after 钩子、异常拦截等概念。
30
24
 
31
- 接下来,我们将深入参数和返回值的定义。虽然说前面已经提到参数和返回值的知识,但仅覆盖最简单同时也是最常用的场景。参数和返回值的知识实在是太大了,有必要专门划出一个章节来介绍它。这里提一下,参数和返回值在 Meta 框架里都统一为一个叫做实体的概念,因此你只需要学会定义一种就能够同时定义两者了。
25
+ 从命名空间引申出的模块的概念也很重要。模块本身也是一个命名空间,命名空间用到的功能都可以用在模块中。除此之外,模块还用来组织大型应用的结构。最后,模块本身也是一个 Rack 应用,可以直接放在服务器下运行。
32
26
 
33
- 最后,将是一个生成文档的方法。虽然它很简单,仅仅是一个方法,但它如此重要以至于我不得不专门划出一个章节来强调它的重要性。说实话将这块内容放在最后我有点不太满意,它是如此重要,开篇就该提到。
27
+ 接下来是重点,我们将深入参数和返回值的定义。虽然说前面已经提到参数和返回值的知识,但仅覆盖最简单同时也是最常用的场景。参数和返回值的知识实在是太大了,有必要专门划出一个章节来介绍它。这里提一下,参数和返回值在 Meta 框架里都统一为一个叫做实体的概念,因此你只需要学会定义一种就能够同时定义两者了。
34
28
 
35
- ## 模块和路由定义
29
+ 最后,将是一个生成文档的方法。虽然它很简单,仅仅是一个方法,但它如此重要以至于我不得不专门划出一个章节来强调它的重要性。
36
30
 
37
- *(我抛弃了 Rack 和中间件的知识,如果你知道它将更好了)*
31
+ 文章的最后是特殊用法举例。说实话,我还没想好把它放哪,但它确实列举了几个比较常见的场景。
38
32
 
39
- Meta 中,`Meta::Application` 类用来定义一个模块。一个模块同时也是一个应用,将它挂载在 Rack 下可以直接作为一个服务运行。我将它称为模块主要是因为它可以复用(后面你将了解到使用 `apply` 方法复用一个模块)。
33
+ ### 继承和宏定义
40
34
 
41
- 我们先看一下最简单的 `Meta::Application` 实例:
35
+ 在使用 Meta 框架提供的组件时,我们往往先要继承一个类,然后直接在类定义中使用宏命令。所谓的宏命令其实就是一个 Ruby 方法,只不过在 DSL 术语中我们将它称为“宏”。
36
+
37
+ 例如,定义一个 API,我们继承的是 `Meta::Application` 类,然后在类中使用 `route` 宏定义路由:
42
38
 
43
39
  ```ruby
44
40
  class DemoApp < Meta::Application
45
- get do
46
- title '应用的根路径'
47
- action do
48
- response.body = ["Hello, world!"]
49
- end
41
+ route '/foo', :get do
42
+ # ... 具体的宏定义
50
43
  end
51
44
  end
52
45
  ```
53
46
 
54
- *将它挂载在 Rack 下并访问 `http://localhost:9292` 你将看到效果。*
47
+ 再比如继承 `Meta::Entity` 定义一个实体,实体内使用 `property` 宏定义属性:
55
48
 
56
- 这里,我们只是用 `get` 方法简单地定义了一个 `get /` 路由,并定义了该路由下的标题和实现。该实现没有用到 Meta 框架提供的参数和返回值的概念,只是简单地操纵原生 Rack Response 对象返回一个纯文本格式。
49
+ ```ruby
50
+ class UserEntity < Meta::Entity
51
+ property :name
52
+ property :age
53
+ end
54
+ ```
57
55
 
58
- ### 路由定义
56
+ ## 路由定义(`route` 宏)
59
57
 
60
- 除了 `get` 之外,我们还支持 `post`、`put`、`patch`、`delete` 四个方法。同时后面可跟一个路径表示路的路径:
58
+ `Meta::Application` 类内,第一个能做的事情就是定义路由。`route` 方法(以后我们称这种特定的 DSL 方法为“宏”)定义一个具体的路由(即接口):
61
59
 
62
60
  ```ruby
63
61
  class DemoApp < Meta::Application
64
- post do
65
- # ...
62
+ route '/', :get do
63
+ # 块内定义路由的详细元素
66
64
  end
65
+ end
66
+ ```
67
+
68
+ ### HTTP 路径和方法
67
69
 
68
- put '/foo' do
70
+ `route` 方法接受一个路径字符串和一个 HTTP 方法,并且可接受一个块用于定义路由的详细元素(将在后面讲到)。HTTP 方法我们一共支持五种,包括 `get`、`post`、`put`、`patch`、`delete`. 为此,我们提供了五个便捷方法用于简化 `route` 方法调用的书写,举例:
71
+
72
+ ```ruby
73
+ class DemoApp < Meta::Application
74
+ get do # 当路径为 `/` 时,路径参数可以省略
69
75
  # ...
70
76
  end
71
77
 
72
- patch '/bar' do
78
+ post '/foo' do
73
79
  # ...
74
80
  end
75
81
 
82
+ put '/foo/bar' do
83
+ # ...
84
+ end
85
+
86
+ patch '/foo/bar' do
87
+ # ...
88
+ end
89
+
76
90
  delete '/foo/bar' do
77
91
  # ...
78
92
  end
79
93
  end
80
94
  ```
81
95
 
82
- 以上方法只是 `route(path, method, &block)` 的简写,例如 `put '/foo'` 可以完整地写为 `route '/foo', :put`.
96
+ > 因为这种写法更为清晰并且视觉效果更好,教程的以后都用 `get`、`post`、`put`、`patch`、`delete` 五个方法代替 `route` 方法的调用。除非是只用到路径而不关心 HTTP 方法的情形。
83
97
 
84
- ### 路径定义
98
+ ### 通配符路径
85
99
 
86
- 路径中可以包括参数:
100
+ 当定义路由 `route /foo/bar` 时,它匹配的是完整的路径 `/foo/bar`. 当你需要匹配一堆路径时,需要为路由加上通配符符号。`:` 和 `*` 是通配符符号的两种,前者匹配一个部分,后者尽可能多地匹配剩余的部份。这么说如果没说清楚,我举两个例子即可明白:
87
101
 
88
102
  - `/foo/:id`:它将匹配诸如 `/foo/1`、`/foo/bar` 等路径,但不能匹配 `/foo/a/b/c` 这样的路径。
89
103
  - `/foo/*path`:它可以匹配 `/foo`、`/foo/bar`、`/foo/a/b/c` 等格式的路径。
90
104
 
91
- 凡是路径中带有命名参数的都能被访问到,例如 `request.params['id']`、`request.params['path']` 。(注意,方括号内一定得是字符串)
105
+ > 通配符符号后面的单词(`id` 和 `path`)是参数名称,它将路由中与其匹配的部分放到参数中可访问。这里先提一下,通过 `request.params['id']`、`request.params['path']` 可以访问到路由当中匹配的部分。
92
106
 
93
- 如果你不需要后续访问到参数,可以忽略命名。不过我认为加个名字语义上更加友好,尽管你不必用到。
107
+ 如果你不需要后续访问到参数,可以忽略命名:
94
108
 
95
109
  - `/foo/:`
96
110
  - `/foo/*`
97
111
 
98
- 在定义参数时要学会不拘一格,试想一下以下的路径定义将匹配哪些:
112
+ 再举两个路由参数的示例:
99
113
 
100
- - `/foo/:id/bar`
101
- - `/foo/*/bar`
114
+ - `/foo/:id/bar`:匹配诸如 `/foo/1/bar`、`/foo/2/bar` 等路径
115
+ - `/foo/*/bar`:匹配 `/foo/bar`、`/foo/a/bar`、`/foo/a/b/bar`、`/foo/a/b/c/bar` 等格式的路径。
102
116
 
103
- ### 嵌套路由
117
+ ### 定义路由的元信息(`meta` 宏)
104
118
 
105
- `route` 方式定义的路由是不支持嵌套的。有一个专门为定义嵌套路由而存在的命令:`namespace`.
119
+ `route` 宏内部,可使用两个宏: `meta` 宏定义路由的元信息,`action` 宏定义路由的执行逻辑。
106
120
 
107
- ```ruby
108
- class DemoApp < Meta::Application
109
- namespace '/user' do
110
- get do
111
- title '获取用户详情'
112
- end
121
+ 首先,通过 `meta` 宏定义路由的“元”信息。注意,“元”信息的作用是双向的,既可以定义接口的文档,也可以约束接口的行为。例如,在 ` meta` 宏内定义参数:
113
122
 
114
- put do
115
- title '更新用户详情'
123
+ ```ruby
124
+ post '/users' do
125
+ meta do
126
+ params do
127
+ param :name, type: 'string', description: '姓名'
128
+ param :age, type: 'integer', description: '年龄'
116
129
  end
117
130
  end
118
131
  end
119
132
  ```
120
133
 
121
- ### 钩子
134
+ 它会产生两个方面的效果:
122
135
 
123
- *(如果不涉及到钩子和异常拦截,嵌套路由将毫无意义。)*
136
+ 1. 文档方面:接口文档的参数部分会暴露出两个参数:`name`、`age`,并声明它的类型和描述信息。
137
+ 2. 业务逻辑方面:业务代码执行时,通过标准的方法获取参数时会对参数作校验。这里面它只会提取出参数的两个字段(`name` 和 `age`),并对它们俩的类型作校验。如果参数不符合定义,会向客户端抛出一个错误。
124
138
 
125
- 正如名字所表达的那样,`before` 就是 before 钩子,`after` 就是 after 钩子。将以上例子加上 `before` 钩子将如下:
139
+ #### `meta` 宏一览
126
140
 
127
- ```ruby
128
- class DemoApp < Meta::Application
129
- namespace '/user' do
130
- before do
131
- @user = get_user
132
- end
141
+ `meta` 宏内部现在只提供了以下五个方法:
133
142
 
134
- get do
135
- title '获取用户详情'
143
+ ```ruby
144
+ post '/users' do
145
+ meta do
146
+ title '创建用户'
147
+ description '接口的详细描述'
148
+ tags ['User'] # 定义接口的 Tag,传递一个数组
149
+ params do
150
+ # 内部定义参数结构
136
151
  end
137
-
138
- put do
139
- title '更新用户详情'
152
+ status 200 do
153
+ # 内部定义返回值结构
140
154
  end
141
155
  end
142
156
  end
143
157
  ```
144
158
 
145
- 同时还支持 `around` 钩子(*试验特性*):
159
+ 以上,`title`、`description`、`tags` 宏分别定义接口的标题、描述信息和标签列表。`params``status` 宏定义接口的参数和返回值,其内部定义比较复杂,将在后面详细讲解。
146
160
 
147
- ```ruby
148
- class DemoApp < Meta::Application
149
- before { puts 1 }
150
- around { |next_action|
151
- puts 2
152
- next_action.execute(self)
153
- puts 7
154
- }
155
- before { puts 3 }
156
- after { puts 5 }
157
- after { puts 6 }
161
+ #### `meta` 宏展开
162
+
163
+ `meta` 宏可以展开定义,亦即可以直接在 `route` 定义内部直接使用 `meta` 宏定义的语法,它是 `route` 定义内部提供的一种快捷方式:
158
164
 
159
- get '/request' do
160
- puts 4
165
+ ```ruby
166
+ post '/users' do
167
+ title '创建用户'
168
+ description '接口的详细描述'
169
+ tags ['User'] # 定义接口的 Tag,传递一个数组
170
+ params do
171
+ # 内部定义参数结构
172
+ end
173
+ status 200 do
174
+ # 内部定义返回值结构
161
175
  end
162
176
  end
163
177
  ```
164
178
 
165
- 所有钩子的执行顺序是:
179
+ > 由于展开定义的方式写起来更加便捷,因此后面的教程示例都将采取这样的写法。
166
180
 
167
- 1. `before` 钩子和 `around` 钩子的前半部分,按照定义的顺序执行
168
- 2. 然后执行路由方法。
169
- 3. 然后执行 `after` 钩子,按照定义的顺序执行。
170
- 4. 最后执行 `around` 钩子的后半部分,按照定义的逆序执行。
181
+ ### 定义路由的执行逻辑(`action` 宏)
171
182
 
172
- ### 异常拦截
183
+ `action` 宏定义业务代码部分。将上面的 `POST /users` 接口的逻辑实现定义完全,大概率是以下这个样子:
173
184
 
174
- 在 `namespace` 中使用 `rescue_error` 拦截异常。
185
+ ```ruby
186
+ post '/users' do
187
+ # ... 定义路由的 meta 部分
188
+ action do
189
+ user = User.create!(params[:user])
190
+ render :user, user
191
+ end
192
+ end
193
+ ```
194
+
195
+ 其中,用到的 `params` 方法和 `render` 方法将在后面讲到。
196
+
197
+ ## 层次化地定义路由(`namespace` 宏)
198
+
199
+ ### 使用 `namespace` 宏定义嵌套路由
175
200
 
176
201
  ```ruby
177
202
  class DemoApp < Meta::Application
178
- namespace '/user' do
179
- rescue_error RecordNotFound do |e|
180
- response.status = 404
181
- response.body = ["所访问的资源不存在"]
203
+ get do # 匹配 GET /
204
+ # ...
205
+ end
206
+
207
+ namespace '/foo' do
208
+ get do # 匹配 GET /foo
209
+ # ...
210
+ end
211
+
212
+ post '/bar' do # 匹配 POST /foo/bar
213
+ # ...
214
+ end
215
+
216
+ namespace '/baz' do
217
+ # ... 匹配前缀为 /foo/baz
182
218
  end
183
219
  end
184
220
  end
185
221
  ```
186
222
 
187
- ### 关于嵌套的进一步说明
223
+ `namespace` 宏是定义父级结构的,它不能定义到具体的路由,它的内部需要有 `namespace` 宏或 `route` 宏定义更具体的结点。**`namespace` 宏匹配的是路径的前缀。**
224
+
225
+ 而 `route` 宏是最深层次的结构,它定义的是具体的路由和它的行为,它的内部不能有 `namesapce` 宏及 `route` 宏。**`route` 宏要匹配完整的路径。**
226
+
227
+ > 为什么要定义一个 `namespace` 宏,它不仅仅是减少路径的重复代码这么简单。综合来讲,`namespace` 宏有如下作用:
228
+ >
229
+ > 1. 通过组合 `namespace` 和 `route` 宏来定义应用的层次结构。
230
+ > 2. `namespace` 内可定义钩子,用于公共的运行逻辑。
231
+ > 3. 可定义 `namespace` 级的异常拦截处理方法。
232
+ > 4. 在 `namespace` 定义 `meta` 宏,可定义公共的“元”信息。
233
+
234
+ ### 钩子
235
+
236
+ `namesapce` 内提供了两种钩子:`before`、`after`. 它在整个 `namespace` 层级执行一遍。
188
237
 
189
- 钩子只在当前作用域和它的子作用域下起作用,父级作用域不会起作用。
238
+ 正如名字所表达的那样,`before` 在 `action` 宏之前执行,`after` 在 `action` 宏之后执行。
190
239
 
191
240
  ```ruby
192
241
  class DemoApp < Meta::Application
193
242
  namespace '/foo' do
194
243
  before do
195
- @foo = 'foo'
244
+ puts 1
196
245
  end
197
246
 
247
+ after do
248
+ puts 2
249
+ end
250
+
198
251
  get do
199
252
  action do
200
- p @foo # 'foo'
201
- p @bar # nil
253
+ puts 3
202
254
  end
203
255
  end
204
256
 
205
- namespace '/bar' do
206
- before do
207
- @foo = 'foo'
208
- end
209
-
210
- get do
211
- action do
212
- p @foo # 'foo'
213
- p @bar # 'bar'
214
- end
257
+ put do
258
+ action do
259
+ puts 4
215
260
  end
216
261
  end
217
262
  end
218
263
  end
219
264
  ```
220
265
 
221
- 异常拦截先拦截子作用域;如果拦截失败则继续在父作用域下拦截。
266
+ 当用户访问 `GET /foo` 接口时,依次打印数字 `1`、`3`、`2`;当用户访问 `PUT /foo` 接口时,依次打印数字 `1`、`4`、`2`.
267
+
268
+ #### `around` 钩子(实验特性)
269
+
270
+ Meta 框架同时还支持 `around` 钩子, `around` 钩子会包裹 `action` 执行:
222
271
 
223
272
  ```ruby
224
273
  class DemoApp < Meta::Application
225
274
  namespace '/foo' do
226
- rescue_error ErrorOne do
227
- # 它将捕获 '/foo' 路由下的异常
275
+ around do |next_action|
276
+ puts 1
277
+ next_action.execute(self)
278
+ puts 2
279
+ end
280
+
281
+ get do
282
+ action do
283
+ puts 3
284
+ end
228
285
  end
229
286
 
230
- namespace '/bar' do
231
- rescue_error ErrorTwo do
232
- # 它将捕获 '/foo/bar' 路由下的异常
287
+ put do
288
+ action do
289
+ puts 4
233
290
  end
234
291
  end
235
292
  end
236
293
  end
237
294
  ```
238
295
 
239
- ### 模块
296
+ 同样的,当用户访问 `GET /foo` 接口时,依次打印数字 `1`、`3`、`2`;当用户访问 `PUT /foo` 接口时,依次打印数字 `1`、`4`、`2`.
297
+
298
+ > `around` 钩子现在还处于实验阶段,不建议实际开发中使用。当 `around` 钩子混合定义 `before`、`after` 钩子时,其执行的顺序比较混乱。而且,现在的 `around` 钩子还无法覆盖参数解析和返回值渲染的过程,这让它们的应用范围受到限制。当需要完整覆盖接口执行的全周期时,推荐使用 Rack 的中间件。最后,一定要在恰当的时机执行 `next_action.execute(self)`,否则后续的动作将不会得到执行。 `next_action.execute(self)` 调用稍显繁琐,不够优雅。
299
+
300
+ #### 所有钩子的执行顺序
301
+
302
+ 如果只包含 `before` 和 `after` 钩子,则执行的顺序是:
240
303
 
241
- `Meta::Application` 可以像 `namespace` 一样,定义路由、设置 before/after 钩子、拦截异常等。将以上例子里 `/foo` 的部分抽成一个模块如下:
304
+ 1. `before` 钩子先执行,按照定义的顺序;
305
+ 2. 接着执行 `action` 定义的块;
306
+ 3. 最后执行 `after` 钩子,按照定义的顺序。
307
+
308
+ 举例(以下按照 `1`、`2`、`3` 的数字顺序执行):
242
309
 
243
310
  ```ruby
244
- class Foo < Meta::Application
245
- rescue_error ErrorOne do
246
- # 它将捕获 '/foo' 路由下的异常
247
- end
311
+ class DemoApp < Meta::Application
312
+ namespace '/foo' do
313
+ before { puts 1 }
314
+ before { puts 2 }
315
+ after { puts 4 }
316
+ after { puts 5 }
248
317
 
249
- namespace '/bar' do
250
- rescue_error ErrorTwo do
251
- # 它将捕获 '/foo/bar' 路由下的异常
318
+ get '/request' do
319
+ puts 3
252
320
  end
253
321
  end
254
322
  end
255
323
  ```
256
324
 
257
- 为达到同样的效果,我们可以在 `DemoApp` 下应用这个模块:
325
+ 如果还包含 `around` 钩子,则会复杂一些,但大体上是:
326
+
327
+ 1. 最先执行的是 `before` 钩子以及 `around` 钩子的前半部分,按照定义的顺序;
328
+ 2. 接着执行 `action` 定义的块;
329
+ 3. 然后执行 `after` 钩子,按照定义的顺序;
330
+ 4. 最后执行 `around` 钩子的后半部分,按照定义的**逆序**执行。
331
+
332
+ 举例(以下按照 `1`、`2`、`3` 的数字顺序执行):
258
333
 
259
334
  ```ruby
260
335
  class DemoApp < Meta::Application
261
336
  namespace '/foo' do
262
- apply Foo
337
+ before { puts 1 }
338
+ around { |next_action|
339
+ puts 2
340
+ next_action.execute(self)
341
+ puts 9
342
+ }
343
+ around { |next_action|
344
+ puts 3
345
+ next_action.execute(self)
346
+ puts 8
347
+ }
348
+ before { puts 4 }
349
+ after { puts 6 }
350
+ after { puts 7 }
351
+
352
+ get '/request' do
353
+ puts 5
354
+ end
263
355
  end
264
356
  end
265
357
  ```
266
358
 
267
- 模块应用最常用的场景是将接口分离到单独的文件中定义。这里我贴出我在一个实际项目中的模块划分:
359
+ #### 使用钩子的注意事项
360
+
361
+ 请注意,钩子的执行顺序是严格按照以上顺序执行的,与你定义的顺序无关。请确保 `before` 和 `around` 钩子优先于 `after` 的顺序定义,因为它们的执行也是优先于 `after` 的。
362
+
363
+ 另外,钩子的执行不会覆盖参数解析和返回值渲染,亦即 `before` 钩子在参数解析之后执行,`after` 钩子在返回值渲染之前执行,而 `around` 钩子亦不会覆盖参数解析和返回值渲染。
364
+
365
+ 钩子不会中断执行。如果要在钩子中中断程序的执行,可使用 `abort_execution!` 方法:
268
366
 
269
367
  ```ruby
270
- class OpenAPIApp < Meta::Application
271
- apply API::Logins
272
- apply API::Users
273
- apply API::Organizations
274
- apply API::Projects
275
- apply API::Versions
276
- apply API::Members
368
+ before do
369
+ token = request.get_header('HTTP_X_TOKEN')
370
+ // ... parse token
371
+ rescue TokenInvalidError => e
372
+ response.status = 401
373
+ response.message = "Token 格式异常:#{e.message}"
374
+ abort_execution!
277
375
  end
278
376
  ```
279
377
 
280
- ## 路由内部定义
378
+ `abort_execution!` 同时会跳过返回值渲染的执行。
281
379
 
282
- 现在我们关注路由内部细节的定义,包括标题、描述、参数、返回值乃至于如何实现业务逻辑等。我们知道,路由是通过 `route` 方法(以及它的一系列便捷方法 `get`、`post` 等),也就是说我们现在开始关注 `route` 方法内部能定义什么。
380
+ 冷知识:`Meta::Application` 本身也可视为一个命名空间定义,`namespace` 内能用到的方法也可以在 `Meta::Application` 内使用。
283
381
 
284
- 本来想大书特书,结果发现以下代码示例便能将用到的宏命令列举完毕。
382
+ ### 异常拦截
383
+
384
+ 在 `namespace` 中可使用 `rescue_error` 拦截异常。
285
385
 
286
386
  ```ruby
287
- route '/user', :put do
288
- title '更新用户'
289
- description '接口的详细描述'
290
- tags ['User'] # 传递一个数组
291
- params do
292
- # 定义参数
293
- param :user do
294
- param :name
295
- param :age
387
+ class DemoApp < Meta::Application
388
+ namespace '/users/:id' do
389
+ rescue_error ActiveRecord::RecordNotFound do |e|
390
+ response.status = 404
391
+ response.body = ["所访问的资源不存在"]
296
392
  end
297
- end
298
- status 200 do
299
- # 定义返回值,其中 200 是状态码
300
- expose :user do
301
- expose :name
302
- expose :age
393
+
394
+ get do
395
+ action do
396
+ user = User.find(params[:id])
397
+ end
303
398
  end
304
399
  end
305
- action do
306
- # 业务逻辑在这里实现,通过 params 方法访问参数,render 方法渲染实体
307
- user = get_user
308
- user.update!(params[:user])
309
- render :user, user
310
- end
311
400
  end
312
401
  ```
313
402
 
314
- ### `meta` 命令
403
+ 以下是 Meta 框架抛出的异常:
404
+
405
+ - `Meta::Errors::NoMatchingRoute`:路由不匹配时。
406
+ - `Meta::Errors::ParameterInvalid`:参数存在异常时。
407
+ - `Meta::Errors::RenderingInvalid`:响应值存在异常时。
408
+ - `Meta::Errors::UnsupportedContentType`:框架只支持 `application/json` 的参数格式。当客户端的请求体不是这个格式时,会抛出这个错误。
315
409
 
316
- `action` 之外,`route` 块下其余的命令都与文档的生成相关。它们都可以被汇总到一个称为 `meta` 的块内:
410
+ #### 嵌套命名空间下的异常拦截
411
+
412
+ 拦截异常先在子作用域下拦截;如果拦截失败则继续在父作用域下拦截。下面的例子中:
317
413
 
318
414
  ```ruby
319
- route '/user', :put do
320
- meta do
321
- title '更新用户'
322
- description '接口的详细描述'
323
- tags ['User'] # 传递一个数组
324
- params do
325
- # 定义参数
415
+ class DemoApp < Meta::Application
416
+ namespace '/foo' do
417
+ rescue_error ErrorOne do
418
+ puts "rescued in /foo" #(1)
326
419
  end
327
- status 200 do
328
- # 定义返回值
420
+
421
+ rescue_error ErrorTwo do
422
+ puts "rescued in /foo" #(2)
423
+ end
424
+
425
+ namespace '/bar' do
426
+ rescue_error ErrorOne do
427
+ puts "rescued in /foo/bar" #(3)
428
+ end
429
+
430
+ get do
431
+ action do
432
+ raise ErrorOne
433
+ end
434
+ end
435
+
436
+ put do
437
+ action do
438
+ raise ErrorTwo
439
+ end
440
+ end
329
441
  end
330
442
  end
331
- action do
332
- # 业务逻辑仍在这里实现
443
+ end
444
+ ```
445
+
446
+ 调用 `GET /foo/bar` 请求时会在(3)处被捕获;调用 `PUT /foo/bar` 请求时会在(2)处被捕获。
447
+
448
+ #### `Meta::Errors::NoMatchingRoute` 只能在顶层被捕获
449
+
450
+ 由于框架实现的特殊性,异常 `Meta::Errors::NoMatchingRoute` 只会在顶层抛出。因此,只有在 `namespace` 的顶层捕获才有效果。
451
+
452
+ ```ruby
453
+ class DemoApp < Meta::Application
454
+ # 在此捕获有效
455
+ rescue_error Meta::Errors::NoMatchingRoute do |e|
456
+ response.status = 404
457
+ response.body = ["404 Not Found"]
458
+ end
459
+
460
+ namespace '/foo' do
461
+ # 在此捕获无效
462
+ rescue_error Meta::Errors::NoMatchingRoute do |e|
463
+ response.status = 404
464
+ response.body = ["404 Not Found"]
465
+ end
333
466
  end
334
467
  end
335
468
  ```
336
469
 
337
- ### `namespace` 下的 `meta` 命令
470
+ 即使是上面的例子,调用 `GET /foo/bar` 请求时也只有顶层的异常拦截起了作用。
338
471
 
339
- `namespace` 下可以也定义 `meta` 块,它可以定义接口声明的公共部分,应用到它的子路由下:
472
+ ### `namespace` `meta`
473
+
474
+ 同 `route` 宏内,`namespace` 宏内部可以定义 `meta` 宏。`namespace` 定义的 `meta` 宏定义下属路由的公共部分,其会应用到全部子路由,除非在 `route` 宏内复写。
340
475
 
341
476
  ```ruby
342
- namespace '/user/:id' do
477
+ namespace '/users/:id' do
478
+ # 以下 meta 内定义的部分会应用到 GET /users/:id 和 PUT /users/:id 两个路由。
479
+ # 其中,因为 title 两个路由有重写,因此会使用两个路由自己的 title 定义。
480
+ # description 两个路由都没有独自定义,因此会统一使用 meta 中的定义。
481
+ # tags 同理,它们都挂载在同一个 Tag 下。
482
+ # params 定义比较特殊,子路由下的定义不是复写而是补充。因此 GET /users/:id 包含一个参数 id,PUT /users/:id 包含了两个参数
483
+ # id 和 user.
484
+ # status 与 params 同理,但由于子路由内没有 status 定义,从而它们两个都是使用 meta 中的定义,即返回一个 user 属性。
343
485
  meta do
344
- # 在 namespace 下定义 title 和 description 没有意义
486
+ title '处理用户详情'
487
+ description '通过路径参数获取用户数据,并对用户数据做一定的处理,比如查看、更新'
345
488
  tags ['User'] # 该 namespace 下的接口归到 User 标签下
346
489
  params do # 定义共同参数
347
490
  param :id
@@ -374,6 +517,206 @@ namespace '/user/:id' do
374
517
  end
375
518
  ```
376
519
 
520
+ ## 路由的执行环境 (`Meta::Execution`)
521
+
522
+ 当路由在执行过程中,会将块绑定到一个 `Meta::Execution` 实例中执行。`before`、`after` 等钩子,`action` 定义的块内,以及异常拦截的过程中,其执行环境都会绑定到当前的 `Meta::Execution` 实例。
523
+
524
+ ```ruby
525
+ class DemoApp < Meta::Application
526
+ before do
527
+ @current_user = "Jim" # 设置一个环境变量
528
+ end
529
+
530
+ rescue_error StandardError do
531
+ p @current_user # 可以访问先前设置的实例变量
532
+ end
533
+
534
+ get '/user' do
535
+ action do
536
+ p @current_user # 可以访问先前设置的实例变量
537
+ raise # 抛出异常
538
+ end
539
+ end
540
+ end
541
+ ```
542
+
543
+ ### `Meta::Execution` 提供的方法。
544
+
545
+ #### `#request`
546
+
547
+ `request` 方法返回 [`Rack::Request`](https://www.rubydoc.info/gems/rack/Rack/Request) 实例,它是 Rack 框架提供的包装类,用于简化 HTTP 操作。
548
+
549
+ #### `#response`
550
+
551
+ `response` 方法返回 [`Rack::Response`](https://www.rubydoc.info/gems/rack/Rack/Response) 的实例,它是 Rack 框架的包装类,用于简化 HTTP 操作。
552
+
553
+ #### `#params`
554
+
555
+ 不要与 `.params` 宏所混淆,它是 `Meta::Execution` 提供的实例方法,返回解析后的参数。参数的解析参考 `params` 宏的定义。
556
+
557
+ #### `#render`
558
+
559
+ 定义实际响应体时使用 `render` 方法。`render` 方法参考 `status` 定义的响应体格式过滤和验证字段。
560
+
561
+ #### `#abort_execution!`
562
+
563
+ 中断后续的执行。如果是在 `before` 块中执行这个方法,则跳过后续的 `before`、`action` 和 `after` 块;如果是在 `action` 块中执行这个方法,则跳过 `after` 块。注意,这个方法会跳过响应体渲染阶段。
564
+
565
+ ### 共享模块
566
+
567
+ 不要在 `Meta::Application` 内部定义方法,在它内部直接定义的方法应用到 `Meta::Execution` 实例。如果需要在当前以及后续路由用到公共的方法,可以在 `shared` 块内定义:
568
+
569
+ ```ruby
570
+ class DemoApp < Meta::Application
571
+ shared do
572
+ def current_user
573
+ @current_user
574
+ end
575
+ end
576
+
577
+ before do
578
+ @current_user = "Jim" # 设置一个环境变量
579
+ end
580
+
581
+ get '/user' do
582
+ action do
583
+ p current_user # 在路由内访问方法
584
+ end
585
+ end
586
+
587
+ namespace '/foo' do
588
+ get '/user' do
589
+ action do
590
+ p current_user # 在子命名空间内也能访问到方法
591
+ end
592
+ end
593
+ end
594
+ end
595
+ ```
596
+
597
+ 除此之外,`shared` 的参数也接受模块。
598
+
599
+ ```ruby
600
+ module HelperFoo
601
+ def foo; 'foo' end
602
+ end
603
+
604
+ module HelperBar
605
+ def bar; 'bar' end
606
+ end
607
+
608
+ class DemoApp < Meta::Application
609
+ shared HelperFoo, HelperBar
610
+
611
+ get '/user' do
612
+ action do
613
+ p foo
614
+ p bar # 可访问模块定义的方法
615
+ end
616
+ end
617
+ end
618
+ ```
619
+
620
+ ## 模块(`Meta::Application`)
621
+
622
+ ### `Meta::Application` 等同于 `namespace` 定义
623
+
624
+ 要知道 `Meta::Application`,第一个事情就是它等同于 `namespace` 定义。像 `namespace` 一样,能定义路由、钩子、异常拦截的地方都可以在 `Meta::Application` 内直接定义。
625
+
626
+ ```ruby
627
+ class DemoApp < Meta::Application
628
+ # meta 定义,能应用到下属子路由的所有地方
629
+ meta do
630
+ # ...
631
+ end
632
+
633
+ # 它将捕获下属子路由的所有异常
634
+ rescue_error Exception do
635
+ # ...
636
+ end
637
+
638
+ # 钩子,最先执行
639
+ before do
640
+ # ...
641
+ end
642
+
643
+ # 钩子,最后执行
644
+ after do
645
+ # ...
646
+ end
647
+
648
+ # 定义嵌套命名空间
649
+ namespace '/...' do
650
+ # ...
651
+ end
652
+
653
+ # 也可以直接定义路由
654
+ route '/...', :post do
655
+ # ...
656
+ end
657
+ end
658
+ ```
659
+
660
+ 你可以将 `Meta::Application ` 视为路径定义为 `/` 的命名空间。
661
+
662
+ ### `Meta::Application` 是可复用的模块
663
+
664
+ 遇到大型项目时,将 API 定义分离成若干个单独的文件更好的组织。做到这一点,就用到 `namespace` 中提供的 `apply` 方法。
665
+
666
+ 继承自 `Meta::Application` 的类都是一个模块,它可以在 `namespace` 中被复用。
667
+
668
+ ```ruby
669
+ class Foo < Meta::Application
670
+ route '/foo' do
671
+ # ...
672
+ end
673
+ end
674
+
675
+ class DemoApp < Meta::Application
676
+ apply Foo
677
+ end
678
+ ```
679
+
680
+ 将定义写在一个类里,其等价于:
681
+
682
+ ```ruby
683
+ class DemoApp < Meta::Application
684
+ route '/foo' do
685
+ # ...
686
+ end
687
+ end
688
+ ```
689
+
690
+ `apply` 方法还可跟一个参数 `tags: [...]`,统一覆盖被引入的模块在渲染文档时声明的 `tags`:
691
+
692
+ ```ruby
693
+ class OpenAPIApp < Meta::Application
694
+ apply API::Logins, tags: ['Login']
695
+ apply API::Users, tags: ['User']
696
+ apply API::Organizations, tags: ['Organization']
697
+ apply API::Projects, tags: ['Project']
698
+ apply API::Versions, tags: ['Version']
699
+ apply API::Members, tags: ['Member']
700
+ end
701
+ ```
702
+
703
+ ### `Meta::Application` 是一个 Rack 应用
704
+
705
+ `Meta::Application` 同时也是一个 Rack 应用,将它挂载在 Rack 下可以直接作为一个服务运行。我们看一个最简单的 `Meta::Application` 实例:
706
+
707
+ ```ruby
708
+ class DemoApp < Meta::Application
709
+ route '/', :get do
710
+ title '应用的根路径'
711
+ action do
712
+ response.body = ["Hello, world!"]
713
+ end
714
+ end
715
+ end
716
+ ```
717
+
718
+ > 将它挂载在 Rack 下并访问 `http://localhost:9292` 你将看到接口返回 `"Hello, world"` 文本。
719
+
377
720
  ## 参数定义
378
721
 
379
722
  本节介绍参数和返回值如何定义。因为 Meta 框架在底层不区分参数和返回值,它们都统一为“实体”的概念。因此,当涉及到语法细节时,在参数、返回值、实体内都是一致的。
@@ -100,22 +100,36 @@ module Meta
100
100
  end
101
101
 
102
102
  new_hash = entity_schema.filter(hash, **options, execution: self, stage: :render, validation: ::Meta.config.render_validation, type_conversion: ::Meta.config.render_type_conversion)
103
+ response.content_type = 'application/json' if response.content_type.nil?
103
104
  response.body = [JSON.generate(new_hash)]
104
105
  else
105
106
  # 渲染多键值结点
106
- new_hash = renders.map do |key, render_content|
107
+ errors = {}
108
+ final_value = {}
109
+ renders.each do |key, render_content|
107
110
  raise Errors::RenderingError, "渲染的键名 `#{key}` 不存在,请检查实体定义以确认是否有拼写错误" unless entity_schema.properties.key?(key)
108
111
  schema = entity_schema.properties[key].schema(:render)
109
-
110
- filtered = schema.filter(render_content[:value], **render_content[:options], execution: self, stage: :render)
111
- [key, filtered]
112
+ final_value[key] = schema.filter(render_content[:value], **render_content[:options], execution: self, stage: :render)
113
+ rescue JsonSchema::ValidationErrors => e
114
+ # 错误信息再度绑定 key
115
+ errors.merge! e.errors.transform_keys! { |k| k.empty? ? key : "#{key}.#{k}" }
112
116
  end.to_h
113
- response.body = [JSON.generate(new_hash)]
117
+
118
+ if errors.empty?
119
+ response.content_type = 'application/json' if response.content_type.nil?
120
+ response.body = [JSON.generate(final_value)]
121
+ else
122
+ raise Errors::RenderingInvalid.new(errors)
123
+ end
114
124
  end
115
125
  rescue JsonSchema::ValidationErrors => e
116
126
  raise Errors::RenderingInvalid.new(e.errors)
117
127
  end
118
128
 
129
+ def abort_execution!
130
+ raise Abort
131
+ end
132
+
119
133
  private
120
134
 
121
135
  def parse_raw_params
@@ -134,22 +148,18 @@ module Meta
134
148
  end
135
149
 
136
150
  def parse_request_body_for_replacing
137
- begin
138
- request_body_schema.filter(params(:raw), stage: :param)
139
- rescue JsonSchema::ValidationErrors => e
140
- raise Errors::ParameterInvalid.new(e.errors)
141
- end
151
+ request_body_schema.filter(params(:raw), stage: :param)
152
+ rescue JsonSchema::ValidationErrors => e
153
+ raise Errors::ParameterInvalid.new(e.errors)
142
154
  end
143
155
 
144
156
  def parse_request_body_for_updating
145
- begin
146
- request_body_schema.filter(params(:raw), stage: :param, discard_missing: true)
147
- rescue JsonSchema::ValidationErrors => e
148
- raise Errors::ParameterInvalid.new(e.errors)
149
- end
157
+ request_body_schema.filter(params(:raw), stage: :param, discard_missing: true)
158
+ rescue JsonSchema::ValidationErrors => e
159
+ raise Errors::ParameterInvalid.new(e.errors)
150
160
  end
151
161
 
152
- class Abort < StandardError
162
+ class Abort < Exception
153
163
  end
154
164
 
155
165
  # 使得能够处理 Execution 的类作为 Rack 中间件
@@ -162,7 +172,6 @@ module Meta
162
172
  execute(execution, request.path)
163
173
 
164
174
  response = execution.response
165
- response.content_type = 'application/json' unless response.no_content?
166
175
  response.to_a
167
176
  end
168
177
  end
@@ -11,15 +11,23 @@ module Meta
11
11
  end
12
12
 
13
13
  def filter(request)
14
- parameters.map do |name, options|
14
+ errors = {}
15
+ final_value = {}
16
+
17
+ parameters.each do |name, options|
15
18
  schema = options[:schema]
16
19
  value = if options[:in] == 'header'
17
- schema.filter(request.get_header('HTTP_' + name.to_s.upcase.gsub('-', '_')))
18
- else
19
- schema.filter(request.params[name.to_s])
20
- end
21
- [name, value]
22
- end.to_h
20
+ schema.filter(request.get_header('HTTP_' + name.to_s.upcase.gsub('-', '_')))
21
+ else
22
+ schema.filter(request.params[name.to_s])
23
+ end
24
+ final_value[name] = value
25
+ rescue JsonSchema::ValidationError => e
26
+ errors[name] = e.message
27
+ end
28
+ raise Errors::ParameterInvalid.new(errors) unless errors.empty?
29
+
30
+ final_value
23
31
  end
24
32
 
25
33
  def to_swagger_doc
@@ -20,16 +20,14 @@ module Meta
20
20
  def execute(execution, remaining_path)
21
21
  path_matching.merge_path_params(remaining_path, execution.request)
22
22
 
23
- begin
24
- execution.parse_parameters(@meta[:parameters]) if @meta[:parameters]
25
- execution.parse_request_body(@meta[:request_body]) if @meta[:request_body]
23
+ execution.parse_parameters(@meta[:parameters]) if @meta[:parameters]
24
+ execution.parse_request_body(@meta[:request_body]) if @meta[:request_body]
26
25
 
27
- action.execute(execution) if action
26
+ action.execute(execution) if action
28
27
 
29
- render_entity(execution) if @meta[:responses]
30
- rescue Execution::Abort
31
- execution
32
- end
28
+ render_entity(execution) if @meta[:responses]
29
+ rescue Execution::Abort
30
+ nil
33
31
  end
34
32
 
35
33
  def match?(execution, remaining_path)
data/lib/meta/errors.rb CHANGED
@@ -13,7 +13,7 @@ module Meta
13
13
  end
14
14
 
15
15
  class RenderingInvalid < JsonSchema::ValidationErrors
16
- def initialize(errors)
16
+ def initialize(errors = {})
17
17
  super(errors, "渲染实体异常:#{errors}")
18
18
  end
19
19
  end
@@ -34,7 +34,7 @@ module Meta
34
34
  private
35
35
 
36
36
  def apply_array_schema?(options, block)
37
- options[:type] == 'array' && (options[:items] || options[:ref] || options[:dynamic_ref] || block)
37
+ options[:type] == 'array'
38
38
  end
39
39
 
40
40
  def apply_object_schema?(options, block)
@@ -23,6 +23,8 @@ module Meta
23
23
 
24
24
  def initialize(options = {})
25
25
  options = OPTIONS_CHECKER.check(options)
26
+ raise '不允许 BaseSchema 直接接受 array 类型,必须通过继承使用 ArraySchema' if options[:type] == 'array' && self.class == BaseSchema
27
+
26
28
  @options = SchemaOptions.normalize(options)
27
29
  end
28
30
 
@@ -96,9 +96,9 @@ module Meta
96
96
  @object_converters = {
97
97
  [Object] => lambda do |value|
98
98
  if [TrueClass, FalseClass, Integer, Float, String].any? { |ruby_type| value.is_a?(ruby_type) }
99
- raise TypeConvertError, I18n.t(:'json_schema.errors.type_convert.object', value: value, real_type: I18n.t(:'json_schema.type_description.basic'))
99
+ raise TypeConvertError, I18n.t(:'json_schema.errors.type_convert.object', value: value, real_type: I18n.t(:'json_schema.type_names.basic'))
100
100
  elsif value.is_a?(Array)
101
- raise TypeConvertError, I18n.t(:'json_schema.errors.type_convert.object', value: value, real_type: I18n.t(:'json_schema.type_description.array'))
101
+ raise TypeConvertError, I18n.t(:'json_schema.errors.type_convert.object', value: value, real_type: I18n.t(:'json_schema.type_names.array'))
102
102
  end
103
103
 
104
104
  ObjectWrapper.new(value)
data/meta-api.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "meta-api"
3
- spec.version = "0.0.5"
3
+ spec.version = "0.0.6"
4
4
  spec.authors = ["yetrun"]
5
5
  spec.email = ["yetrun@foxmail.com"]
6
6
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: meta-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - yetrun
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-27 00:00:00.000000000 Z
11
+ date: 2023-05-26 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 一个 Web API 框架,该框架采用定义元信息的方式编写 API,并同步生成 API 文档
14
14
  email:
@@ -130,7 +130,7 @@ metadata:
130
130
  allowed_push_host: https://rubygems.org
131
131
  homepage_uri: https://github.com/yetrun/web-frame
132
132
  source_code_uri: https://github.com/yetrun/web-frame.git
133
- post_install_message:
133
+ post_install_message:
134
134
  rdoc_options: []
135
135
  require_paths:
136
136
  - lib
@@ -145,8 +145,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
145
145
  - !ruby/object:Gem::Version
146
146
  version: '0'
147
147
  requirements: []
148
- rubygems_version: 3.3.7
149
- signing_key:
148
+ rubygems_version: 3.3.26
149
+ signing_key:
150
150
  specification_version: 4
151
151
  summary: 一个 Web API 框架
152
152
  test_files: []