meta-api 0.0.7 → 0.0.9

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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/lib/meta/application/execution.rb +19 -49
  4. data/lib/meta/application/metadata.rb +65 -5
  5. data/lib/meta/application/route.rb +4 -26
  6. data/lib/meta/config.rb +14 -14
  7. data/lib/meta/json_schema/builders/schema_builder_tool.rb +2 -1
  8. data/lib/meta/json_schema/schemas/array_schema.rb +13 -14
  9. data/lib/meta/json_schema/schemas/base_schema.rb +101 -36
  10. data/lib/meta/json_schema/schemas/object_schema.rb +22 -9
  11. data/lib/meta/json_schema/schemas/properties.rb +20 -74
  12. data/lib/meta/json_schema/schemas/scoping_schema.rb +41 -0
  13. data/lib/meta/json_schema/schemas/staging_schema.rb +58 -0
  14. data/lib/meta/json_schema/schemas/unsupported_schema.rb +22 -0
  15. data/lib/meta/json_schema/support/errors.rb +3 -0
  16. data/lib/meta/json_schema/support/schema_options.rb +0 -9
  17. data/lib/meta/route_dsl/meta_builder.rb +2 -2
  18. data/lib/meta/route_dsl/uniformed_params_builder.rb +5 -5
  19. data/lib/meta/swagger_doc.rb +1 -1
  20. data/lib/meta/utils/kwargs/builder.rb +2 -0
  21. data/lib/meta/utils/route_dsl_builders.rb +1 -0
  22. data/meta-api.gemspec +6 -5
  23. metadata +71 -111
  24. data/.autoenv.zsh +0 -1
  25. data/.gitignore +0 -7
  26. data/.rubocop.yml +0 -28
  27. data/Gemfile +0 -18
  28. data/Gemfile.lock +0 -66
  29. data/README.md +0 -166
  30. data/Rakefile +0 -3
  31. data/docs/Rails.md +0 -61
  32. data/docs//345/220/215/347/247/260/347/224/261/346/235/245.md +0 -7
  33. data/docs//345/246/202/344/275/225/350/264/241/347/214/256.md +0 -10
  34. data/docs//346/225/231/347/250/213.md +0 -1647
  35. data/docs//347/264/242/345/274/225.md +0 -183
  36. data/examples/lobster.rb +0 -71
  37. data/examples/rack_app/README.md +0 -3
  38. data/examples/rack_app/config.ru +0 -6
  39. data/examples/rack_app/hello.rb +0 -6
  40. data/examples/rack_app/timing.rb +0 -15
  41. data/examples/rails_app/.gitattributes +0 -5
  42. data/examples/rails_app/.gitignore +0 -23
  43. data/examples/rails_app/.rspec +0 -1
  44. data/examples/rails_app/.ruby-version +0 -1
  45. data/examples/rails_app/Gemfile +0 -29
  46. data/examples/rails_app/Gemfile.lock +0 -190
  47. data/examples/rails_app/README.md +0 -11
  48. data/examples/rails_app/Rakefile +0 -6
  49. data/examples/rails_app/app/controllers/application_controller.rb +0 -7
  50. data/examples/rails_app/app/controllers/concerns/.keep +0 -0
  51. data/examples/rails_app/app/controllers/data_controller.rb +0 -63
  52. data/examples/rails_app/app/controllers/swagger_controller.rb +0 -13
  53. data/examples/rails_app/app/models/concerns/.keep +0 -0
  54. data/examples/rails_app/bin/rails +0 -4
  55. data/examples/rails_app/bin/rake +0 -4
  56. data/examples/rails_app/bin/setup +0 -25
  57. data/examples/rails_app/config/application.rb +0 -39
  58. data/examples/rails_app/config/boot.rb +0 -3
  59. data/examples/rails_app/config/credentials.yml.enc +0 -1
  60. data/examples/rails_app/config/environment.rb +0 -5
  61. data/examples/rails_app/config/environments/development.rb +0 -51
  62. data/examples/rails_app/config/environments/production.rb +0 -65
  63. data/examples/rails_app/config/environments/test.rb +0 -50
  64. data/examples/rails_app/config/initializers/cors.rb +0 -16
  65. data/examples/rails_app/config/initializers/filter_parameter_logging.rb +0 -8
  66. data/examples/rails_app/config/initializers/inflections.rb +0 -16
  67. data/examples/rails_app/config/initializers/meta_rails_plugin.rb +0 -3
  68. data/examples/rails_app/config/locales/en.yml +0 -33
  69. data/examples/rails_app/config/puma.rb +0 -43
  70. data/examples/rails_app/config/routes.rb +0 -13
  71. data/examples/rails_app/config.ru +0 -6
  72. data/examples/rails_app/lib/tasks/.keep +0 -0
  73. data/examples/rails_app/log/.keep +0 -0
  74. data/examples/rails_app/public/robots.txt +0 -1
  75. data/examples/rails_app/spec/data_controller_spec.rb +0 -60
  76. data/examples/rails_app/spec/rails_helper.rb +0 -55
  77. data/examples/rails_app/spec/spec_helper.rb +0 -94
  78. data/examples/rails_app/spec/swagger_controller_spec.rb +0 -13
  79. data/examples/rails_app/tmp/.keep +0 -0
  80. data/examples/rails_app/tmp/pids/.keep +0 -0
@@ -1,1647 +0,0 @@
1
- # 教程
2
-
3
- 现有的 Web API 框架并不关注文档的问题,文档往往是作为插件挂载到框架上的。但是,文档和业务实现并不需要割裂开,它们在很大程度上应该是耦合在一起的。比方说,某个接口我定义了参数如此,就该自动生成一致的文档向前端告知;同样,当我提供了文档是如此后,我的接口实现就该自动地约束为这样实现。
4
-
5
- Meta 框架天生就是将文档和实现统一起来的,并始终致力于此(如果真的有什么不一致或者不到位的地方,那只能说框架实现上尚有欠缺,并不能从思想上说本该如此)。Meta 框架与 Swagger 合作,致力于产生符合 Restful 和社区规范的文档格式。它提供了几乎完整的描述接口信息的宏命令,并且在描述接口的同时就能基本实现接口的一些约束,其中最重要的莫过于对参数和返回值的声明。
6
-
7
- ## 准备工作
8
-
9
- 在正式阅读本教程之前,有一些*准备工作*需要提前了解的。
10
-
11
- ### 只接受 JSON
12
-
13
- **只接受格式为 `application/json` 的请求体参数,并且响应实体的格式一律为 `application/json`.**
14
-
15
- 这在当前的环境下并不算太大的限制,如果你是致力于新项目的开发的话。但是,如果你处理旧项目,并且要求格式为 `application/json` 之外的格式,如 `application/xml`,则框架目前是不能自动处理的。
16
-
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 请求和响应操作。
18
-
19
- ### 教程脉络
20
-
21
- 首先,你将学到定义路由的全部知识。换句话说,你该如何具体地*描述*一个接口。一般来说,我们需要描述接口的标题、详述、标签、参数和返回值。
22
-
23
- 然后,你将学到命名空间的概念。命名空间用来组织接口的层级结构,并且会用到诸如如路由嵌套、before/after 钩子、异常拦截等概念。
24
-
25
- 从命名空间引申出的模块的概念也很重要。模块本身也是一个命名空间,命名空间用到的功能都可以用在模块中。除此之外,模块还用来组织大型应用的结构。最后,模块本身也是一个 Rack 应用,可以直接放在服务器下运行。
26
-
27
- 接下来是重点,我们将深入参数和返回值的定义。虽然说前面已经提到参数和返回值的知识,但仅覆盖最简单同时也是最常用的场景。参数和返回值的知识实在是太大了,有必要专门划出一个章节来介绍它。这里提一下,参数和返回值在 Meta 框架里都统一为一个叫做实体的概念,因此你只需要学会定义一种就能够同时定义两者了。
28
-
29
- 最后,将是一个生成文档的方法。虽然它很简单,仅仅是一个方法,但它如此重要以至于我不得不专门划出一个章节来强调它的重要性。
30
-
31
- 文章的最后是特殊用法举例。说实话,我还没想好把它放哪,但它确实列举了几个比较常见的场景。
32
-
33
- ### 继承和宏定义
34
-
35
- 在使用 Meta 框架提供的组件时,我们往往先要继承一个类,然后直接在类定义中使用宏命令。所谓的宏命令其实就是一个 Ruby 方法,只不过在 DSL 术语中我们将它称为“宏”。
36
-
37
- 例如,定义一个 API,我们继承的是 `Meta::Application` 类,然后在类中使用 `route` 宏定义路由:
38
-
39
- ```ruby
40
- class DemoApp < Meta::Application
41
- route '/foo', :get do
42
- # ... 具体的宏定义
43
- end
44
- end
45
- ```
46
-
47
- 再比如继承 `Meta::Entity` 定义一个实体,实体内使用 `property` 宏定义属性:
48
-
49
- ```ruby
50
- class UserEntity < Meta::Entity
51
- property :name
52
- property :age
53
- end
54
- ```
55
-
56
- ## 路由定义(`route` 宏)
57
-
58
- 在 `Meta::Application` 类内,第一个能做的事情就是定义路由。`route` 方法(以后我们称这种特定的 DSL 方法为“宏”)定义一个具体的路由(即接口):
59
-
60
- ```ruby
61
- class DemoApp < Meta::Application
62
- route '/', :get do
63
- # 块内定义路由的详细元素
64
- end
65
- end
66
- ```
67
-
68
- ### HTTP 路径和方法
69
-
70
- `route` 方法接受一个路径字符串和一个 HTTP 方法,并且可接受一个块用于定义路由的详细元素(将在后面讲到)。HTTP 方法我们一共支持五种,包括 `get`、`post`、`put`、`patch`、`delete`. 为此,我们提供了五个便捷方法用于简化 `route` 方法调用的书写,举例:
71
-
72
- ```ruby
73
- class DemoApp < Meta::Application
74
- get do # 当路径为 `/` 时,路径参数可以省略
75
- # ...
76
- end
77
-
78
- post '/foo' do
79
- # ...
80
- end
81
-
82
- put '/foo/bar' do
83
- # ...
84
- end
85
-
86
- patch '/foo/bar' do
87
- # ...
88
- end
89
-
90
- delete '/foo/bar' do
91
- # ...
92
- end
93
- end
94
- ```
95
-
96
- > 因为这种写法更为清晰并且视觉效果更好,教程的以后都用 `get`、`post`、`put`、`patch`、`delete` 五个方法代替 `route` 方法的调用。除非是只用到路径而不关心 HTTP 方法的情形。
97
-
98
- ### 通配符路径
99
-
100
- 当定义路由 `route /foo/bar` 时,它匹配的是完整的路径 `/foo/bar`. 当你需要匹配一堆路径时,需要为路由加上通配符符号。`:` 和 `*` 是通配符符号的两种,前者匹配一个部分,后者尽可能多地匹配剩余的部份。这么说如果没说清楚,我举两个例子即可明白:
101
-
102
- - `/foo/:id`:它将匹配诸如 `/foo/1`、`/foo/bar` 等路径,但不能匹配 `/foo/a/b/c` 这样的路径。
103
- - `/foo/*path`:它可以匹配 `/foo`、`/foo/bar`、`/foo/a/b/c` 等格式的路径。
104
-
105
- > 通配符符号后面的单词(`id` 和 `path`)是参数名称,它将路由中与其匹配的部分放到参数中可访问。这里先提一下,通过 `request.params['id']`、`request.params['path']` 可以访问到路由当中匹配的部分。
106
-
107
- 如果你不需要后续访问到参数,可以忽略命名:
108
-
109
- - `/foo/:`
110
- - `/foo/*`
111
-
112
- 再举两个路由参数的示例:
113
-
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` 等格式的路径。
116
-
117
- ### 定义路由的元信息(`meta` 宏)
118
-
119
- 在 `route` 宏内部,可使用两个宏: `meta` 宏定义路由的元信息,`action` 宏定义路由的执行逻辑。
120
-
121
- 首先,通过 `meta` 宏定义路由的“元”信息。注意,“元”信息的作用是双向的,既可以定义接口的文档,也可以约束接口的行为。例如,在 ` meta` 宏内定义参数:
122
-
123
- ```ruby
124
- post '/users' do
125
- meta do
126
- params do
127
- param :name, type: 'string', description: '姓名'
128
- param :age, type: 'integer', description: '年龄'
129
- end
130
- end
131
- end
132
- ```
133
-
134
- 它会产生两个方面的效果:
135
-
136
- 1. 文档方面:接口文档的参数部分会暴露出两个参数:`name`、`age`,并声明它的类型和描述信息。
137
- 2. 业务逻辑方面:业务代码执行时,通过标准的方法获取参数时会对参数作校验。这里面它只会提取出参数的两个字段(`name` 和 `age`),并对它们俩的类型作校验。如果参数不符合定义,会向客户端抛出一个错误。
138
-
139
- #### `meta` 宏一览
140
-
141
- `meta` 宏内部现在只提供了以下五个方法:
142
-
143
- ```ruby
144
- post '/users' do
145
- meta do
146
- title '创建用户'
147
- description '接口的详细描述'
148
- tags ['User'] # 定义接口的 Tag,传递一个数组
149
- params do
150
- # 内部定义参数结构
151
- end
152
- status 200 do
153
- # 内部定义返回值结构
154
- end
155
- end
156
- end
157
- ```
158
-
159
- 以上,`title`、`description`、`tags` 宏分别定义接口的标题、描述信息和标签列表。`params` 和 `status` 宏定义接口的参数和返回值,其内部定义比较复杂,将在后面详细讲解。
160
-
161
- #### `meta` 宏展开
162
-
163
- `meta` 宏可以展开定义,亦即可以直接在 `route` 定义内部直接使用 `meta` 宏定义的语法,它是 `route` 定义内部提供的一种快捷方式:
164
-
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
- # 内部定义返回值结构
175
- end
176
- end
177
- ```
178
-
179
- > 由于展开定义的方式写起来更加便捷,因此后面的教程示例都将采取这样的写法。
180
-
181
- ### 定义路由的执行逻辑(`action` 宏)
182
-
183
- `action` 宏定义业务代码部分。将上面的 `POST /users` 接口的逻辑实现定义完全,大概率是以下这个样子:
184
-
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` 宏定义嵌套路由
200
-
201
- ```ruby
202
- class DemoApp < Meta::Application
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
218
- end
219
- end
220
- end
221
- ```
222
-
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` 层级执行一遍。
237
-
238
- 正如名字所表达的那样,`before` 在 `action` 宏之前执行,`after` 在 `action` 宏之后执行。
239
-
240
- ```ruby
241
- class DemoApp < Meta::Application
242
- namespace '/foo' do
243
- before do
244
- puts 1
245
- end
246
-
247
- after do
248
- puts 2
249
- end
250
-
251
- get do
252
- action do
253
- puts 3
254
- end
255
- end
256
-
257
- put do
258
- action do
259
- puts 4
260
- end
261
- end
262
- end
263
- end
264
- ```
265
-
266
- 当用户访问 `GET /foo` 接口时,依次打印数字 `1`、`3`、`2`;当用户访问 `PUT /foo` 接口时,依次打印数字 `1`、`4`、`2`.
267
-
268
- #### `around` 钩子(实验特性)
269
-
270
- Meta 框架同时还支持 `around` 钩子, `around` 钩子会包裹 `action` 执行:
271
-
272
- ```ruby
273
- class DemoApp < Meta::Application
274
- namespace '/foo' do
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
285
- end
286
-
287
- put do
288
- action do
289
- puts 4
290
- end
291
- end
292
- end
293
- end
294
- ```
295
-
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` 钩子,则执行的顺序是:
303
-
304
- 1. `before` 钩子先执行,按照定义的顺序;
305
- 2. 接着执行 `action` 定义的块;
306
- 3. 最后执行 `after` 钩子,按照定义的顺序。
307
-
308
- 举例(以下按照 `1`、`2`、`3` 的数字顺序执行):
309
-
310
- ```ruby
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 }
317
-
318
- get '/request' do
319
- puts 3
320
- end
321
- end
322
- end
323
- ```
324
-
325
- 如果还包含 `around` 钩子,则会复杂一些,但大体上是:
326
-
327
- 1. 最先执行的是 `before` 钩子以及 `around` 钩子的前半部分,按照定义的顺序;
328
- 2. 接着执行 `action` 定义的块;
329
- 3. 然后执行 `after` 钩子,按照定义的顺序;
330
- 4. 最后执行 `around` 钩子的后半部分,按照定义的**逆序**执行。
331
-
332
- 举例(以下按照 `1`、`2`、`3` 的数字顺序执行):
333
-
334
- ```ruby
335
- class DemoApp < Meta::Application
336
- namespace '/foo' do
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
355
- end
356
- end
357
- ```
358
-
359
- #### 使用钩子的注意事项
360
-
361
- 请注意,钩子的执行顺序是严格按照以上顺序执行的,与你定义的顺序无关。请确保 `before` 和 `around` 钩子优先于 `after` 的顺序定义,因为它们的执行也是优先于 `after` 的。
362
-
363
- 另外,钩子的执行不会覆盖参数解析和返回值渲染,亦即 `before` 钩子在参数解析之后执行,`after` 钩子在返回值渲染之前执行,而 `around` 钩子亦不会覆盖参数解析和返回值渲染。
364
-
365
- 钩子不会中断执行。如果要在钩子中中断程序的执行,可使用 `abort_execution!` 方法:
366
-
367
- ```ruby
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!
375
- end
376
- ```
377
-
378
- `abort_execution!` 同时会跳过返回值渲染的执行。
379
-
380
- 冷知识:`Meta::Application` 本身也可视为一个命名空间定义,`namespace` 内能用到的方法也可以在 `Meta::Application` 内使用。
381
-
382
- ### 异常拦截
383
-
384
- 在 `namespace` 中可使用 `rescue_error` 拦截异常。
385
-
386
- ```ruby
387
- class DemoApp < Meta::Application
388
- namespace '/users/:id' do
389
- rescue_error ActiveRecord::RecordNotFound do |e|
390
- response.status = 404
391
- response.body = ["所访问的资源不存在"]
392
- end
393
-
394
- get do
395
- action do
396
- user = User.find(params[:id])
397
- end
398
- end
399
- end
400
- end
401
- ```
402
-
403
- 以下是 Meta 框架抛出的异常:
404
-
405
- - `Meta::Errors::NoMatchingRoute`:路由不匹配时。
406
- - `Meta::Errors::ParameterInvalid`:参数存在异常时。
407
- - `Meta::Errors::RenderingInvalid`:响应值存在异常时。
408
- - `Meta::Errors::UnsupportedContentType`:框架只支持 `application/json` 的参数格式。当客户端的请求体不是这个格式时,会抛出这个错误。
409
-
410
- #### 嵌套命名空间下的异常拦截
411
-
412
- 拦截异常先在子作用域下拦截;如果拦截失败则继续在父作用域下拦截。下面的例子中:
413
-
414
- ```ruby
415
- class DemoApp < Meta::Application
416
- namespace '/foo' do
417
- rescue_error ErrorOne do
418
- puts "rescued in /foo" #(1)
419
- end
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
441
- end
442
- end
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
466
- end
467
- end
468
- ```
469
-
470
- 即使是上面的例子,调用 `GET /foo/bar` 请求时也只有顶层的异常拦截起了作用。
471
-
472
- ### `namespace` 的 `meta` 宏
473
-
474
- 同 `route` 宏内,`namespace` 宏内部可以定义 `meta` 宏。`namespace` 定义的 `meta` 宏定义下属路由的公共部分,其会应用到全部子路由,除非在 `route` 宏内复写。
475
-
476
- ```ruby
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 属性。
485
- meta do
486
- title '处理用户详情'
487
- description '通过路径参数获取用户数据,并对用户数据做一定的处理,比如查看、更新'
488
- tags ['User'] # 该 namespace 下的接口归到 User 标签下
489
- params do # 定义共同参数
490
- param :id
491
- end
492
- status 200 do # 定义共同返回值
493
- expose :user, type: 'object'
494
- end
495
- end
496
-
497
- get do
498
- title '返回用户详情'
499
- action do
500
- user = User.find(params[:id]) # params 包括 id 字段
501
- render :user, user # render 包括 user 字段
502
- end
503
- end
504
-
505
- put do
506
- title '更新用户'
507
- params do
508
- # 补充参数 user
509
- param :user, type: 'object'
510
- end
511
- action do
512
- user = User.find(params[:id])
513
- user.update!(params[:user]) # params 包括 id、user 字段
514
- render :user, user # render 包括 user 字段
515
- end
516
- end
517
- end
518
- ```
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
-
720
- ## 参数定义
721
-
722
- 本节介绍参数和返回值如何定义。因为 Meta 框架在底层不区分参数和返回值,它们都统一为“实体”的概念。因此,当涉及到语法细节时,在参数、返回值、实体内都是一致的。
723
-
724
- 可以说,有关实体的定义,是 Meta 框架中细节最多的地方。在撰写这一章节的时候,我尝试写过很多遍,都无法很好地将方方面面说明清楚。我在行文时,一方面希望大家在入门的时候方便,能够很快地定义常用的用法;另一方面,也希望将所涉及的细节都能够阐述清楚,希望大家能够全面了解到 Meta 框架实体定义的方方面。现在,我只能尽可能地做到这两点,却不再强求。我将以场景的形式阐述用法,而不是孤立地介绍每个知识点。
725
-
726
- ### 初探:参数定义和过滤
727
-
728
- 在路由中,我们用 `params` 命令定义参数:
729
-
730
- ```ruby
731
- post '/users' do
732
- params do
733
- param :name
734
- param :age
735
- end
736
- end
737
- ```
738
-
739
- 然后,我们可以在 `action` 命令中使用 `params` 方法访问参数。如果我们发送了一个这样的请求:
740
-
741
- ```bash
742
- POST '/users' -d '{"name": "Jim", "age": 18}'
743
- ```
744
-
745
- 我们将在 `action` 命令中获取到这样的参数结构:
746
-
747
- ```ruby
748
- post '/users' do
749
- action do
750
- p params # => { name: "Jim", age: 18 }
751
- end
752
- end
753
- ```
754
-
755
- 参数解析时有自动过滤的作用,如果我们发送了一个这样的请求:
756
-
757
- ```bash
758
- POST '/users' -d '{"name": "Jim", "foo": "foo"}'
759
- ```
760
-
761
- 我们将在 `action` 命令中获取到这样的参数结构:
762
-
763
- ```ruby
764
- post '/users' do
765
- action do
766
- p params # => { name: "Jim", age: nil }
767
- end
768
- end
769
- ```
770
-
771
- 可以看到,它的做法是过滤了未定义的 `foo` 字段,并且将没有提供的 `age` 字段设为 `nil`.
772
-
773
- ### 嵌套:参数的层次结构
774
-
775
- 一般我在实际项目中不会这么定义,而是更习惯将它们套在一个根字段下,这样做有利于结构的划分和后期的扩展。
776
-
777
- ```ruby
778
- post '/users' do
779
- params do
780
- param :user do
781
- param :name
782
- param :age
783
- end
784
- end
785
- end
786
- ```
787
-
788
- 以上定义需要接收以下格式的参数:
789
-
790
- ```bash
791
- POST '/users' -d '{
792
- "user": {
793
- "name": "Jim",
794
- "age": 18
795
- }
796
- }'
797
- ```
798
-
799
- 在 `action` 命令中获取到的也是这样的嵌套结构:
800
-
801
- ```ruby
802
- post '/users' do
803
- action do
804
- p params # => { user: { name: "Jim", age: 18 } }
805
- end
806
- end
807
- ```
808
-
809
- 参数过滤也会发挥作用。如果请求参数传递的是这样的格式:
810
-
811
- ```ruby
812
- # HTTP params
813
- { "user": { "name": "Jim", "foo": "foo" } }
814
- ```
815
-
816
- 程序中获取的参数格式将是这样:
817
-
818
- ```ruby
819
- # p params
820
- { user: { name: "Jim", age: nil } }
821
- ```
822
-
823
- 如果顶层的 `user` 字段未提供,则整个 `user` 字段将设为 `nil`.
824
-
825
- > **提醒:**后续我们会在代码第一行添加 `# HTTP params` 注释表明这是 HTTP 请求的参数格式,`# p params` 注释表明这是程序中获取到的参数格式。
826
-
827
- ### 类型:参数的约束之一
828
-
829
- 参数提供很多约束选项,类型只是其中之一。
830
-
831
- #### 基本类型定义
832
-
833
- 将以上的参数定义添加上类型定义,应当是:
834
-
835
- ```ruby
836
- params do
837
- param :name, type: 'string'
838
- param :age, type: 'integer'
839
- end
840
- ```
841
-
842
- 类型定义首要的作用是报错,例如以下的参数格式会向客户端抛出 `400 Bad Request`:
843
-
844
- ```ruby
845
- # HTTP params
846
- { "name": "Jim", "age": "eighteen" }
847
- ```
848
-
849
- 参数在进行类型校验时是宽容的。如果值能够成功转化为定义的类型,则参数校验不会报错。以下参数不会报错:
850
-
851
- ```ruby
852
- # HTTP params
853
- { "name": "Jim", "age": "18" }
854
- ```
855
-
856
- 只不过参数过滤会规范化最终参数的格式,因此以上参数在程序中会获取为:
857
-
858
- ```ruby
859
- # p params
860
- { name: "Jim", age: 18 }
861
- ```
862
-
863
- `age` 字段获取到的是数字类型而非字符串类型。
864
-
865
- #### 嵌套类型定义
866
-
867
- 对于嵌套参数,你可以定义为 `object` 类型。但这是没必要的,因为嵌套参数必然应当是 `object` 类型。以下两个参数定义等价:
868
-
869
- ```ruby
870
- params do
871
- param :user, type: 'object' do
872
- param :name
873
- param :age
874
- end
875
- end
876
-
877
- params do
878
- param :user do
879
- param :name
880
- param :age
881
- end
882
- end
883
- ```
884
-
885
- 当定义 `object` 类型时,也可以不提供内部结构,此时将不再进行内部结构的过滤:
886
-
887
- ```ruby
888
- params do
889
- param :user, type: 'object'
890
- end
891
-
892
- # 接受的 HTTP 请求参数
893
- { "user": { "name": "Jim", "age": 18 }}
894
- { "user": { "foo": "foo" } }
895
- { "user": {} }
896
- { "user": nil }
897
-
898
- # 不接受的 HTTP 请求参数
899
- { "user": "foo" }
900
- { "user": 18 }
901
- { "user": [] }
902
- ```
903
-
904
- 记住,不校验和 `object` 类型不是一回事。不提供任何类型时,此时参数接受一切值:
905
-
906
- ```ruby
907
- params do
908
- param :user
909
- end
910
-
911
- # 以下格式都接受
912
- { "user": { "name": "Jim", "age": 18 }}
913
- { "user": { "foo": "foo" } }
914
- { "user": {} }
915
- { "user": nil }
916
- { "user": "foo" }
917
- { "user": 18 }
918
- { "user": [] }
919
- ```
920
-
921
- #### 数组类型定义
922
-
923
- 你可以定义为数组类型,此时参数必须接受为对象数组的格式。
924
-
925
- ```ruby
926
- params do
927
- param :users, type: 'array' do
928
- param :name
929
- param :age
930
- end
931
- end
932
-
933
- # 接受的参数格式
934
- # HTTP params
935
- {
936
- "users": [
937
- { "name": "Jim", age: 18 },
938
- { "name": "Jack", age: 19}
939
- ]
940
- }
941
- ```
942
-
943
- 有时候我们遇到数组内部不是对象的情况,这时候就不能使用嵌套定义:
944
-
945
- ```ruby
946
- params do
947
- param :tags, type: 'array'
948
- end
949
- ```
950
-
951
- 这个时候参数必须接收数组格式,但内部元素不会做校验。如果希望内部元素也要校验,用 `items` 选项定义内部结构:
952
-
953
- ```ruby
954
- params do
955
- param :tags, type: 'array', items: { type: 'string' }
956
- end
957
- ```
958
-
959
- #### 完整的类型列表
960
-
961
- 你能用到的 `type` 取值只能是以下之一:
962
-
963
- - `"boolean"`
964
- - `"integer"`
965
- - `"number"`
966
- - `"string"`
967
- - `"object"`
968
- - `"array"`
969
-
970
- ### `required`:参数的约束之二
971
-
972
- 正如其名,`required` 作“必须”校验。先前说过,未传递的字段会被赋予 `nil` . 然而,若字段被配置为 `required`,则参数校验会报错。以下参数定义中,`age` 字段被配置为 `required`:
973
-
974
- ```ruby
975
- # 参数定义
976
- params do
977
- param :name
978
- param :age, required: true
979
- end
980
- ```
981
-
982
- 以下请求皆会报错:
983
-
984
- ```ruby
985
- # HTTP params
986
- POST '/users' -d '{"name": "Jim"}'
987
-
988
- # HTTP params
989
- POST '/users' -d '{"name": "Jim", age: null}'
990
- ```
991
-
992
- > **小提示:**先前说过,将参数套在一个根字段下是一个好的设计习惯。同时,将这个根字段配置为 `required` 也是一个好习惯。
993
- >
994
- > ```ruby
995
- > # 参数定义
996
- > params do
997
- > param :user, required: true do
998
- > param :name
999
- > param :age
1000
- > end
1001
- > end
1002
- > ```
1003
- >
1004
- > 这会杜绝传递诸如 `{}`、`{ "user": null }` 这样的 JSON 格式。
1005
-
1006
- ### 其他参数约束
1007
-
1008
- 框架自带若干参数验证配置,在本节列举。
1009
-
1010
- #### `required`
1011
-
1012
- `required` 可同时配置 `allow_empty: true` 或 `allow_empty: false` 用以是否接受空字符串或空数组:
1013
-
1014
- ```ruby
1015
- params do
1016
- param :title, require: { allow_empty: false } # 不接受空字符串
1017
- param :tags, require: { allow_empty: true } # 可接受空数组
1018
- end
1019
- ```
1020
-
1021
- #### `format`
1022
-
1023
- 为字符串参数配置 `format` 可限制参数的格式。以下是用到的 `format` 示例:
1024
-
1025
- ```ruby
1026
- # 参数定义
1027
- params do
1028
- param :date, format: /^\d{4}-\d{2}-\d{2}$/
1029
- param :mobile, format: /^1[3456789]\d{9}$/
1030
- param :email, format: /^$/
1031
- end
1032
- ```
1033
-
1034
- #### `allowable`
1035
-
1036
- 通过一个数组配置一个字段可接受的值:
1037
-
1038
- ```ruby
1039
- # 参数定义
1040
- params do
1041
- param :p_state, description: '进程状态', allowable: ["idle", "pending", "running", "exited"]
1042
- param :gender, description: '性别', allowable: ["male", "female"]
1043
- endparams do
1044
- param :state, description: '进程状态', allowable: ["idle", "pending", "running", "exited"]
1045
- param :sex, description: '性别', allowable: ["male", "female"]
1046
- end
1047
- ```
1048
-
1049
- > **小提示:**一直忘记说了,我们可以通过 `description` 选项配置字段的描述,这个描述会在生成文档时生效。
1050
-
1051
- #### `validate`:自定义校验
1052
-
1053
- 如果以上的校验均不够用,Meta 支持自定义编写校验。`validate` 接受一个块,当校验失败时需要主动地抛出 `Meta::JsonSchema::ValidationError`:
1054
-
1055
- ```ruby
1056
- params do
1057
- raise Meta::JsonSchema::ValidationError, '手机号格式不正确' unless value =~ /^1[3456789]\d{9}$/
1058
- end
1059
- ```
1060
-
1061
- ### 设置参数的默认值
1062
-
1063
- `default` 选项可设置参数的默认值,当参数未提供或为 `nil` 时,默认值就会起作用:
1064
-
1065
- ```ruby
1066
- params do
1067
- param :age, default: 18 # 通过值设定
1068
- param :name, default: -> { 'Jim' } # 通过块设定
1069
- end
1070
- ```
1071
-
1072
- ### 参数在文档中的位置
1073
-
1074
- 默认情况下参数是放在 Request Body 下的(作为 `application/json` 的格式的一部分),但参数还可能存在于 path 或 query hash 中。使用 `in` 选项可以定义之,它对框架的执行没有影响,只对文档的生成产生效果。
1075
-
1076
- ```ruby
1077
- post '/:in_path' do
1078
- params do
1079
- param :in_path, in: 'path'
1080
- param :in_query, in: 'query'
1081
- param :in_body, in: 'body'
1082
- end
1083
- end
1084
- ```
1085
-
1086
- ## 返回值定义
1087
-
1088
- `status` 宏命令用来定义返回值。你需要传递一个(或多个)状态码,并用一个同样结构的块作为实体的定义。
1089
-
1090
- ```ruby
1091
- # 定义简单的返回实体
1092
- status 200 do
1093
- expose :name, type: 'string'
1094
- expose :age, type: 'integer'
1095
- end
1096
-
1097
- # 同样支持嵌套
1098
- status 200 do
1099
- expose :user do
1100
- expose :name, type: 'string'
1101
- expose :age, type: 'integer'
1102
- end
1103
- end
1104
-
1105
- # 同样支持校验,虽然我觉得校验返回值有点多此一举
1106
- status 200 do
1107
- expose :user, required: true do
1108
- expose :name, type: 'string'
1109
- expose :age, type: 'integer', required: true
1110
- end
1111
- end
1112
- ```
1113
-
1114
- ## 实体定义
1115
-
1116
- ### 统一参数和返回值
1117
-
1118
- 参数和返回值的定义并不需要割裂开,它们在很多行为上是统一的。现在,我们分别单独定义了参数和返回值:
1119
-
1120
- ```ruby
1121
- params do
1122
- param :user do
1123
- param :name, type: 'string'
1124
- param :age, type: 'integer'
1125
- end
1126
- end
1127
-
1128
- status 200 do
1129
- expose :user do
1130
- expose :name, type: 'string'
1131
- expose :age, type: 'integer'
1132
- end
1133
- end
1134
- ```
1135
-
1136
- 为将上述定义改造,内部的块可以封装在一个实体内定义:
1137
-
1138
- ```ruby
1139
- class UserEntity < Meta::Entity
1140
- property :name, type: 'string'
1141
- property :age, type: 'integer'
1142
- end
1143
- ```
1144
-
1145
- 然后在 `params` 和 `status` 内部使用 `ref` 引用这个实体:
1146
-
1147
- ```ruby
1148
- params do
1149
- param :user, ref: UserEntity
1150
- end
1151
-
1152
- status 200 do
1153
- expose :user, ref: UserEntity
1154
- end
1155
- ```
1156
-
1157
- 我们通过继承 `Meta::Entity` 类定义了实体并到处引用,从而简化了代码。这在实践中是推荐的方案。鉴于参数、返回值、实体的定义语法是完全一致的,故而接下来我们将重点进入实体的讲解环节。希望读者清楚的是,以上参数介绍的语法在实体定义中是完全可用的;并且,接下来有关实体的语法也能完全运用到单独的参数和返回值定义块中。
1158
-
1159
- > **小提示:**我们在 `params` 中用 `param` 命令定义参数字段,在 `status` 中用 `expose` 命令定义返回值字段,而在实体定义中这个命令变成了 `property`. 这里需要阐明的是,用 `param`、`expose` 还是 `property` 只是习惯的不同而已,它们的行为都是一致的并且能够混用。例如,你完全可以在 `params` 和 `status` 中一律使用 `property` 命令:
1160
- >
1161
- > ```ruby
1162
- > params do
1163
- > property :user, ref: UserEntity
1164
- > end
1165
- >
1166
- > status 200 do
1167
- > property :user, ref: UserEntity
1168
- > end
1169
- > ```
1170
-
1171
- ### 实体定义的其他介绍
1172
-
1173
- `param` 和 `expose` 的只会作用到同层的字段,不会作用到实体内部。
1174
-
1175
- 数组内部也可以引用实体,只要在字段上加上 `type: "array"` 即可:
1176
-
1177
- ```ruby
1178
- params do
1179
- param :users, type: "array", ref: UserEntity
1180
- end
1181
- ```
1182
-
1183
- 接下来会涉及之前没提过的配置选项,包括 `param`、`render`、`scope`、`value`、`convert` 等。单独说明某个选项的用法显得枯燥,我接下来将以列举场景的方式说明。
1184
-
1185
- ### 如何设置某个字段只作为参数或返回值
1186
-
1187
- 由于实体内部既包括参数的字段,也包括返回值的字段,必然有某些字段只可作为参数或返回值。这种情况该如何做呢?我们可以配置 `param: false` 定义这个字段不可作为参数,另外配置 `render: false` 定义这个字段不可用作返回值。如下是一个例子:
1188
-
1189
- ```ruby
1190
- class UserEntity < Meta::Entity
1191
- property :id, param: false # id 字段不可用作参数
1192
- property :name # name 和 age 字段既可用作参数,也可用作返回值
1193
- property :age
1194
- property :password, render: false # password 字段不可用作返回值
1195
- end
1196
- ```
1197
-
1198
- 虽然有点啰嗦,上述例子如果接收的参数是:
1199
-
1200
- ```ruby
1201
- # HTTP params
1202
- { "id": 1, "name": "Jim", "age": 18, "password": "123456" }
1203
- ```
1204
-
1205
- 则程序中获取到的参数内容是:
1206
-
1207
- ```ruby
1208
- # p params
1209
- { name: "Jim", age: 18, password: "123456" }
1210
- ```
1211
-
1212
- 另外,如果我们渲染了如下的数据:
1213
-
1214
- ```ruby
1215
- action do
1216
- render("id" => 1, "name" => "Jim", "age" => 18, "password" => "123456")
1217
- end
1218
- ```
1219
-
1220
- 则客户端实际得到的 JSON 格式是:
1221
-
1222
- ```ruby
1223
- # HTTP Response
1224
- { "id": 1, "name": "Jim", "age": 18 }
1225
- ```
1226
-
1227
- #### 引申:`param` 和 `render` 本质探究
1228
-
1229
- `param` 和 `render` 支持两种格式:其一是刚刚见过的 `false`,它将禁用参数或返回值;另一个可传递 Hash,它将设置独属于参数或返回值的选项。例如,我希望只对参数作校验,而返回值不作校验,可如下设配置:
1230
-
1231
- ```ruby
1232
- property :name, param: { required: true }
1233
- ```
1234
-
1235
- 再比如,我对参数不设置默认值,而返回渲染的时候提供默认值(这个例子比较少见):
1236
-
1237
- ```ruby
1238
- property :age, render: { default: 18 }
1239
- ```
1240
-
1241
- ### 如何控制不同场景下的字段
1242
-
1243
- 上一节讲的是字段如何区分参数或返回值的情况。这是一个方面,另一方面是如何控制在不同接口下的字段返回。
1244
-
1245
- 例如,我们定义列表接口时不需要返回详情字段,一来是列表页面用不到,另一来是详情内容会导致返回实体过大而造成网络拥塞。这时,`scope` 选项能够起到作用了。我们定义一个实体 `ArticleEntity`:
1246
-
1247
- ```ruby
1248
- class ArticleEntity < Meta::Entity
1249
- property :title
1250
- property :content, render: { scope: 'full' }
1251
- end
1252
- ```
1253
-
1254
- > **小提示:**`scope` 选项放在 `render` 下定义,因为参数获取不需要区分场景。
1255
-
1256
- 注意到 `content` 被限制了 scope 为 `"full"` 了,默认情况下它是不会返回的。像列表接口就可以直接渲染它:
1257
-
1258
- ```ruby
1259
- get '/articles' do
1260
- status 200 do
1261
- expose :articles, ref: ArticleEntity
1262
- end
1263
- action do
1264
- articles = Article.all
1265
- render :articles, articles
1266
- end
1267
- end
1268
- ```
1269
-
1270
- 而详情接口下需要返回 `content` 字段,需要明确附加一个 `scope` 为 `"full"`:
1271
-
1272
- ```ruby
1273
- get '/articles/:id' do
1274
- status 200 do
1275
- expose :article, ref: ArticleEntity
1276
- end
1277
- action do
1278
- article = Article.find(request.params['id'])
1279
- render :article, article, scope: "full"
1280
- end
1281
- end
1282
- ```
1283
-
1284
- 如果你认为在调用 `render` 方法时较为繁琐,好奇为什么不在声明时定义。如果你更青睐这种方式,可以在声明时控制,方法是将实体锁住。实体被锁住后就不需要在 `render` 时传递任何选项了:
1285
-
1286
- ```ruby
1287
- get '/articles/:id' do
1288
- status 200 do
1289
- expose :article, ref: ArticleEntity.lock_scope('full')
1290
- end
1291
- action do
1292
- article = Article.find(request.params['id'])
1293
- render :article, article
1294
- end
1295
- end
1296
- ```
1297
-
1298
- 也许在参数声明中这种方式更有效果。因为调用参数时我们无法传递 `scope` 选项,锁住是唯一的途径:
1299
-
1300
- ```ruby
1301
- post '/articles' do
1302
- params do
1303
- param :article, ref: ArticleEntity.lock_scope('on_create')
1304
- end
1305
- ...
1306
- end
1307
-
1308
- put '/articles/:id' do
1309
- params do
1310
- param :article, ref: ArticleEntity.lock_scope('on_update')
1311
- end
1312
- ...
1313
- end
1314
- ```
1315
-
1316
- ### 如何渲染计算出来的结果
1317
-
1318
- 假设现有如下实体:
1319
-
1320
- ```ruby
1321
- class UserEntity < Meta::Entity
1322
- property :first_name
1323
- property :last_name
1324
- end
1325
- ```
1326
-
1327
- 现在我们想要加一个 `full_name` 字段,它是 `first_name` 和 `last_name` 加起来的结果。这时我们可以使用 `value` 选项自己将结果计算下来:
1328
-
1329
- ```ruby
1330
- class UserEntity < Meta::Entity
1331
- property :first_name
1332
- property :last_name
1333
- property :full_name, param: false, value: lambda do |user|
1334
- "#{user['first_name']} #{user['last_name']}"
1335
- end
1336
- end
1337
- ```
1338
-
1339
- > **小提示:** 设置 `param` 为 `false`,因为参数获取时没有这个字段。
1340
-
1341
- `value` 传递的块可以访问到执行环境,以下是一个示例:
1342
-
1343
- ```ruby
1344
- status 200 do
1345
- expose :is_admin, value: lambda { @user.admin? }
1346
- end
1347
- action do
1348
- @user = get_user
1349
- end
1350
- ```
1351
-
1352
- ### 参数值转化
1353
-
1354
- 首先,我们定义实体:
1355
-
1356
- ```ruby
1357
- class ArticleEntity < Meta::Entity
1358
- property :image,
1359
- type: 'string',
1360
- description: '客户端传递 Base64 格式的数据'
1361
- end
1362
- ```
1363
-
1364
- 然后我们定义路由参数:
1365
-
1366
- ```ruby
1367
- params do
1368
- param :article, ref: ArticleEntity
1369
- end
1370
- ```
1371
-
1372
- 由于客户端传递的参数是 Base64 格式的,我们需要在执行环境中将其转化为原本格式:
1373
-
1374
- ```ruby
1375
- action do
1376
- article_params = {
1377
- **params[:article],
1378
- image: decode_base64(params[:article][:image])
1379
- }
1380
- end
1381
- ```
1382
-
1383
- 但每个接口下都做这样的转化工作挺是繁琐,框架提供了 `convert` 选项,它用于将值转化为预期的格式:
1384
-
1385
- ```ruby
1386
- class ArticleEntity < Meta::Entity
1387
- property :image,
1388
- type: 'string',
1389
- description: '客户端传递 Base64 格式的数据',
1390
- param: {
1391
- convert: lambda { |value| decode_base64(value) }
1392
- }
1393
- end
1394
- ```
1395
-
1396
- > **小提示:** 注意只应当在参数过程中做格式转换,渲染过程中做同样的转换将会出错。
1397
-
1398
- 这样执行环境中就不需要手动进行格式转换了:
1399
-
1400
- ```ruby
1401
- action do
1402
- article_params = params[:article]
1403
- end
1404
- ```
1405
-
1406
- ### 多态参数和返回值
1407
-
1408
- 定义属性时可定义多态类型,`dynamic_ref` 选项可接受一个块,它根据值来返回指定的类型:
1409
-
1410
- ```ruby
1411
- property :target, dynamic_ref: ->(value) {
1412
- # 根据 value.target_type 值返回实体类型
1413
- # 例如,value.target_type == 'UserEntity',将返回 UserEntity 类
1414
- value.target_type.constantize
1415
- }
1416
- ```
1417
-
1418
- 或者接受一个 Hash,这时可提供 `one_of` 选项为文档生成提供加成:
1419
-
1420
- ```ruby
1421
- property :animal, dynamic_ref: {
1422
- one_of: [CatEntity, DogEntity, PigEntity],
1423
- resolve: ->(value) { value.animal_type.constantize }
1424
- }
1425
- ```
1426
-
1427
- ## 生成文档
1428
-
1429
- 应用模块提供一个 `to_swagger_doc` 方法生成 Open API 规格文档,该文档可被 Swagger UI 或基于 Swagger UI 的引擎渲染。
1430
-
1431
- ```ruby
1432
- class DemoApp < Meta::Application
1433
- end
1434
-
1435
- # 生成 JSON 格式的规格文档
1436
- DemoApp.to_swagger_doc(
1437
- info: {
1438
- title: 'Web API 示例项目',
1439
- version: 'current'
1440
- },
1441
- servers: [
1442
- { url: 'http://localhost:9292', description: 'Web API 示例项目' }
1443
- ]
1444
- )
1445
- ```
1446
-
1447
- 其中 `info` 和 `servers` 选项是 *Open API 规格文档* 中提供。
1448
-
1449
- > 了解 [Open API 规格文档](https://swagger.io/resources/open-api/https://swagger.io/resources/open-api/)。
1450
- >
1451
- > 了解 [Swagger UI](https://swagger.io/tools/swagger-ui/).
1452
-
1453
- ## 全局配置
1454
-
1455
- ### 定义默认的 `locked_scope`
1456
-
1457
- ```ruby
1458
- Meta.config.default_locked_scope = 'default'
1459
- ```
1460
-
1461
- 当 `Meta::Entity` 被引用时,其没有被锁定 `scope`. 如果我们如上设定了默认的 `scope`,则
1462
-
1463
- ```ruby
1464
- param :user, ref: UserEntity
1465
- ```
1466
-
1467
- 的效果等价于
1468
-
1469
- ```ruby
1470
- param :user, ref: UserEntity.lock_scope('default')
1471
- ```
1472
-
1473
- 这样做对生成文档有帮助。因为我们没有锁定 `UserEntity`,则文档中它的所有属性都会被列出来,而在实际执行时却不能得到带有 `scope` 标记的字段,这样往往在造成文档和实际情况的不一致。
1474
-
1475
- ### 定义 `JsonSchema#filter` 方法的 `user_options`
1476
-
1477
- ```ruby
1478
- Meta.config.json_schema_user_options = {...}
1479
- Meta.config.json_schema_param_stage_options = {...}
1480
- Meta.config.json_schema_render_stage_options = {...}
1481
- ```
1482
-
1483
- **示例一:关闭渲染时验证**
1484
-
1485
- 渲染时不执行类型转换和数据验证:
1486
- ```ruby
1487
- Meta.config.json_schema_render_stage_options = {
1488
- type_conversion: false,
1489
- render_validation: false
1490
- }
1491
- ```
1492
-
1493
- **示例二:默认使用 `discard_missing: true` 方案**
1494
-
1495
- ```ruby
1496
- Meta.config.json_schema_user_options = {
1497
- discard_missing: true
1498
- }
1499
- ```
1500
-
1501
- 或仅在参数阶段使用 `discard_missing: true` 方案:
1502
-
1503
- ```ruby
1504
- Meta.config.json_schema_param_stage_options = {
1505
- discard_missing: true
1506
- }
1507
- ```
1508
-
1509
- > 提示:可以传入一切 `JsonSchema#filter` 支持的选项,参考 [JsonSchema#filter 支持的选项](索引.md)。
1510
-
1511
- ## 特殊用法举例
1512
-
1513
- ### 路由中实体定义的特殊用法
1514
-
1515
- 虽然推荐的方案是在实体之上包裹一个根字段,像下面这样:
1516
-
1517
- ```ruby
1518
- params do
1519
- param :user, ref: UserEntity
1520
- end
1521
-
1522
-
1523
- # 接受如下格式的数据
1524
- { "user": { "name": "Jim", "age": 18 } }
1525
- ```
1526
-
1527
- 但也可以将包裹的外层字段去掉,即将 `UserEntity` 直接用在顶层:
1528
-
1529
- ```ruby
1530
- params ref: UserEntity
1531
-
1532
-
1533
- # 接受如下格式的数据
1534
- { "name": "Jim", "age": 18 }
1535
- ```
1536
-
1537
- 这个方案同时也支持数组:
1538
-
1539
- ```ruby
1540
- params type: 'array', ref: UserEntity
1541
-
1542
- # 接受如下格式的数据
1543
- [
1544
- { "name": "Jim", "age": 18 },
1545
- { "name": "Jack", "age": 19 }
1546
- ]
1547
- ```
1548
-
1549
- 虽然更不常见,标量值也是支持的:
1550
-
1551
- ```ruby
1552
- params type: 'string'
1553
-
1554
- # 接受字符串数据
1555
- "foo"
1556
- ```
1557
-
1558
- ### 完整更新和局部更新
1559
-
1560
- HTTP 提供了两个方法 `PUT` 和 `PATCH`,它们的语义差别体现在更新策略上。`PUT` 要求是完整更新,`PATCH` 要求是局部更新。
1561
-
1562
- 假设我们定义参数格式为:
1563
-
1564
- ```ruby
1565
- params do
1566
- param :user do
1567
- param :name
1568
- param :age
1569
- end
1570
- end
1571
- ```
1572
-
1573
- 同时我们收到客户端的参数格式为:
1574
-
1575
- ```json
1576
- {
1577
- "user": {
1578
- "name": "Jim"
1579
- }
1580
- }
1581
- ```
1582
-
1583
- `params` 方法默认的逻辑符合完整更新:
1584
-
1585
- ```ruby
1586
- put '/users/:id' do
1587
- action do
1588
- user = User.find(request.params['id'])
1589
-
1590
- user_params = params[:user] # => { name: "Jim", age: nil }
1591
- user.update(user_params)
1592
- end
1593
- end
1594
- ```
1595
-
1596
- 而 `params(:discard_missing)` 将符合局部更新的逻辑:
1597
-
1598
- ```ruby
1599
- patch '/users/:id' do
1600
- action do
1601
- user = User.find(request.params['id'])
1602
-
1603
- user_params = params(:discard_missing)[:user] # => { name: "Jim" }
1604
- user.update(user_params)
1605
- end
1606
- end
1607
- ```
1608
-
1609
- > **小提示:**还有一种调用方式 `params(:raw)`,它返回无任何转换逻辑的原生参数。它与 `request.params` 的行为一致。
1610
-
1611
- > **大提示:**如果你是通过 `ref:` 引用一个实体定义,另一个更符合语义的方式是使用 `lock` 方法。
1612
- >
1613
- > ```ruby
1614
- > patch '/users/:id' do
1615
- > params do
1616
- > param :user, ref: UserEntity.lock(:discard_missing, true)
1617
- > end
1618
- > action do
1619
- > user = User.find(params['id'])
1620
- >
1621
- > user_params = params[:user] # 不需要传递 `:discard_missing` 符号了,同样也会返回 `{ name: "Jim" }`
1622
- > user.update(user_params)
1623
- > end
1624
- > end
1625
- > ```
1626
-
1627
- ### `namespace` 中使用 `rescue_error Meta::Errors::NoMatchingRoute` 无效
1628
-
1629
- `Meta::Errors::NoMatchingRoute` 只在顶层捕获有效,在内部捕获无效。
1630
-
1631
- ```ruby
1632
- class DemoApp < Meta::Application
1633
- # 在此捕获有效
1634
- rescue_error Meta::Errors::NoMatchingRoute do |e|
1635
- response.status = 404
1636
- response.body = ["404 Not Found"]
1637
- end
1638
-
1639
- namespace '/namespace' do
1640
- # 在此捕获无效
1641
- rescue_error Meta::Errors::NoMatchingRoute do |e|
1642
- response.status = 404
1643
- response.body = ["404 Not Found"]
1644
- end
1645
- end
1646
- end
1647
- ```