meta-api 0.0.1

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 (52) hide show
  1. checksums.yaml +7 -0
  2. data/.autoenv.zsh +1 -0
  3. data/.gitignore +6 -0
  4. data/.rubocop.yml +28 -0
  5. data/Gemfile +18 -0
  6. data/Gemfile.lock +66 -0
  7. data/LICENSE.txt +502 -0
  8. data/README.md +149 -0
  9. data/Rakefile +3 -0
  10. data/config/locales/zh-CN.yml +6 -0
  11. data/docs//345/220/215/347/247/260/347/224/261/346/235/245.md +7 -0
  12. data/docs//346/225/231/347/250/213.md +1199 -0
  13. data/docs//347/264/242/345/274/225.md +173 -0
  14. data/examples/lobster.rb +71 -0
  15. data/examples/rack_app/README.md +3 -0
  16. data/examples/rack_app/config.ru +6 -0
  17. data/examples/rack_app/hello.rb +6 -0
  18. data/examples/rack_app/timing.rb +15 -0
  19. data/lib/meta/api.rb +3 -0
  20. data/lib/meta/application/application.rb +63 -0
  21. data/lib/meta/application/execution.rb +178 -0
  22. data/lib/meta/application/meta.rb +71 -0
  23. data/lib/meta/application/path_matching_mod.rb +53 -0
  24. data/lib/meta/application/route.rb +58 -0
  25. data/lib/meta/application.rb +42 -0
  26. data/lib/meta/entity.rb +59 -0
  27. data/lib/meta/errors.rb +29 -0
  28. data/lib/meta/json_schema/builders/array_schema_builder.rb +29 -0
  29. data/lib/meta/json_schema/builders/object_schema_builder.rb +120 -0
  30. data/lib/meta/json_schema/builders/schema_builder_tool.rb +29 -0
  31. data/lib/meta/json_schema/schemas/array_schema.rb +40 -0
  32. data/lib/meta/json_schema/schemas/base_schema.rb +110 -0
  33. data/lib/meta/json_schema/schemas/object_schema.rb +161 -0
  34. data/lib/meta/json_schema/schemas.rb +12 -0
  35. data/lib/meta/json_schema/support/errors.rb +38 -0
  36. data/lib/meta/json_schema/support/presenters.rb +35 -0
  37. data/lib/meta/json_schema/support/schema_options.rb +55 -0
  38. data/lib/meta/json_schema/support/type_converter.rb +137 -0
  39. data/lib/meta/json_schema/support/validators.rb +54 -0
  40. data/lib/meta/load_i18n.rb +8 -0
  41. data/lib/meta/route_dsl/action_builder.rb +15 -0
  42. data/lib/meta/route_dsl/application_builder.rb +108 -0
  43. data/lib/meta/route_dsl/chain_builder.rb +48 -0
  44. data/lib/meta/route_dsl/helpers.rb +15 -0
  45. data/lib/meta/route_dsl/meta_builder.rb +57 -0
  46. data/lib/meta/route_dsl/parameters_builder.rb +24 -0
  47. data/lib/meta/route_dsl/route_builder.rb +85 -0
  48. data/lib/meta/route_dsl/uniformed_params_builder.rb +34 -0
  49. data/lib/meta/swagger_doc.rb +86 -0
  50. data/lib/meta/utils/path.rb +20 -0
  51. data/meta-api.gemspec +23 -0
  52. metadata +96 -0
@@ -0,0 +1,1199 @@
1
+ # 教程
2
+
3
+ 现有的 Web API 框架并不关注文档的问题,文档往往是作为插件挂载到框架上的。但是,文档和业务实现并不需要割裂开,它们在很大程度上应该是耦合在一起的。比方说,某个接口我定义了参数如此,就该自动生成一致的文档向前端告知;同样,当我提供了文档是如此后,我的接口实现就改自动地约束为这样实现。
4
+
5
+ Meta 框架(暂定名)天生就是将文档和实现统一起来的,并始终致力于此(如果真的有什么不一致或者不到位的地方,那只能说框架实现上尚有欠缺,并不能从思想上说本该如此)。Meta 框架与 Swagger 合作,致力于产生符合 Restful 和社区规范的文档格式。它提供了几乎完整的描述接口信息的宏命令,并且在描述接口的同时就能基本实现接口的一些约束,其中最重要的莫过于对参数和返回值的声明。
6
+
7
+ ## 准备工作
8
+
9
+ 在正式阅读本教程之前,有一些*准备工作*需要提前了解的。
10
+
11
+ ### 一些限制
12
+
13
+ 在正式介绍框架能力之前,我先声明一下框架的约束。设定这些约束,主要是因为框架开发尚处于初期阶段,而且精力有限,因此将框架的完成度限定在更小的范围内。
14
+
15
+ 1. **只接受格式为 `application/json` 的请求参数,并且响应实体的格式一律为 `application/json`.**
16
+
17
+ 这在当前的环境下并不算太大的限制,如果你是致力于新项目的开发的话。但是,如果你处理旧项目,并且要求格式为 `application/json` 之外的格式,如 `application/xml`,则框架目前是无能为力的。
18
+
19
+ 这中限制只包括请求参数的实体,诸如路径里的参数、或 query 中的参数依然可用,不受该限制。
20
+
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))。
24
+
25
+ ### 教程脉络
26
+
27
+ 首先,你将了解定义路由的全部知识。你从其他框架学习的经验也同样适用于本框架,如嵌套路由、before/after 钩子、异常拦截等、以及模块共享和复用等。
28
+
29
+ 然后,你将了解路由的内部如何定义。换句话说,你该如何具体地*描述*一个接口。一般来说,我们需要描述接口的标题、详述、标签、参数和返回值。
30
+
31
+ 接下来,我们将深入参数和返回值的定义。虽然说前面已经提到参数和返回值的知识,但仅覆盖最简单同时也是最常用的场景。参数和返回值的知识实在是太大了,有必要专门划出一个章节来介绍它。这里提一下,参数和返回值在 Meta 框架里都统一为一个叫做实体的概念,因此你只需要学会定义一种就能够同时定义两者了。
32
+
33
+ 最后,将是一个生成文档的方法。虽然它很简单,仅仅是一个方法,但它如此重要以至于我不得不专门划出一个章节来强调它的重要性。说实话将这块内容放在最后我有点不太满意,它是如此重要,开篇就该提到。
34
+
35
+ ## 模块和路由定义
36
+
37
+ *(我抛弃了 Rack 和中间件的知识,如果你知道它将更好了)*
38
+
39
+ 在 Meta 中,`Meta::Application` 类用来定义一个模块。一个模块同时也是一个应用,将它挂载在 Rack 下可以直接作为一个服务运行。我将它称为模块主要是因为它可以复用(后面你将了解到使用 `apply` 方法复用一个模块)。
40
+
41
+ 我们先看一下最简单的 `Meta::Application` 实例:
42
+
43
+ ```ruby
44
+ class DemoApp < Meta::Application
45
+ get do
46
+ title '应用的根路径'
47
+ action do
48
+ response.body = ["Hello, world!"]
49
+ end
50
+ end
51
+ end
52
+ ```
53
+
54
+ *将它挂载在 Rack 下并访问 `http://localhost:9292` 你将看到效果。*
55
+
56
+ 这里,我们只是用 `get` 方法简单地定义了一个 `get /` 路由,并定义了该路由下的标题和实现。该实现没有用到 Meta 框架提供的参数和返回值的概念,只是简单地操纵原生 Rack Response 对象返回一个纯文本格式。
57
+
58
+ ### 路由定义
59
+
60
+ 除了 `get` 之外,我们还支持 `post`、`put`、`patch`、`delete` 四个方法。同时后面可跟一个路径表示路的路径:
61
+
62
+ ```ruby
63
+ class DemoApp < Meta::Application
64
+ post do
65
+ # ...
66
+ end
67
+
68
+ put '/foo' do
69
+ # ...
70
+ end
71
+
72
+ patch '/bar' do
73
+ # ...
74
+ end
75
+
76
+ delete '/foo/bar' do
77
+ # ...
78
+ end
79
+ end
80
+ ```
81
+
82
+ 以上方法只是 `route(path, method, &block)` 的简写,例如 `put '/foo'` 可以完整地写为 `route '/foo', :put`.
83
+
84
+ ### 路径定义
85
+
86
+ 路径中可以包括参数:
87
+
88
+ - `/foo/:id`:它将匹配诸如 `/foo/1`、`/foo/bar` 等路径,但不能匹配 `/foo/a/b/c` 这样的路径。
89
+ - `/foo/*path`:它可以匹配 `/foo`、`/foo/bar`、`/foo/a/b/c` 等格式的路径。
90
+
91
+ 凡是路径中带有命名参数的都能被访问到,例如 `request.params['id']`、`request.params['path']` 。(注意,方括号内一定得是字符串)
92
+
93
+ 如果你不需要后续访问到参数,可以忽略命名。不过我认为加个名字语义上更加友好,尽管你不必用到。
94
+
95
+ - `/foo/:`
96
+ - `/foo/*`
97
+
98
+ 在定义参数时要学会不拘一格,试想一下以下的路径定义将匹配哪些:
99
+
100
+ - `/foo/:id/bar`
101
+ - `/foo/*/bar`
102
+
103
+ ### 嵌套路由
104
+
105
+ 用 `route` 方式定义的路由是不支持嵌套的。有一个专门为定义嵌套路由而存在的命令:`namespace`.
106
+
107
+ ```ruby
108
+ class DemoApp < Meta::Application
109
+ namespace '/user' do
110
+ get do
111
+ title '获取用户详情'
112
+ end
113
+
114
+ put do
115
+ title '更新用户详情'
116
+ end
117
+ end
118
+ end
119
+ ```
120
+
121
+ ### before/after 钩子
122
+
123
+ *(如果不涉及到钩子和异常拦截,嵌套路由将毫无意义。)*
124
+
125
+ 正如名字所表达的那样,`before` 就是 before 钩子,`after` 就是 after 钩子。将以上例子加上 `before` 钩子将如下:
126
+
127
+ ```ruby
128
+ class DemoApp < Meta::Application
129
+ namespace '/user' do
130
+ before do
131
+ @user = get_user
132
+ end
133
+
134
+ get do
135
+ title '获取用户详情'
136
+ end
137
+
138
+ put do
139
+ title '更新用户详情'
140
+ end
141
+ end
142
+ end
143
+ ```
144
+
145
+ ### 异常拦截
146
+
147
+ 在 `namespace` 中使用 `rescue_error` 拦截异常。
148
+
149
+ ```ruby
150
+ class DemoApp < Meta::Application
151
+ namespace '/user' do
152
+ rescue_error RecordNotFound do |e|
153
+ response.status = 404
154
+ response.body = ["所访问的资源不存在"]
155
+ end
156
+ end
157
+ end
158
+ ```
159
+
160
+ ### 关于嵌套的进一步说明
161
+
162
+ 钩子只在当前作用域和它的子作用域下起作用,父级作用域不会起作用。
163
+
164
+ ```ruby
165
+ class DemoApp < Meta::Application
166
+ namespace '/foo' do
167
+ before do
168
+ @foo = 'foo'
169
+ end
170
+
171
+ get do
172
+ action do
173
+ p @foo # 'foo'
174
+ p @bar # nil
175
+ end
176
+ end
177
+
178
+ namespace '/bar' do
179
+ before do
180
+ @foo = 'foo'
181
+ end
182
+
183
+ get do
184
+ action do
185
+ p @foo # 'foo'
186
+ p @bar # 'bar'
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
192
+ ```
193
+
194
+ 异常拦截先拦截子作用域;如果拦截失败则继续在父作用域下拦截。
195
+
196
+ ```ruby
197
+ class DemoApp < Meta::Application
198
+ namespace '/foo' do
199
+ rescue_error ErrorOne do
200
+ # 它将捕获 '/foo' 路由下的异常
201
+ end
202
+
203
+ namespace '/bar' do
204
+ rescue_error ErrorTwo do
205
+ # 它将捕获 '/foo/bar' 路由下的异常
206
+ end
207
+ end
208
+ end
209
+ end
210
+ ```
211
+
212
+ ### 模块
213
+
214
+ `Meta::Application` 可以像 `namespace` 一样,定义路由、设置 before/after 钩子、拦截异常等。将以上例子里 `/foo` 的部分抽成一个模块如下:
215
+
216
+ ```ruby
217
+ class Foo < Meta::Application
218
+ rescue_error ErrorOne do
219
+ # 它将捕获 '/foo' 路由下的异常
220
+ end
221
+
222
+ namespace '/bar' do
223
+ rescue_error ErrorTwo do
224
+ # 它将捕获 '/foo/bar' 路由下的异常
225
+ end
226
+ end
227
+ end
228
+ ```
229
+
230
+ 为达到同样的效果,我们可以在 `DemoApp` 下应用这个模块:
231
+
232
+ ```ruby
233
+ class DemoApp < Meta::Application
234
+ namespace '/foo' do
235
+ apply Foo
236
+ end
237
+ end
238
+ ```
239
+
240
+ 模块应用最常用的场景是将接口分离到单独的文件中定义。这里我贴出我在一个实际项目中的模块划分:
241
+
242
+ ```ruby
243
+ class OpenAPIApp < Meta::Application
244
+ apply API::Logins
245
+ apply API::Users
246
+ apply API::Organizations
247
+ apply API::Projects
248
+ apply API::Versions
249
+ apply API::Members
250
+ end
251
+ ```
252
+
253
+ ## 路由内部定义
254
+
255
+ 现在我们关注路由内部细节的定义,包括标题、描述、参数、返回值乃至于如何实现业务逻辑等。我们知道,路由是通过 `route` 方法(以及它的一系列便捷方法 `get`、`post` 等),也就是说我们现在开始关注 `route` 方法内部能定义什么。
256
+
257
+ 本来想大书特书,结果发现以下代码示例便能将用到的宏命令列举完毕。
258
+
259
+ ```ruby
260
+ route '/user', :put do
261
+ title '更新用户'
262
+ description '接口的详细描述'
263
+ tags ['User'] # 传递一个数组
264
+ params do
265
+ # 定义参数
266
+ param :user do
267
+ param :name
268
+ param :age
269
+ end
270
+ end
271
+ status 200 do
272
+ # 定义返回值,其中 200 是状态码
273
+ expose :user do
274
+ expose :name
275
+ expose :age
276
+ end
277
+ end
278
+ action do
279
+ # 业务逻辑在这里实现,通过 params 方法访问参数,render 方法渲染实体
280
+ user = get_user
281
+ user.update!(params[:user])
282
+ render :user, user
283
+ end
284
+ end
285
+ ```
286
+
287
+ ### `meta` 命令
288
+
289
+ 除 `action` 之外,`route` 块下其余的命令都与文档的生成相关。它们都可以被汇总到一个称为 `meta` 的块内:
290
+
291
+ ```ruby
292
+ route '/user', :put do
293
+ meta do
294
+ title '更新用户'
295
+ description '接口的详细描述'
296
+ tags ['User'] # 传递一个数组
297
+ params do
298
+ # 定义参数
299
+ end
300
+ status 200 do
301
+ # 定义返回值
302
+ end
303
+ end
304
+ action do
305
+ # 业务逻辑仍在这里实现
306
+ end
307
+ end
308
+ ```
309
+
310
+ ### `namespace` 下的 `meta` 命令
311
+
312
+ `namespace` 下可以也定义 `meta` 块,它可以定义接口声明的公共部分,应用到它的子路由下:
313
+
314
+ ```ruby
315
+ namespace '/user/:id' do
316
+ meta do
317
+ # 在 namespace 下定义 title 和 description 没有意义
318
+ tags ['User'] # 该 namespace 下的接口归到 User 标签下
319
+ params do # 定义共同参数
320
+ param :id
321
+ end
322
+ status 200 do # 定义共同返回值
323
+ expose :user, type: 'object'
324
+ end
325
+ end
326
+
327
+ get do
328
+ title '返回用户详情'
329
+ action do
330
+ user = User.find(params[:id]) # params 包括 id 字段
331
+ render :user, user # render 包括 user 字段
332
+ end
333
+ end
334
+
335
+ put do
336
+ title '更新用户'
337
+ params do
338
+ # 补充参数 user
339
+ param :user, type: 'object'
340
+ end
341
+ action do
342
+ user = User.find(params[:id])
343
+ user.update!(params[:user]) # params 包括 id、user 字段
344
+ render :user, user # render 包括 user 字段
345
+ end
346
+ end
347
+ end
348
+ ```
349
+
350
+ ## 参数定义
351
+
352
+ 本节介绍参数和返回值如何定义。因为 Meta 框架在底层不区分参数和返回值,它们都统一为“实体”的概念。因此,当涉及到语法细节时,在参数、返回值、实体内都是一致的。
353
+
354
+ 可以说,有关实体的定义,是 Meta 框架中细节最多的地方。在撰写这一章节的时候,我尝试写过很多遍,都无法很好地将方方面面说明清楚。我在行文时,一方面希望大家在入门的时候方便,能够很快地定义常用的用法;另一方面,也希望将所涉及的细节都能够阐述清楚,希望大家能够全面了解到 Meta 框架实体定义的方方面。现在,我只能尽可能地做到这两点,却不再强求。我将以场景的形式阐述用法,而不是孤立地介绍每个知识点。
355
+
356
+ ### 初探:参数定义和过滤
357
+
358
+ 在路由中,我们用 `params` 命令定义参数:
359
+
360
+ ```ruby
361
+ post '/users' do
362
+ params do
363
+ param :name
364
+ param :age
365
+ end
366
+ end
367
+ ```
368
+
369
+ 然后,我们可以在 `action` 命令中使用 `params` 方法访问参数。如果我们发送了一个这样的请求:
370
+
371
+ ```bash
372
+ POST '/users' -d '{"name": "Jim", "age": 18}'
373
+ ```
374
+
375
+ 我们将在 `action` 命令中获取到这样的参数结构:
376
+
377
+ ```ruby
378
+ post '/users' do
379
+ action do
380
+ p params # => { name: "Jim", age: 18 }
381
+ end
382
+ end
383
+ ```
384
+
385
+ 参数解析时有自动过滤的作用,如果我们发送了一个这样的请求:
386
+
387
+ ```bash
388
+ POST '/users' -d '{"name": "Jim", "foo": "foo"}'
389
+ ```
390
+
391
+ 我们将在 `action` 命令中获取到这样的参数结构:
392
+
393
+ ```ruby
394
+ post '/users' do
395
+ action do
396
+ p params # => { name: "Jim", age: nil }
397
+ end
398
+ end
399
+ ```
400
+
401
+ 可以看到,它的做法是过滤了未定义的 `foo` 字段,并且将没有提供的 `age` 字段设为 `nil`.
402
+
403
+ ### 嵌套:参数的层次结构
404
+
405
+ 一般我在实际项目中不会这么定义,而是更习惯将它们套在一个根字段下,这样做有利于结构的划分和后期的扩展。
406
+
407
+ ```ruby
408
+ post '/users' do
409
+ params do
410
+ param :user do
411
+ param :name
412
+ param :age
413
+ end
414
+ end
415
+ end
416
+ ```
417
+
418
+ 以上定义需要接收以下格式的参数:
419
+
420
+ ```bash
421
+ POST '/users' -d '{
422
+ "user": {
423
+ "name": "Jim",
424
+ "age": 18
425
+ }
426
+ }'
427
+ ```
428
+
429
+ 在 `action` 命令中获取到的也是这样的嵌套结构:
430
+
431
+ ```ruby
432
+ post '/users' do
433
+ action do
434
+ p params # => { user: { name: "Jim", age: 18 } }
435
+ end
436
+ end
437
+ ```
438
+
439
+ 参数过滤也会发挥作用。如果请求参数传递的是这样的格式:
440
+
441
+ ```ruby
442
+ # HTTP params
443
+ { "user": { "name": "Jim", "foo": "foo" } }
444
+ ```
445
+
446
+ 程序中获取的参数格式将是这样:
447
+
448
+ ```ruby
449
+ # p params
450
+ { user: { name: "Jim", age: nil } }
451
+ ```
452
+
453
+ 如果顶层的 `user` 字段未提供,则整个 `user` 字段将设为 `nil`.
454
+
455
+ > **提醒:**后续我们会在代码第一行添加 `# HTTP params` 注释表明这是 HTTP 请求的参数格式,`# p params` 注释表明这是程序中获取到的参数格式。
456
+
457
+ ### 类型:参数的约束之一
458
+
459
+ 参数提供很多约束选项,类型只是其中之一。
460
+
461
+ #### 基本类型定义
462
+
463
+ 将以上的参数定义添加上类型定义,应当是:
464
+
465
+ ```ruby
466
+ params do
467
+ param :name, type: 'string'
468
+ param :age, type: 'integer'
469
+ end
470
+ ```
471
+
472
+ 类型定义首要的作用是报错,例如以下的参数格式会向客户端抛出 `400 Bad Request`:
473
+
474
+ ```ruby
475
+ # HTTP params
476
+ { "name": "Jim", "age": "eighteen" }
477
+ ```
478
+
479
+ 参数在进行类型校验时是宽容的。如果值能够成功转化为定义的类型,则参数校验不会报错。以下参数不会报错:
480
+
481
+ ```ruby
482
+ # HTTP params
483
+ { "name": "Jim", "age": "18" }
484
+ ```
485
+
486
+ 只不过参数过滤会规范化最终参数的格式,因此以上参数在程序中会获取为:
487
+
488
+ ```ruby
489
+ # p params
490
+ { name: "Jim", age: 18 }
491
+ ```
492
+
493
+ `age` 字段获取到的是数字类型而非字符串类型。
494
+
495
+ #### 嵌套类型定义
496
+
497
+ 对于嵌套参数,你可以定义为 `object` 类型。但这是没必要的,因为嵌套参数必然应当是 `object` 类型。以下两个参数定义等价:
498
+
499
+ ```ruby
500
+ params do
501
+ param :user, type: 'object' do
502
+ param :name
503
+ param :age
504
+ end
505
+ end
506
+
507
+ params do
508
+ param :user do
509
+ param :name
510
+ param :age
511
+ end
512
+ end
513
+ ```
514
+
515
+ 当定义 `object` 类型时,也可以不提供内部结构,此时将不再进行内部结构的过滤:
516
+
517
+ ```ruby
518
+ params do
519
+ param :user, type: 'object'
520
+ end
521
+
522
+ # 接受的 HTTP 请求参数
523
+ { "user": { "name": "Jim", "age": 18 }}
524
+ { "user": { "foo": "foo" } }
525
+ { "user": {} }
526
+ { "user": nil }
527
+
528
+ # 不接受的 HTTP 请求参数
529
+ { "user": "foo" }
530
+ { "user": 18 }
531
+ { "user": [] }
532
+ ```
533
+
534
+ 记住,不校验和 `object` 类型不是一回事。不提供任何类型时,此时参数接受一切值:
535
+
536
+ ```ruby
537
+ params do
538
+ param :user
539
+ end
540
+
541
+ # 以下格式都接受
542
+ { "user": { "name": "Jim", "age": 18 }}
543
+ { "user": { "foo": "foo" } }
544
+ { "user": {} }
545
+ { "user": nil }
546
+ { "user": "foo" }
547
+ { "user": 18 }
548
+ { "user": [] }
549
+ ```
550
+
551
+ #### 数组类型定义
552
+
553
+ 你可以定义为数组类型,此时参数必须接受为对象数组的格式。
554
+
555
+ ```ruby
556
+ params do
557
+ param :users, type: 'array' do
558
+ param :name
559
+ param :age
560
+ end
561
+ end
562
+
563
+ # 接受的参数格式
564
+ # HTTP params
565
+ {
566
+ "users": [
567
+ { "name": "Jim", age: 18 },
568
+ { "name": "Jack", age: 19}
569
+ ]
570
+ }
571
+ ```
572
+
573
+ 有时候我们遇到数组内部不是对象的情况,这时候就不能使用嵌套定义:
574
+
575
+ ```ruby
576
+ params do
577
+ param :tags, type: 'array'
578
+ end
579
+ ```
580
+
581
+ 这个时候参数必须接收数组格式,但内部元素不会做校验。如果希望内部元素也要校验,用 `items` 选项定义内部结构:
582
+
583
+ ```ruby
584
+ params do
585
+ param :tags, type: 'array', items: { type: 'string' }
586
+ end
587
+ ```
588
+
589
+ #### 完整的类型列表
590
+
591
+ 你能用到的 `type` 取值只能是以下之一:
592
+
593
+ - `"boolean"`
594
+ - `"integer"`
595
+ - `"number"`
596
+ - `"string"`
597
+ - `"object"`
598
+ - `"array"`
599
+
600
+ ### `required`:参数的约束之二
601
+
602
+ 正如其名,`required` 作“必须”校验。先前说过,未传递的字段会被赋予 `nil` . 然而,若字段被配置为 `required`,则参数校验会报错。以下参数定义中,`age` 字段被配置为 `required`:
603
+
604
+ ```ruby
605
+ # 参数定义
606
+ params do
607
+ param :name
608
+ param :age, required: true
609
+ end
610
+ ```
611
+
612
+ 以下请求皆会报错:
613
+
614
+ ```ruby
615
+ # HTTP params
616
+ POST '/users' -d '{"name": "Jim"}'
617
+
618
+ # HTTP params
619
+ POST '/users' -d '{"name": "Jim", age: null}'
620
+ ```
621
+
622
+ > **小提示:**先前说过,将参数套在一个根字段下是一个好的设计习惯。同时,将这个根字段配置为 `required` 也是一个好习惯。
623
+ >
624
+ > ```ruby
625
+ > # 参数定义
626
+ > params do
627
+ > param :user, required: true do
628
+ > param :name
629
+ > param :age
630
+ > end
631
+ > end
632
+ > ```
633
+ >
634
+ > 这会杜绝传递诸如 `{}`、`{ "user": null }` 这样的 JSON 格式。
635
+
636
+ ### 其他参数约束
637
+
638
+ 框架自带若干参数验证配置,在本节列举。
639
+
640
+ #### `required`
641
+
642
+ `required` 可同时配置 `allow_empty: true` 或 `allow_empty: false` 用以是否接受空字符串或空数组:
643
+
644
+ ```ruby
645
+ params do
646
+ param :title, require: { allow_empty: false } # 不接受空字符串
647
+ param :tags, require: { allow_empty: true } # 可接受空数组
648
+ end
649
+ ```
650
+
651
+ #### `format`
652
+
653
+ 为字符串参数配置 `format` 可限制参数的格式。以下是用到的 `format` 示例:
654
+
655
+ ```ruby
656
+ # 参数定义
657
+ params do
658
+ param :date, format: /^\d{4}-\d{2}-\d{2}$/
659
+ param :mobile, format: /^1[3456789]\d{9}$/
660
+ param :email, format: /^$/
661
+ end
662
+ ```
663
+
664
+ #### `allowable`
665
+
666
+ 通过一个数组配置一个字段可接受的值:
667
+
668
+ ```ruby
669
+ # 参数定义
670
+ params do
671
+ param :p_state, description: '进程状态', allowable: ["idle", "pending", "running", "exited"]
672
+ param :gender, description: '性别', allowable: ["male", "female"]
673
+ endparams do
674
+ param :state, description: '进程状态', allowable: ["idle", "pending", "running", "exited"]
675
+ param :sex, description: '性别', allowable: ["male", "female"]
676
+ end
677
+ ```
678
+
679
+ > **小提示:**一直忘记说了,我们可以通过 `description` 选项配置字段的描述,这个描述会在生成文档时生效。
680
+
681
+ #### `validate`:自定义校验
682
+
683
+ 如果以上的校验均不够用,Meta 支持自定义编写校验。`validate` 接受一个块,当校验失败时需要主动地抛出 `Meta::JsonSchema::ValidationError`:
684
+
685
+ ```ruby
686
+ params do
687
+ raise Meta::JsonSchema::ValidationError, '手机号格式不正确' unless value =~ /^1[3456789]\d{9}$/
688
+ end
689
+ ```
690
+
691
+ ### 设置参数的默认值
692
+
693
+ `default` 选项可设置参数的默认值,当参数未提供或为 `nil` 时,默认值就会起作用:
694
+
695
+ ```ruby
696
+ params do
697
+ param :age, default: 18
698
+ end
699
+ ```
700
+
701
+ ### 参数在文档中的位置
702
+
703
+ 默认情况下参数是放在 Request Body 下的(作为 `application/json` 的格式的一部分),但参数还可能存在于 path 或 query hash 中。使用 `in` 选项可以定义之,它对框架的执行没有影响,只对文档的生成产生效果。
704
+
705
+ ```ruby
706
+ post '/:in_path' do
707
+ params do
708
+ param :in_path, in: 'path'
709
+ param :in_query, in: 'query'
710
+ param :in_body, in: 'body'
711
+ end
712
+ end
713
+ ```
714
+
715
+ ## 返回值定义
716
+
717
+ `status` 宏命令用来定义返回值。你需要传递一个(或多个)状态码,并用一个同样结构的块作为实体的定义。
718
+
719
+ ```ruby
720
+ # 定义简单的返回实体
721
+ status 200 do
722
+ expose :name, type: 'string'
723
+ expose :age, type: 'integer'
724
+ end
725
+
726
+ # 同样支持嵌套
727
+ status 200 do
728
+ expose :user do
729
+ expose :name, type: 'string'
730
+ expose :age, type: 'integer'
731
+ end
732
+ end
733
+
734
+ # 同样支持校验,虽然我觉得校验返回值有点多此一举
735
+ status 200 do
736
+ expose :user, required: true do
737
+ expose :name, type: 'string'
738
+ expose :age, type: 'integer', required: true
739
+ end
740
+ end
741
+ ```
742
+
743
+ ## 实体定义
744
+
745
+ ### 统一参数和返回值
746
+
747
+ 参数和返回值的定义并不需要割裂开,它们在很多行为上是统一的。现在,我们分别单独定义了参数和返回值:
748
+
749
+ ```ruby
750
+ params do
751
+ param :user do
752
+ param :name, type: 'string'
753
+ param :age, type: 'integer'
754
+ end
755
+ end
756
+
757
+ status 200 do
758
+ expose :user do
759
+ expose :name, type: 'string'
760
+ expose :age, type: 'integer'
761
+ end
762
+ end
763
+ ```
764
+
765
+ 为将上述定义改造,内部的块可以封装在一个实体内定义:
766
+
767
+ ```ruby
768
+ class UserEntity < Meta::Entity
769
+ property :name, type: 'string'
770
+ property :age, type: 'integer'
771
+ end
772
+ ```
773
+
774
+ 然后在 `params` 和 `status` 内部使用 `using` 引用这个实体:
775
+
776
+ ```ruby
777
+ params do
778
+ param :user, using: UserEntity
779
+ end
780
+
781
+ status 200 do
782
+ expose :user, using: UserEntity
783
+ end
784
+ ```
785
+
786
+ 我们通过继承 `Meta::Entity` 类定义了实体并到处引用,从而简化了代码。这在实践中是推荐的方案。鉴于参数、返回值、实体的定义语法是完全一致的,故而接下来我们将重点进入实体的讲解环节。希望读者清楚的是,以上参数介绍的语法在实体定义中是完全可用的;并且,接下来有关实体的语法也能完全运用到单独的参数和返回值定义块中。
787
+
788
+ > **小提示:**我们在 `params` 中用 `param` 命令定义参数字段,在 `status` 中用 `expose` 命令定义返回值字段,而在实体定义中这个命令变成了 `property`. 这里需要阐明的是,用 `param`、`expose` 还是 `property` 只是习惯的不同而已,它们的行为都是一致的并且能够混用。例如,你完全可以在 `params` 和 `status` 中一律使用 `property` 命令:
789
+ >
790
+ > ```ruby
791
+ > params do
792
+ > property :user, using: UserEntity
793
+ > end
794
+ >
795
+ > status 200 do
796
+ > property :user, using: UserEntity
797
+ > end
798
+ > ```
799
+
800
+ ### 实体定义的其他介绍
801
+
802
+ `param` 和 `expose` 的只会作用到同层的字段,不会作用到实体内部。
803
+
804
+ 数组内部也可以引用实体,只要在字段上加上 `type: "array"` 即可:
805
+
806
+ ```ruby
807
+ params do
808
+ param :users, type: "array", using: UserEntity
809
+ end
810
+ ```
811
+
812
+ 接下来会涉及之前没提过的配置选项,包括 `param`、`render`、`scope`、`value`、`convert` 等。单独说明某个选项的用法显得枯燥,我接下来将以列举场景的方式说明。
813
+
814
+ ### 如何设置某个字段只作为参数或返回值
815
+
816
+ 由于实体内部既包括参数的字段,也包括返回值的字段,必然有某些字段只可作为参数或返回值。这种情况该如何做呢?我们可以配置 `param: false` 定义这个字段不可作为参数,另外配置 `render: false` 定义这个字段不可用作返回值。如下是一个例子:
817
+
818
+ ```ruby
819
+ class UserEntity < Meta::Entity
820
+ property :id, param: false # id 字段不可用作参数
821
+ property :name # name 和 age 字段既可用作参数,也可用作返回值
822
+ property :age
823
+ property :password, render: false # password 字段不可用作返回值
824
+ end
825
+ ```
826
+
827
+ 虽然有点啰嗦,上述例子如果接收的参数是:
828
+
829
+ ```ruby
830
+ # HTTP params
831
+ { "id": 1, "name": "Jim", "age": 18, "password": "123456" }
832
+ ```
833
+
834
+ 则程序中获取到的参数内容是:
835
+
836
+ ```ruby
837
+ # p params
838
+ { name: "Jim", age: 18, password: "123456" }
839
+ ```
840
+
841
+ 另外,如果我们渲染了如下的数据:
842
+
843
+ ```ruby
844
+ action do
845
+ render("id" => 1, "name" => "Jim", "age" => 18, "password" => "123456")
846
+ end
847
+ ```
848
+
849
+ 则客户端实际得到的 JSON 格式是:
850
+
851
+ ```ruby
852
+ # HTTP Response
853
+ { "id": 1, "name": "Jim", "age": 18 }
854
+ ```
855
+
856
+ #### 引申:`param` 和 `render` 本质探究
857
+
858
+ `param` 和 `render` 支持两种格式:其一是刚刚见过的 `false`,它将禁用参数或返回值;另一个可传递 Hash,它将设置独属于参数或返回值的选项。例如,我希望只对参数作校验,而返回值不作校验,可如下设配置:
859
+
860
+ ```ruby
861
+ property :name, param: { required: true }
862
+ ```
863
+
864
+ 再比如,我对参数不设置默认值,而返回渲染的时候提供默认值(这个例子比较少见):
865
+
866
+ ```ruby
867
+ property :age, render: { default: 18 }
868
+ ```
869
+
870
+ ### 如何控制不同场景下的字段
871
+
872
+ 上一节讲的是字段如何区分参数或返回值的情况。这是一个方面,另一方面是如何控制在不同接口下的字段返回。
873
+
874
+ 例如,我们定义列表接口时不需要返回详情字段,一来是列表页面用不到,另一来是详情内容会导致返回实体过大而造成网络拥塞。这时,`scope` 选项能够起到作用了。我们定义一个实体 `ArticleEntity`:
875
+
876
+ ```ruby
877
+ class ArticleEntity < Meta::Entity
878
+ property :title
879
+ property :content, render: { scope: 'full' }
880
+ end
881
+ ```
882
+
883
+ > **小提示:**`scope` 选项放在 `render` 下定义,因为参数获取不需要区分场景。
884
+
885
+ 注意到 `content` 被限制了 scope 为 `"full"` 了,默认情况下它是不会返回的。像列表接口就可以直接渲染它:
886
+
887
+ ```ruby
888
+ get '/articles' do
889
+ status 200 do
890
+ expose :articles, using: ArticleEntity
891
+ end
892
+ action do
893
+ articles = Article.all
894
+ render :articles, articles
895
+ end
896
+ end
897
+ ```
898
+
899
+ 而详情接口下需要返回 `content` 字段,需要明确附加一个 `scope` 为 `"full"`:
900
+
901
+ ```ruby
902
+ get '/articles/:id' do
903
+ status 200 do
904
+ expose :article, using: ArticleEntity
905
+ end
906
+ action do
907
+ article = Article.find(request.params['id'])
908
+ render :article, article, scope: "full"
909
+ end
910
+ end
911
+ ```
912
+
913
+ 如果你认为在调用 `render` 方法时较为繁琐,好奇为什么不在声明时定义。如果你更青睐这种方式,可以在声明时控制,方法是将实体锁住。实体被锁住后就不需要在 `render` 时传递任何选项了:
914
+
915
+ ```ruby
916
+ get '/articles/:id' do
917
+ status 200 do
918
+ expose :article, using: ArticleEntity.lock_scope('full')
919
+ end
920
+ action do
921
+ article = Article.find(request.params['id'])
922
+ render :article, article
923
+ end
924
+ end
925
+ ```
926
+
927
+ 也许在参数声明中这种方式更有效果。因为调用参数时我们无法传递 `scope` 选项,锁住是唯一的途径:
928
+
929
+ ```ruby
930
+ post '/articles' do
931
+ params do
932
+ param :article, using: ArticleEntity.lock_scope('on_create')
933
+ end
934
+ ...
935
+ end
936
+
937
+ put '/articles/:id' do
938
+ params do
939
+ param :article, using: ArticleEntity.lock_scope('on_update')
940
+ end
941
+ ...
942
+ end
943
+ ```
944
+
945
+ ### 如何渲染计算出来的结果
946
+
947
+ 假设现有如下实体:
948
+
949
+ ```ruby
950
+ class UserEntity < Meta::Entity
951
+ property :first_name
952
+ property :last_name
953
+ end
954
+ ```
955
+
956
+ 现在我们想要加一个 `full_name` 字段,它是 `first_name` 和 `last_name` 加起来的结果。这时我们可以使用 `value` 选项自己将结果计算下来:
957
+
958
+ ```ruby
959
+ class UserEntity < Meta::Entity
960
+ property :first_name
961
+ property :last_name
962
+ property :full_name, param: false, value: lambda do |user|
963
+ "#{user['first_name']} #{user['last_name']}"
964
+ end
965
+ end
966
+ ```
967
+
968
+ > **小提示:** 设置 `param` 为 `false`,因为参数获取时没有这个字段。
969
+
970
+ `value` 传递的块可以访问到执行环境,以下是一个示例:
971
+
972
+ ```ruby
973
+ status 200 do
974
+ expose :is_admin, value: lambda { @user.admin? }
975
+ end
976
+ action do
977
+ @user = get_user
978
+ end
979
+ ```
980
+
981
+ ### 参数值转化
982
+
983
+ 首先,我们定义实体:
984
+
985
+ ```ruby
986
+ class ArticleEntity < Meta::Entity
987
+ property :image,
988
+ type: 'string',
989
+ description: '客户端传递 Base64 格式的数据'
990
+ end
991
+ ```
992
+
993
+ 然后我们定义路由参数:
994
+
995
+ ```ruby
996
+ params do
997
+ param :article, using: ArticleEntity
998
+ end
999
+ ```
1000
+
1001
+ 由于客户端传递的参数是 Base64 格式的,我们需要在执行环境中将其转化为原本格式:
1002
+
1003
+ ```ruby
1004
+ action do
1005
+ article_params = {
1006
+ **params[:article],
1007
+ image: decode_base64(params[:article][:image])
1008
+ }
1009
+ end
1010
+ ```
1011
+
1012
+ 但每个接口下都做这样的转化工作挺是繁琐,框架提供了 `convert` 选项,它用于将值转化为预期的格式:
1013
+
1014
+ ```ruby
1015
+ class ArticleEntity < Meta::Entity
1016
+ property :image,
1017
+ type: 'string',
1018
+ description: '客户端传递 Base64 格式的数据',
1019
+ param: {
1020
+ convert: lambda { |value| decode_base64(value) }
1021
+ }
1022
+ end
1023
+ ```
1024
+
1025
+ > **小提示:** 注意只应当在参数过程中做格式转换,渲染过程中做同样的转换将会出错。
1026
+
1027
+ 这样执行环境中就不需要手动进行格式转换了:
1028
+
1029
+ ```ruby
1030
+ action do
1031
+ article_params = params[:article]
1032
+ end
1033
+ ```
1034
+
1035
+ ## 生成文档
1036
+
1037
+ 应用模块提供一个 `to_swagger_doc` 方法生成 Open API 规格文档,该文档可被 Swagger UI 或基于 Swagger UI 的引擎渲染。
1038
+
1039
+ ```ruby
1040
+ class DemoApp < Meta::Application
1041
+ end
1042
+
1043
+ # 生成 JSON 格式的规格文档
1044
+ DemoApp.to_swagger_doc(
1045
+ info: {
1046
+ title: 'Web API 示例项目',
1047
+ version: 'current'
1048
+ },
1049
+ servers: [
1050
+ { url: 'http://localhost:9292', description: 'Web API 示例项目' }
1051
+ ]
1052
+ )
1053
+ ```
1054
+
1055
+ 其中 `info` 和 `servers` 选项是 *Open API 规格文档* 中提供。
1056
+
1057
+ > 了解 [Open API 规格文档](https://swagger.io/resources/open-api/https://swagger.io/resources/open-api/)。
1058
+ >
1059
+ > 了解 [Swagger UI](https://swagger.io/tools/swagger-ui/).
1060
+
1061
+ ## 特殊用法举例
1062
+
1063
+ ##
1064
+
1065
+ ### 路由中实体定义的特殊用法
1066
+
1067
+ 虽然推荐的方案是在实体之上包裹一个根字段,像下面这样:
1068
+
1069
+ ```ruby
1070
+ params do
1071
+ param :user, using: UserEntity
1072
+ end
1073
+
1074
+
1075
+ # 接受如下格式的数据
1076
+ { "user": { "name": "Jim", "age": 18 } }
1077
+ ```
1078
+
1079
+ 但也可以将包裹的外层字段去掉,即将 `UserEntity` 直接用在顶层:
1080
+
1081
+ ```ruby
1082
+ params using: UserEntity
1083
+
1084
+
1085
+ # 接受如下格式的数据
1086
+ { "name": "Jim", "age": 18 }
1087
+ ```
1088
+
1089
+ 这个方案同时也支持数组:
1090
+
1091
+ ```ruby
1092
+ params type: 'array', using: UserEntity
1093
+
1094
+ # 接受如下格式的数据
1095
+ [
1096
+ { "name": "Jim", "age": 18 },
1097
+ { "name": "Jack", "age": 19 }
1098
+ ]
1099
+ ```
1100
+
1101
+ 虽然更不常见,标量值也是支持的:
1102
+
1103
+ ```ruby
1104
+ params type: 'string'
1105
+
1106
+ # 接受字符串数据
1107
+ "foo"
1108
+ ```
1109
+
1110
+ ### 完整更新和局部更新
1111
+
1112
+ HTTP 提供了两个方法 `PUT` 和 `PATCH`,它们的语义差别体现在更新策略上。`PUT` 要求是完整更新,`PATCH` 要求是局部更新。
1113
+
1114
+ 假设我们定义参数格式为:
1115
+
1116
+ ```ruby
1117
+ params do
1118
+ param :user do
1119
+ param :name
1120
+ param :age
1121
+ end
1122
+ end
1123
+ ```
1124
+
1125
+ 同时我们收到客户端的参数格式为:
1126
+
1127
+ ```json
1128
+ {
1129
+ "user": {
1130
+ "name": "Jim"
1131
+ }
1132
+ }
1133
+ ```
1134
+
1135
+ `params` 方法默认的逻辑符合完整更新:
1136
+
1137
+ ```ruby
1138
+ put '/users/:id' do
1139
+ action do
1140
+ user = User.find(request.params['id'])
1141
+
1142
+ user_params = params[:user] # => { name: "Jim", age: nil }
1143
+ user.update(user_params)
1144
+ end
1145
+ end
1146
+ ```
1147
+
1148
+ 而 `params(:discard_missing)` 将符合局部更新的逻辑:
1149
+
1150
+ ```ruby
1151
+ patch '/users/:id' do
1152
+ action do
1153
+ user = User.find(request.params['id'])
1154
+
1155
+ user_params = params(:discard_missing)[:user] # => { name: "Jim" }
1156
+ user.update(user_params)
1157
+ end
1158
+ end
1159
+ ```
1160
+
1161
+ > **小提示:**还有一种调用方式 `params(:raw)`,它返回无任何转换逻辑的原生参数。它与 `request.params` 的行为一致。
1162
+
1163
+ > **大提示:**如果你是通过 `using:` 引用一个实体定义,另一个更符合语义的方式是使用 `lock` 方法。
1164
+ >
1165
+ > ```ruby
1166
+ > patch '/users/:id' do
1167
+ > params do
1168
+ > param :user, using: UserEntity.lock(:discard_missing, true)
1169
+ > end
1170
+ > action do
1171
+ > user = User.find(params['id'])
1172
+ >
1173
+ > user_params = params[:user] # 不需要传递 `:discard_missing` 符号了,同样也会返回 `{ name: "Jim" }`
1174
+ > user.update(user_params)
1175
+ > end
1176
+ > end
1177
+ > ```
1178
+
1179
+ ### `namespace` 中使用 `rescue_error Meta::Errors::NoMatchingRoute` 无效
1180
+
1181
+ `Meta::Errors::NoMatchingRoute` 只在顶层捕获有效,在内部捕获无效。
1182
+
1183
+ ```ruby
1184
+ class DemoApp < Meta::Application
1185
+ # 在此捕获有效
1186
+ rescue_error Meta::Errors::NoMatchingRoute do |e|
1187
+ response.status = 404
1188
+ response.body = ["404 Not Found"]
1189
+ end
1190
+
1191
+ namespace '/namespace' do
1192
+ # 在此捕获无效
1193
+ rescue_error Meta::Errors::NoMatchingRoute do |e|
1194
+ response.status = 404
1195
+ response.body = ["404 Not Found"]
1196
+ end
1197
+ end
1198
+ end
1199
+ ```