meta-api 0.0.4 → 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 +4 -4
- data/CHANGELOG.md +11 -0
- data/Gemfile.lock +2 -2
- data/README.md +32 -21
- data/config/locales/zh-CN.yml +11 -3
- data/docs//346/225/231/347/250/213.md +513 -171
- data/examples/rails_app/Gemfile.lock +1 -1
- data/lib/meta/application/execution.rb +27 -18
- data/lib/meta/application/metadata.rb +1 -1
- data/lib/meta/application/parameters.rb +17 -9
- data/lib/meta/application/route.rb +6 -8
- data/lib/meta/errors.rb +1 -1
- data/lib/meta/json_schema/builders/schema_builder_tool.rb +1 -1
- data/lib/meta/json_schema/schemas/base_schema.rb +2 -0
- data/lib/meta/json_schema/support/type_converter.rb +13 -11
- data/lib/meta/route_dsl/application_builder.rb +18 -10
- data/lib/meta/route_dsl/around_action_builder.rb +30 -3
- data/lib/meta/route_dsl/route_builder.rb +10 -4
- data/meta-api.gemspec +1 -1
- metadata +6 -6
@@ -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,341 +8,483 @@ Meta 框架天生就是将文档和实现统一起来的,并始终致力于此
|
|
8
8
|
|
9
9
|
在正式阅读本教程之前,有一些*准备工作*需要提前了解的。
|
10
10
|
|
11
|
-
###
|
11
|
+
### 只接受 JSON
|
12
12
|
|
13
|
-
|
13
|
+
**只接受格式为 `application/json` 的请求体参数,并且响应实体的格式一律为 `application/json`.**
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
这在当前的环境下并不算太大的限制,如果你是致力于新项目的开发的话。但是,如果你处理旧项目,并且要求格式为 `application/json` 之外的格式,如 `application/xml`,则框架目前是无能为力的。
|
18
|
-
|
19
|
-
这中限制只包括请求参数的实体,诸如路径里的参数、或 query 中的参数依然可用,不受该限制。
|
15
|
+
这在当前的环境下并不算太大的限制,如果你是致力于新项目的开发的话。但是,如果你处理旧项目,并且要求格式为 `application/json` 之外的格式,如 `application/xml`,则框架目前是不能自动处理的。
|
20
16
|
|
21
|
-
|
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
|
-
|
21
|
+
首先,你将学到定义路由的全部知识。换句话说,你该如何具体地*描述*一个接口。一般来说,我们需要描述接口的标题、详述、标签、参数和返回值。
|
28
22
|
|
29
|
-
|
23
|
+
然后,你将学到命名空间的概念。命名空间用来组织接口的层级结构,并且会用到诸如如路由嵌套、before/after 钩子、异常拦截等概念。
|
30
24
|
|
31
|
-
|
25
|
+
从命名空间引申出的模块的概念也很重要。模块本身也是一个命名空间,命名空间用到的功能都可以用在模块中。除此之外,模块还用来组织大型应用的结构。最后,模块本身也是一个 Rack 应用,可以直接放在服务器下运行。
|
32
26
|
|
33
|
-
|
27
|
+
接下来是重点,我们将深入参数和返回值的定义。虽然说前面已经提到参数和返回值的知识,但仅覆盖最简单同时也是最常用的场景。参数和返回值的知识实在是太大了,有必要专门划出一个章节来介绍它。这里提一下,参数和返回值在 Meta 框架里都统一为一个叫做实体的概念,因此你只需要学会定义一种就能够同时定义两者了。
|
34
28
|
|
35
|
-
|
29
|
+
最后,将是一个生成文档的方法。虽然它很简单,仅仅是一个方法,但它如此重要以至于我不得不专门划出一个章节来强调它的重要性。
|
36
30
|
|
37
|
-
|
31
|
+
文章的最后是特殊用法举例。说实话,我还没想好把它放哪,但它确实列举了几个比较常见的场景。
|
38
32
|
|
39
|
-
|
33
|
+
### 继承和宏定义
|
40
34
|
|
41
|
-
|
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
|
-
|
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
|
-
|
47
|
+
再比如继承 `Meta::Entity` 定义一个实体,实体内使用 `property` 宏定义属性:
|
55
48
|
|
56
|
-
|
49
|
+
```ruby
|
50
|
+
class UserEntity < Meta::Entity
|
51
|
+
property :name
|
52
|
+
property :age
|
53
|
+
end
|
54
|
+
```
|
57
55
|
|
58
|
-
|
56
|
+
## 路由定义(`route` 宏)
|
59
57
|
|
60
|
-
|
58
|
+
在 `Meta::Application` 类内,第一个能做的事情就是定义路由。`route` 方法(以后我们称这种特定的 DSL 方法为“宏”)定义一个具体的路由(即接口):
|
61
59
|
|
62
60
|
```ruby
|
63
61
|
class DemoApp < Meta::Application
|
64
|
-
|
65
|
-
#
|
62
|
+
route '/', :get do
|
63
|
+
# 块内定义路由的详细元素
|
66
64
|
end
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
### HTTP 路径和方法
|
67
69
|
|
68
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
119
|
+
在 `route` 宏内部,可使用两个宏: `meta` 宏定义路由的元信息,`action` 宏定义路由的执行逻辑。
|
106
120
|
|
107
|
-
|
108
|
-
class DemoApp < Meta::Application
|
109
|
-
namespace '/user' do
|
110
|
-
get do
|
111
|
-
title '获取用户详情'
|
112
|
-
end
|
121
|
+
首先,通过 `meta` 宏定义路由的“元”信息。注意,“元”信息的作用是双向的,既可以定义接口的文档,也可以约束接口的行为。例如,在 ` meta` 宏内定义参数:
|
113
122
|
|
114
|
-
|
115
|
-
|
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
|
-
|
139
|
+
#### `meta` 宏一览
|
126
140
|
|
127
|
-
|
128
|
-
class DemoApp < Meta::Application
|
129
|
-
namespace '/user' do
|
130
|
-
before do
|
131
|
-
@user = get_user
|
132
|
-
end
|
141
|
+
`meta` 宏内部现在只提供了以下五个方法:
|
133
142
|
|
134
|
-
|
135
|
-
|
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
|
-
|
139
|
-
title '更新用户详情'
|
152
|
+
status 200 do
|
153
|
+
# 内部定义返回值结构
|
140
154
|
end
|
141
155
|
end
|
142
156
|
end
|
143
157
|
```
|
144
158
|
|
145
|
-
|
159
|
+
以上,`title`、`description`、`tags` 宏分别定义接口的标题、描述信息和标签列表。`params` 和 `status` 宏定义接口的参数和返回值,其内部定义比较复杂,将在后面详细讲解。
|
146
160
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
before { puts 2 }
|
151
|
-
around { |next_action|
|
152
|
-
puts 3
|
153
|
-
next_action.execute(self)
|
154
|
-
puts 5
|
155
|
-
}
|
156
|
-
after { puts 6 }
|
157
|
-
after { puts 7 }
|
161
|
+
#### `meta` 宏展开
|
162
|
+
|
163
|
+
`meta` 宏可以展开定义,亦即可以直接在 `route` 定义内部直接使用 `meta` 宏定义的语法,它是 `route` 定义内部提供的一种快捷方式:
|
158
164
|
|
159
|
-
|
160
|
-
|
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
|
-
|
168
|
-
2. 然后执行 `around` 钩子的前半部分,按照定义的顺序执行。
|
169
|
-
3. 然后执行路由方法。
|
170
|
-
4. 然后执行 `around` 钩子的后半部分,按照定义的顺序执行。
|
171
|
-
5. 最后执行 `after` 钩子,按照定义的顺序执行。
|
181
|
+
### 定义路由的执行逻辑(`action` 宏)
|
172
182
|
|
173
|
-
|
183
|
+
`action` 宏定义业务代码部分。将上面的 `POST /users` 接口的逻辑实现定义完全,大概率是以下这个样子:
|
174
184
|
|
175
|
-
|
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` 宏定义嵌套路由
|
176
200
|
|
177
201
|
```ruby
|
178
202
|
class DemoApp < Meta::Application
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
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
|
183
218
|
end
|
184
219
|
end
|
185
220
|
end
|
186
221
|
```
|
187
222
|
|
188
|
-
|
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` 层级执行一遍。
|
189
237
|
|
190
|
-
|
238
|
+
正如名字所表达的那样,`before` 在 `action` 宏之前执行,`after` 在 `action` 宏之后执行。
|
191
239
|
|
192
240
|
```ruby
|
193
241
|
class DemoApp < Meta::Application
|
194
242
|
namespace '/foo' do
|
195
243
|
before do
|
196
|
-
|
244
|
+
puts 1
|
197
245
|
end
|
198
246
|
|
247
|
+
after do
|
248
|
+
puts 2
|
249
|
+
end
|
250
|
+
|
199
251
|
get do
|
200
252
|
action do
|
201
|
-
|
202
|
-
p @bar # nil
|
253
|
+
puts 3
|
203
254
|
end
|
204
255
|
end
|
205
256
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
end
|
210
|
-
|
211
|
-
get do
|
212
|
-
action do
|
213
|
-
p @foo # 'foo'
|
214
|
-
p @bar # 'bar'
|
215
|
-
end
|
257
|
+
put do
|
258
|
+
action do
|
259
|
+
puts 4
|
216
260
|
end
|
217
261
|
end
|
218
262
|
end
|
219
263
|
end
|
220
264
|
```
|
221
265
|
|
222
|
-
|
266
|
+
当用户访问 `GET /foo` 接口时,依次打印数字 `1`、`3`、`2`;当用户访问 `PUT /foo` 接口时,依次打印数字 `1`、`4`、`2`.
|
267
|
+
|
268
|
+
#### `around` 钩子(实验特性)
|
269
|
+
|
270
|
+
Meta 框架同时还支持 `around` 钩子, `around` 钩子会包裹 `action` 执行:
|
223
271
|
|
224
272
|
```ruby
|
225
273
|
class DemoApp < Meta::Application
|
226
274
|
namespace '/foo' do
|
227
|
-
|
228
|
-
|
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
|
229
285
|
end
|
230
286
|
|
231
|
-
|
232
|
-
|
233
|
-
|
287
|
+
put do
|
288
|
+
action do
|
289
|
+
puts 4
|
234
290
|
end
|
235
291
|
end
|
236
292
|
end
|
237
293
|
end
|
238
294
|
```
|
239
295
|
|
240
|
-
|
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` 钩子,则执行的顺序是:
|
241
303
|
|
242
|
-
|
304
|
+
1. `before` 钩子先执行,按照定义的顺序;
|
305
|
+
2. 接着执行 `action` 定义的块;
|
306
|
+
3. 最后执行 `after` 钩子,按照定义的顺序。
|
307
|
+
|
308
|
+
举例(以下按照 `1`、`2`、`3` 的数字顺序执行):
|
243
309
|
|
244
310
|
```ruby
|
245
|
-
class
|
246
|
-
|
247
|
-
|
248
|
-
|
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 }
|
249
317
|
|
250
|
-
|
251
|
-
|
252
|
-
# 它将捕获 '/foo/bar' 路由下的异常
|
318
|
+
get '/request' do
|
319
|
+
puts 3
|
253
320
|
end
|
254
321
|
end
|
255
322
|
end
|
256
323
|
```
|
257
324
|
|
258
|
-
|
325
|
+
如果还包含 `around` 钩子,则会复杂一些,但大体上是:
|
326
|
+
|
327
|
+
1. 最先执行的是 `before` 钩子以及 `around` 钩子的前半部分,按照定义的顺序;
|
328
|
+
2. 接着执行 `action` 定义的块;
|
329
|
+
3. 然后执行 `after` 钩子,按照定义的顺序;
|
330
|
+
4. 最后执行 `around` 钩子的后半部分,按照定义的**逆序**执行。
|
331
|
+
|
332
|
+
举例(以下按照 `1`、`2`、`3` 的数字顺序执行):
|
259
333
|
|
260
334
|
```ruby
|
261
335
|
class DemoApp < Meta::Application
|
262
336
|
namespace '/foo' do
|
263
|
-
|
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
|
264
355
|
end
|
265
356
|
end
|
266
357
|
```
|
267
358
|
|
268
|
-
|
359
|
+
#### 使用钩子的注意事项
|
360
|
+
|
361
|
+
请注意,钩子的执行顺序是严格按照以上顺序执行的,与你定义的顺序无关。请确保 `before` 和 `around` 钩子优先于 `after` 的顺序定义,因为它们的执行也是优先于 `after` 的。
|
362
|
+
|
363
|
+
另外,钩子的执行不会覆盖参数解析和返回值渲染,亦即 `before` 钩子在参数解析之后执行,`after` 钩子在返回值渲染之前执行,而 `around` 钩子亦不会覆盖参数解析和返回值渲染。
|
364
|
+
|
365
|
+
钩子不会中断执行。如果要在钩子中中断程序的执行,可使用 `abort_execution!` 方法:
|
269
366
|
|
270
367
|
```ruby
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
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!
|
278
375
|
end
|
279
376
|
```
|
280
377
|
|
281
|
-
|
378
|
+
`abort_execution!` 同时会跳过返回值渲染的执行。
|
282
379
|
|
283
|
-
|
380
|
+
冷知识:`Meta::Application` 本身也可视为一个命名空间定义,`namespace` 内能用到的方法也可以在 `Meta::Application` 内使用。
|
284
381
|
|
285
|
-
|
382
|
+
### 异常拦截
|
383
|
+
|
384
|
+
在 `namespace` 中可使用 `rescue_error` 拦截异常。
|
286
385
|
|
287
386
|
```ruby
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
# 定义参数
|
294
|
-
param :user do
|
295
|
-
param :name
|
296
|
-
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 = ["所访问的资源不存在"]
|
297
392
|
end
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
expose :age
|
393
|
+
|
394
|
+
get do
|
395
|
+
action do
|
396
|
+
user = User.find(params[:id])
|
397
|
+
end
|
304
398
|
end
|
305
399
|
end
|
306
|
-
action do
|
307
|
-
# 业务逻辑在这里实现,通过 params 方法访问参数,render 方法渲染实体
|
308
|
-
user = get_user
|
309
|
-
user.update!(params[:user])
|
310
|
-
render :user, user
|
311
|
-
end
|
312
400
|
end
|
313
401
|
```
|
314
402
|
|
315
|
-
|
403
|
+
以下是 Meta 框架抛出的异常:
|
404
|
+
|
405
|
+
- `Meta::Errors::NoMatchingRoute`:路由不匹配时。
|
406
|
+
- `Meta::Errors::ParameterInvalid`:参数存在异常时。
|
407
|
+
- `Meta::Errors::RenderingInvalid`:响应值存在异常时。
|
408
|
+
- `Meta::Errors::UnsupportedContentType`:框架只支持 `application/json` 的参数格式。当客户端的请求体不是这个格式时,会抛出这个错误。
|
316
409
|
|
317
|
-
|
410
|
+
#### 嵌套命名空间下的异常拦截
|
411
|
+
|
412
|
+
拦截异常先在子作用域下拦截;如果拦截失败则继续在父作用域下拦截。下面的例子中:
|
318
413
|
|
319
414
|
```ruby
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
tags ['User'] # 传递一个数组
|
325
|
-
params do
|
326
|
-
# 定义参数
|
415
|
+
class DemoApp < Meta::Application
|
416
|
+
namespace '/foo' do
|
417
|
+
rescue_error ErrorOne do
|
418
|
+
puts "rescued in /foo" #(1)
|
327
419
|
end
|
328
|
-
|
329
|
-
|
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
|
330
441
|
end
|
331
442
|
end
|
332
|
-
|
333
|
-
|
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
|
334
466
|
end
|
335
467
|
end
|
336
468
|
```
|
337
469
|
|
338
|
-
|
470
|
+
即使是上面的例子,调用 `GET /foo/bar` 请求时也只有顶层的异常拦截起了作用。
|
339
471
|
|
340
|
-
`namespace`
|
472
|
+
### `namespace` 的 `meta` 宏
|
473
|
+
|
474
|
+
同 `route` 宏内,`namespace` 宏内部可以定义 `meta` 宏。`namespace` 定义的 `meta` 宏定义下属路由的公共部分,其会应用到全部子路由,除非在 `route` 宏内复写。
|
341
475
|
|
342
476
|
```ruby
|
343
|
-
namespace '/
|
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 属性。
|
344
485
|
meta do
|
345
|
-
|
486
|
+
title '处理用户详情'
|
487
|
+
description '通过路径参数获取用户数据,并对用户数据做一定的处理,比如查看、更新'
|
346
488
|
tags ['User'] # 该 namespace 下的接口归到 User 标签下
|
347
489
|
params do # 定义共同参数
|
348
490
|
param :id
|
@@ -375,6 +517,206 @@ namespace '/user/:id' do
|
|
375
517
|
end
|
376
518
|
```
|
377
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
|
+
|
378
720
|
## 参数定义
|
379
721
|
|
380
722
|
本节介绍参数和返回值如何定义。因为 Meta 框架在底层不区分参数和返回值,它们都统一为“实体”的概念。因此,当涉及到语法细节时,在参数、返回值、实体内都是一致的。
|