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.
- checksums.yaml +7 -0
- data/.autoenv.zsh +1 -0
- data/.gitignore +6 -0
- data/.rubocop.yml +28 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +66 -0
- data/LICENSE.txt +502 -0
- data/README.md +149 -0
- data/Rakefile +3 -0
- data/config/locales/zh-CN.yml +6 -0
- data/docs//345/220/215/347/247/260/347/224/261/346/235/245.md +7 -0
- data/docs//346/225/231/347/250/213.md +1199 -0
- data/docs//347/264/242/345/274/225.md +173 -0
- data/examples/lobster.rb +71 -0
- data/examples/rack_app/README.md +3 -0
- data/examples/rack_app/config.ru +6 -0
- data/examples/rack_app/hello.rb +6 -0
- data/examples/rack_app/timing.rb +15 -0
- data/lib/meta/api.rb +3 -0
- data/lib/meta/application/application.rb +63 -0
- data/lib/meta/application/execution.rb +178 -0
- data/lib/meta/application/meta.rb +71 -0
- data/lib/meta/application/path_matching_mod.rb +53 -0
- data/lib/meta/application/route.rb +58 -0
- data/lib/meta/application.rb +42 -0
- data/lib/meta/entity.rb +59 -0
- data/lib/meta/errors.rb +29 -0
- data/lib/meta/json_schema/builders/array_schema_builder.rb +29 -0
- data/lib/meta/json_schema/builders/object_schema_builder.rb +120 -0
- data/lib/meta/json_schema/builders/schema_builder_tool.rb +29 -0
- data/lib/meta/json_schema/schemas/array_schema.rb +40 -0
- data/lib/meta/json_schema/schemas/base_schema.rb +110 -0
- data/lib/meta/json_schema/schemas/object_schema.rb +161 -0
- data/lib/meta/json_schema/schemas.rb +12 -0
- data/lib/meta/json_schema/support/errors.rb +38 -0
- data/lib/meta/json_schema/support/presenters.rb +35 -0
- data/lib/meta/json_schema/support/schema_options.rb +55 -0
- data/lib/meta/json_schema/support/type_converter.rb +137 -0
- data/lib/meta/json_schema/support/validators.rb +54 -0
- data/lib/meta/load_i18n.rb +8 -0
- data/lib/meta/route_dsl/action_builder.rb +15 -0
- data/lib/meta/route_dsl/application_builder.rb +108 -0
- data/lib/meta/route_dsl/chain_builder.rb +48 -0
- data/lib/meta/route_dsl/helpers.rb +15 -0
- data/lib/meta/route_dsl/meta_builder.rb +57 -0
- data/lib/meta/route_dsl/parameters_builder.rb +24 -0
- data/lib/meta/route_dsl/route_builder.rb +85 -0
- data/lib/meta/route_dsl/uniformed_params_builder.rb +34 -0
- data/lib/meta/swagger_doc.rb +86 -0
- data/lib/meta/utils/path.rb +20 -0
- data/meta-api.gemspec +23 -0
- metadata +96 -0
@@ -0,0 +1,173 @@
|
|
1
|
+
# 索引
|
2
|
+
|
3
|
+
## 属性校验
|
4
|
+
|
5
|
+
### `validate`
|
6
|
+
|
7
|
+
通用的校验器,可传递一个自定义的块。注意,如果校验不通过,需要主动地抛出 `Meta::JsonSchema::ValidationError`. 例如:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
property :mobile, validate: lambda do |value|
|
11
|
+
raise Meta::JsonSchema::ValidationError, '手机号格式不正确' unless value =~ /\d+/
|
12
|
+
end
|
13
|
+
```
|
14
|
+
|
15
|
+
### `required`
|
16
|
+
|
17
|
+
有关必须要传递的字段可通过 `required: true` 设置。
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
property :name, required: true # 必须
|
21
|
+
property :age, required: false # 非必须
|
22
|
+
```
|
23
|
+
|
24
|
+
对于字符串属性,即使是空白字符串也无法通过 `required` 校验。这时可通过设置 `allow_empty: true` 来改变这一行为。
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
property :name, required: { allow_empty: true }
|
28
|
+
```
|
29
|
+
|
30
|
+
数组属性默认是允许空数组的。如果你不喜欢这种行为,需明确设置 `allow_empty` 为 `false`.
|
31
|
+
|
32
|
+
### `format`
|
33
|
+
|
34
|
+
`format` 校验可传递一个正则表达式选项,参数需匹配正则表达式代表的模式。
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
property :date, format: /^\d{4}-\d{2}-\d{2}$/
|
38
|
+
```
|
39
|
+
|
40
|
+
### `allowable`
|
41
|
+
|
42
|
+
对于枚举值来说,通过 `allowable` 来定义可选项。
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
property :gender, allowable: ['male', 'female']
|
46
|
+
```
|
47
|
+
|
48
|
+
如果用户传递了不同于 `male` 或 `female` 的值,就会报错。
|
49
|
+
|
50
|
+
## 错误列表
|
51
|
+
|
52
|
+
框架中使用 `rescue_error` 捕获异常,框架能够抛出的异常包括:
|
53
|
+
|
54
|
+
- `Meta::Errors::NoMatchingRoute`:路由不匹配时。
|
55
|
+
- `Meta::Errors::ParameterInvalid`:参数存在异常时。
|
56
|
+
- `Meta::Errors::RenderingInvalid`:响应值存在异常时。
|
57
|
+
- `Meta::Errors::UnsupportedContentType`:框架只支持 `application/json` 的参数格式。当客户端的请求体不是这个格式时,会抛出这个错误。
|
58
|
+
|
59
|
+
## I18n
|
60
|
+
|
61
|
+
框架默认提供的语言是 `zh-CN`,可通过文件 `config/locales/zh-CN.yml` 查看可配置的内容。如果需要添加其他语言的支持,需要自行配置。
|
62
|
+
|
63
|
+
## `lock` 系列方法
|
64
|
+
|
65
|
+
`locked` 方法返回一个新的实体,它将若干选项锁定。例如,锁定某个实体为 `scope: 'full'`:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
# 如下定义一个 Entity
|
69
|
+
class ArticleEntity < Meta::Entity
|
70
|
+
property :title, type: 'string'
|
71
|
+
property :content, type: 'string', scope: 'full'
|
72
|
+
property :hidden, type: 'boolean', scope: 'o'
|
73
|
+
end
|
74
|
+
|
75
|
+
# 在 using 时锁定 scope
|
76
|
+
params do
|
77
|
+
property :article, using: ArticleEntity.locked(scope: 'full')
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
如上,`article` 内容仅包含字段 `title`、`content`.
|
82
|
+
|
83
|
+
再例如,我们也可以去掉某些指定的字段,这时用到 `exclude: ...` 选项。下面的例子将达到与上面例子等效的结果:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
# 在 using 时去掉某些字段
|
87
|
+
params do
|
88
|
+
property :article, using: ArticleEntity.locked(exclude: [:hidden])
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
注意 `exclude` 的传递格式,它接受一个数组,并且字段应用符号而不是字符串。
|
93
|
+
|
94
|
+
最后一个示例是如何处理缺失字段的。在处理请求参数时,我们有时候需要包括缺失字段,有时候需要去掉缺失字段。默认情况下是按照前者处理的,如果需要按照后者的方式处理,需要锁定选项 `discard_missing: true`:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
# 在 using 时不包括缺失字段
|
98
|
+
params do
|
99
|
+
property :article, using: ArticleEntity.locked(discard_missing: true)
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
我们也可以综合这几个选项(由于综合 `scope` 和 `exclude` 往往没有意义,这里我们只综合 `scope` 和 `discard_missing`):
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
params do
|
107
|
+
property :article, using: ArticleEntity.locked(scope: ['full'], discard_missing: true)
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
如上,`scope` 选项也可以传递数组的。
|
112
|
+
|
113
|
+
除了 `locked` 方法,还包括几个便捷方法。例如 `lock` 方法,它与 `locked` 在某种程度上等价:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
ArticleEntity.lock(:scope, 'full') # 它与 locked(scope: 'full') 等价
|
117
|
+
ArticleEntity.lock(:discard_missing, true) # 它与 locked(discard_missing: true) 等价
|
118
|
+
```
|
119
|
+
|
120
|
+
还有 `lock_xxx` 便捷方法:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
ArticleEntity.lock_scope('full') # 它与 locked(scope: 'full') 等价
|
124
|
+
```
|
125
|
+
|
126
|
+
## `parameters` 、`request_body` 和 Open API 规格定义
|
127
|
+
|
128
|
+
在 Open API 中,参数和请求体是分开定义的,它们不属于同一个语义。下面是一个 HTTP 请求的示例:
|
129
|
+
|
130
|
+
```http
|
131
|
+
POST /request?foo=foo
|
132
|
+
X-Bar: bar
|
133
|
+
|
134
|
+
{
|
135
|
+
"user": ...
|
136
|
+
}
|
137
|
+
```
|
138
|
+
|
139
|
+
其中 `foo=foo` 和 `X-Bar: bar` 是参数,而 JSON 格式的 `{ "user": ... }` 才是请求体。
|
140
|
+
|
141
|
+
在 Meta 框架中,为了方便,可以用同一个宏命令(即 `params`)来定义参数和请求体:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
params do
|
145
|
+
param :foo, in: 'query'
|
146
|
+
param 'X-Bar', in: 'header'
|
147
|
+
param :user, type: 'object'
|
148
|
+
end
|
149
|
+
```
|
150
|
+
|
151
|
+
而事实上,Meta 框架也给出了分开定义参数和请求体的的宏命令。也许有些混淆,`parameters` 宏专用于定义参数,`request_body` 宏用于定义请求体:
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
parameters do
|
155
|
+
param :foo, in: 'query'
|
156
|
+
param 'X-Bar', in: 'header'
|
157
|
+
end
|
158
|
+
request_body do
|
159
|
+
property :user, type: 'object'
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
而在应用中,也许有些混淆,`params`、`parameters`、`request_body` 能够获取到参数和请求体:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
action do
|
167
|
+
params # 合并获取参数和请求体
|
168
|
+
parameters # 专用于获取参数
|
169
|
+
request_body # 专用于获取请求体
|
170
|
+
end
|
171
|
+
```
|
172
|
+
|
173
|
+
面对这两种使用方式,有何使用上的建议呢?我的建议是,你习惯用哪种就用哪种。
|
data/examples/lobster.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# 一个示例 Rack 应用,运行方式:`ruby -Ilib <this-file>`
|
4
|
+
|
5
|
+
require 'zlib'
|
6
|
+
|
7
|
+
# Paste has a Pony, Rack has a Lobster!
|
8
|
+
class Lobster
|
9
|
+
LobsterString = Zlib::Inflate.inflate("eJx9kEEOwyAMBO99xd7MAcytUhPlJyj2
|
10
|
+
P6jy9i4k9EQyGAnBarEXeCBqSkntNXsi/ZCvC48zGQoZKikGrFMZvgS5ZHd+aGWVuWwhVF0
|
11
|
+
t1drVmiR42HcWNz5w3QanT+2gIvTVCiE1lm1Y0eU4JGmIIbaKwextKn8rvW+p5PIwFl8ZWJ
|
12
|
+
I8jyiTlhTcYXkekJAzTyYN6E08A+dk8voBkAVTJQ==".delete("\n ").unpack("m*")[0])
|
13
|
+
|
14
|
+
LambdaLobster = lambda { |env|
|
15
|
+
if env[Rack::QUERY_STRING].include?("flip")
|
16
|
+
lobster = LobsterString.split("\n").
|
17
|
+
map { |line| line.ljust(42).reverse }.
|
18
|
+
join("\n")
|
19
|
+
href = "?"
|
20
|
+
else
|
21
|
+
lobster = LobsterString
|
22
|
+
href = "?flip"
|
23
|
+
end
|
24
|
+
|
25
|
+
content = ["<title>Lobstericious!</title>",
|
26
|
+
"<pre>", lobster, "</pre>",
|
27
|
+
"<a href='#{href}'>flip!</a>"]
|
28
|
+
length = content.inject(0) { |a, e| a + e.size }.to_s
|
29
|
+
[200, { Rack::CONTENT_TYPE => "text/html", Rack::CONTENT_LENGTH => length }, content]
|
30
|
+
}
|
31
|
+
|
32
|
+
def call(env)
|
33
|
+
req = Rack::Request.new(env)
|
34
|
+
if req.GET["flip"] == "left"
|
35
|
+
lobster = LobsterString.split("\n").map do |line|
|
36
|
+
line.ljust(42).reverse.
|
37
|
+
gsub('\\', 'TEMP').
|
38
|
+
gsub('/', '\\').
|
39
|
+
gsub('TEMP', '/').
|
40
|
+
gsub('{', '}').
|
41
|
+
gsub('(', ')')
|
42
|
+
end.join("\n")
|
43
|
+
href = "?flip=right"
|
44
|
+
elsif req.GET["flip"] == "crash"
|
45
|
+
raise "Lobster crashed"
|
46
|
+
else
|
47
|
+
lobster = LobsterString
|
48
|
+
href = "?flip=left"
|
49
|
+
end
|
50
|
+
|
51
|
+
res = Rack::Response.new
|
52
|
+
res.write "<title>Lobstericious!</title>"
|
53
|
+
res.write "<pre>"
|
54
|
+
res.write lobster
|
55
|
+
res.write "</pre>"
|
56
|
+
res.write "<p><a href='#{href}'>flip!</a></p>"
|
57
|
+
res.write "<p><a href='?flip=crash'>crash!</a></p>"
|
58
|
+
res.finish
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
if $0 == __FILE__
|
63
|
+
# :nocov:
|
64
|
+
require 'rack'
|
65
|
+
|
66
|
+
app = Lobster.new # or Lobster::LambdaLobster
|
67
|
+
Rack::Server.start(
|
68
|
+
app: Rack::ShowExceptions.new(Rack::Lint.new(app)), Port: 9292
|
69
|
+
)
|
70
|
+
# :nocov:
|
71
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Timing
|
2
|
+
def initialize(app)
|
3
|
+
@app = app
|
4
|
+
end
|
5
|
+
|
6
|
+
def call(env)
|
7
|
+
env['Timing-Start'] = Time.now.to_i
|
8
|
+
|
9
|
+
ts = Time.now
|
10
|
+
status, headers, body = @app.call(env)
|
11
|
+
elapsed_time = Time.now - ts
|
12
|
+
puts "Timing: #{env['REQUEST_METHOD']} #{env['REQUEST_URI']} #{elapsed_time.round(3)}"
|
13
|
+
return [status, headers, body]
|
14
|
+
end
|
15
|
+
end
|
data/lib/meta/api.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../utils/path'
|
4
|
+
|
5
|
+
module Meta
|
6
|
+
class Application
|
7
|
+
include Execution::MakeToRackMiddleware
|
8
|
+
include PathMatchingMod.new(path_method: :prefix, matching_mode: :prefix)
|
9
|
+
|
10
|
+
attr_reader :prefix, :mods, :error_guards
|
11
|
+
|
12
|
+
def initialize(prefix: '', mods: [], shared_mods: [], error_guards: [])
|
13
|
+
@prefix = Utils::Path.normalize_path(prefix)
|
14
|
+
@mods = mods
|
15
|
+
@shared_mods = shared_mods
|
16
|
+
@error_guards = error_guards
|
17
|
+
end
|
18
|
+
|
19
|
+
def execute(execution, remaining_path = '')
|
20
|
+
remaining_path_for_children = path_matching.merge_path_params(remaining_path, execution.request)
|
21
|
+
|
22
|
+
@shared_mods.each { |mod| execution.singleton_class.include(mod) }
|
23
|
+
|
24
|
+
mod = find_child_mod(execution, remaining_path_for_children)
|
25
|
+
if mod
|
26
|
+
mod.execute(execution, remaining_path_for_children)
|
27
|
+
else
|
28
|
+
request = execution.request
|
29
|
+
raise Errors::NoMatchingRoute, "未能发现匹配的路由:#{request.request_method} #{request.path}"
|
30
|
+
end
|
31
|
+
rescue StandardError => e
|
32
|
+
guard = error_guards.find { |g| e.is_a?(g[:error_class]) }
|
33
|
+
raise unless guard
|
34
|
+
|
35
|
+
execution.instance_exec(e, &guard[:caller])
|
36
|
+
end
|
37
|
+
|
38
|
+
def match?(execution, remaining_path)
|
39
|
+
return false unless path_matching.match?(remaining_path)
|
40
|
+
|
41
|
+
remaining_path_for_children = path_matching.capture_remaining_part(remaining_path)
|
42
|
+
find_child_mod(execution, remaining_path_for_children) != nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def applications
|
46
|
+
mods.filter { |r| r.is_a?(Application) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def routes
|
50
|
+
mods.filter { |r| r.is_a?(Route) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_swagger_doc(options = {})
|
54
|
+
SwaggerDocUtil.generate(self, **options)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def find_child_mod(execution, remaining_path_for_children)
|
60
|
+
mods.find { |mod| mod.match?(execution, remaining_path_for_children) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
|
5
|
+
module Meta
|
6
|
+
class Execution
|
7
|
+
attr_reader :request, :response, :params_schema, :request_body_schema
|
8
|
+
attr_accessor :parameters
|
9
|
+
|
10
|
+
def initialize(request)
|
11
|
+
@request = request
|
12
|
+
@response = Rack::Response.new
|
13
|
+
@parameters = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# 调用方式:
|
17
|
+
#
|
18
|
+
# - `request_body`:等价于 request_body(:keep_missing)
|
19
|
+
# - `request_body(:keep_missing)`
|
20
|
+
# - `request_body(:discard_missing)`
|
21
|
+
def request_body(mode = :keep_missing)
|
22
|
+
@_request_body ||= {}
|
23
|
+
|
24
|
+
case mode
|
25
|
+
when :keep_missing
|
26
|
+
@_request_body[:keep_missing] || @_request_body[:keep_missing] = parse_request_body_for_replacing.freeze
|
27
|
+
when :discard_missing
|
28
|
+
@_request_body[:discard_missing] || @_request_body[:discard_missing] = parse_request_body_for_updating.freeze
|
29
|
+
else
|
30
|
+
raise NameError, "未知的 mode 参数:#{mode}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# 调用方式:
|
35
|
+
#
|
36
|
+
# - `params`:等价于 params(:keep_missing)
|
37
|
+
# - `params(:keep_missing)`
|
38
|
+
# - `params(:discard_missing)`
|
39
|
+
# - `params(:raw)`
|
40
|
+
def params(mode = :keep_missing)
|
41
|
+
@_params ||= {}
|
42
|
+
return @_params[mode] if @_params.key?(mode)
|
43
|
+
|
44
|
+
if mode == :raw
|
45
|
+
@_params[:raw] = parse_raw_params.freeze
|
46
|
+
else
|
47
|
+
params = parameters
|
48
|
+
params = params.merge(request_body(mode) || {}) if @request_body_schema
|
49
|
+
@_params[mode] = params
|
50
|
+
end
|
51
|
+
|
52
|
+
@_params[mode]
|
53
|
+
end
|
54
|
+
|
55
|
+
# 调用方式:
|
56
|
+
#
|
57
|
+
# - render(value, options?)
|
58
|
+
# - render(key, value, options?)
|
59
|
+
def render(*params)
|
60
|
+
if (params.length < 1 || params.length > 3)
|
61
|
+
raise ArgumentError, "wrong number of arguments (given #{params.length} expected 1..3)"
|
62
|
+
elsif params[0].is_a?(Symbol)
|
63
|
+
key, value, options = params
|
64
|
+
else
|
65
|
+
key = :__root__
|
66
|
+
value, options = params
|
67
|
+
end
|
68
|
+
|
69
|
+
@renders ||= {}
|
70
|
+
@renders[key] = { value: value, options: options || {} }
|
71
|
+
end
|
72
|
+
|
73
|
+
# 运行过程中首先会解析参数
|
74
|
+
def parse_parameters(parameters_meta)
|
75
|
+
self.parameters = parameters_meta.map do |name, options|
|
76
|
+
schema = options[:schema]
|
77
|
+
value = if options[:in] == 'header'
|
78
|
+
schema.filter(request.get_header('HTTP_' + name.to_s.upcase.gsub('-', '_')))
|
79
|
+
else
|
80
|
+
schema.filter(request.params[name.to_s])
|
81
|
+
end
|
82
|
+
[name, value]
|
83
|
+
end.to_h.freeze
|
84
|
+
end
|
85
|
+
|
86
|
+
# REVIEW: parse_params 不再解析参数了,而只是设置 @params_schema,并清理父路由解析的变量
|
87
|
+
def parse_params(params_schema)
|
88
|
+
@params_schema = params_schema
|
89
|
+
end
|
90
|
+
|
91
|
+
def parse_request_body(schema)
|
92
|
+
@request_body_schema = schema
|
93
|
+
end
|
94
|
+
|
95
|
+
def render_entity(entity_schema)
|
96
|
+
# 首先获取 JSON 响应值
|
97
|
+
renders = @renders || {}
|
98
|
+
|
99
|
+
if renders.key?(:__root__) || renders.empty?
|
100
|
+
# 从 root 角度获取
|
101
|
+
if renders[:__root__]
|
102
|
+
hash = renders[:__root__][:value]
|
103
|
+
options = renders[:__root__][:options]
|
104
|
+
else
|
105
|
+
response_body = response.body ? response.body[0] : nil
|
106
|
+
hash = response_body ? JSON.parse(response_body) : {}
|
107
|
+
options = {}
|
108
|
+
end
|
109
|
+
|
110
|
+
new_hash = entity_schema.filter(hash, **options, execution: self, stage: :render)
|
111
|
+
response.body = [JSON.generate(new_hash)]
|
112
|
+
else
|
113
|
+
# 渲染多键值结点
|
114
|
+
new_hash = renders.map do |key, render_content|
|
115
|
+
schema = entity_schema.properties[key]
|
116
|
+
raise Errors::RenderingError, "渲染的键名 `#{key}` 不存在,请检查实体定义以确认是否有拼写错误" if schema.nil?
|
117
|
+
|
118
|
+
filtered = schema.filter(render_content[:value], **render_content[:options], execution: self, stage: :render)
|
119
|
+
[key, filtered]
|
120
|
+
end.to_h
|
121
|
+
response.body = [JSON.generate(new_hash)]
|
122
|
+
end
|
123
|
+
rescue JsonSchema::ValidationErrors => e
|
124
|
+
raise Errors::RenderingInvalid.new(e.errors)
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def parse_raw_params
|
130
|
+
request_body = request.body.read
|
131
|
+
if request_body.empty?
|
132
|
+
json = {}
|
133
|
+
elsif !request.content_type.start_with?('application/json')
|
134
|
+
raise Errors::UnsupportedContentType, "只接受 Content-Type 为 application/json 的请求参数"
|
135
|
+
else
|
136
|
+
json = JSON.parse(request_body)
|
137
|
+
end
|
138
|
+
json.merge!(request.params) if json.is_a?(Hash)
|
139
|
+
|
140
|
+
request.body.rewind
|
141
|
+
json
|
142
|
+
end
|
143
|
+
|
144
|
+
def parse_request_body_for_replacing
|
145
|
+
begin
|
146
|
+
request_body_schema.filter(params(:raw), stage: :param)
|
147
|
+
rescue JsonSchema::ValidationErrors => e
|
148
|
+
raise Errors::ParameterInvalid.new(e.errors)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def parse_request_body_for_updating
|
153
|
+
begin
|
154
|
+
request_body_schema.filter(params(:raw), stage: :param, discard_missing: true)
|
155
|
+
rescue JsonSchema::ValidationErrors => e
|
156
|
+
raise Errors::ParameterInvalid.new(e.errors)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
class Abort < StandardError
|
161
|
+
end
|
162
|
+
|
163
|
+
# 使得能够处理 Execution 的类作为 Rack 中间件
|
164
|
+
module MakeToRackMiddleware
|
165
|
+
def call(env)
|
166
|
+
# 初始化一个执行环境
|
167
|
+
request = Rack::Request.new(env)
|
168
|
+
execution = Execution.new(request)
|
169
|
+
|
170
|
+
execute(execution, request.path)
|
171
|
+
|
172
|
+
response = execution.response
|
173
|
+
response.content_type = 'application/json' unless response.no_content?
|
174
|
+
response.to_a
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Meta
|
4
|
+
class Meta
|
5
|
+
attr_reader :title, :description, :tags, :parameters, :request_body, :responses
|
6
|
+
|
7
|
+
def initialize(title: nil, description: nil, tags: [], parameters: {}, request_body: nil, responses: nil)
|
8
|
+
@title = title
|
9
|
+
@description = description
|
10
|
+
@tags = tags
|
11
|
+
@parameters = parameters
|
12
|
+
@request_body = request_body
|
13
|
+
@responses = responses || { 204 => nil }
|
14
|
+
end
|
15
|
+
|
16
|
+
def [](key)
|
17
|
+
send(key)
|
18
|
+
end
|
19
|
+
|
20
|
+
def generate_operation_doc(schemas)
|
21
|
+
operation_object = {}
|
22
|
+
|
23
|
+
operation_object[:summary] = title if title
|
24
|
+
operation_object[:tags] = tags unless tags.empty?
|
25
|
+
operation_object[:description] = description if description
|
26
|
+
|
27
|
+
operation_object[:parameters] = parameters.map do |name, options|
|
28
|
+
property_options = options[:schema].param_options
|
29
|
+
{
|
30
|
+
name: name,
|
31
|
+
in: options[:in],
|
32
|
+
required: property_options[:required] || nil,
|
33
|
+
description: property_options[:description] || '',
|
34
|
+
schema: {
|
35
|
+
type: property_options[:type]
|
36
|
+
}
|
37
|
+
}.compact
|
38
|
+
end unless parameters.empty?
|
39
|
+
|
40
|
+
if request_body
|
41
|
+
schema = request_body.to_schema_doc(stage: :param, schemas: schemas)
|
42
|
+
if schema || true
|
43
|
+
operation_object[:requestBody] = {
|
44
|
+
content: {
|
45
|
+
'application/json' => {
|
46
|
+
schema: schema
|
47
|
+
}
|
48
|
+
}
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
operation_object[:responses] = responses.transform_values do |schema|
|
54
|
+
{
|
55
|
+
description: '', # description 属性必须存在
|
56
|
+
content: schema ? {
|
57
|
+
'application/json' => {
|
58
|
+
schema: schema.to_schema_doc(stage: :render, schemas: schemas)
|
59
|
+
}
|
60
|
+
} : nil
|
61
|
+
}.compact
|
62
|
+
end unless responses.empty?
|
63
|
+
|
64
|
+
operation_object
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.new(meta = {})
|
68
|
+
meta.is_a?(Meta) ? meta : super(**meta)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Meta
|
2
|
+
class PathMatchingMod < Module
|
3
|
+
def initialize(path_method: :path, matching_mode: :full)
|
4
|
+
@path_method = path_method
|
5
|
+
@matching_mode = matching_mode
|
6
|
+
end
|
7
|
+
|
8
|
+
def included(base)
|
9
|
+
path_method = @path_method
|
10
|
+
matching_mode = @matching_mode
|
11
|
+
|
12
|
+
base.class_eval do
|
13
|
+
define_method(:path_matching) do
|
14
|
+
@_path_matching ||= PathMatching.new(send(path_method), matching_mode)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class PathMatching
|
21
|
+
def initialize(path_schema, matching_mode)
|
22
|
+
raw_regex = path_schema
|
23
|
+
.gsub(/:(\w+)/, '(?<\1>[^/]+)')
|
24
|
+
.gsub(/\*(\w+)/, '(?<\1>.+)')
|
25
|
+
.gsub(/:/, '[^/]+').gsub('*', '.+')
|
26
|
+
@path_matching_regex = matching_mode == :prefix ? Regexp.new("^#{raw_regex}") : Regexp.new("^#{raw_regex}$")
|
27
|
+
end
|
28
|
+
|
29
|
+
def match?(real_path)
|
30
|
+
@path_matching_regex.match?(real_path)
|
31
|
+
end
|
32
|
+
|
33
|
+
def merge_path_params(path, request)
|
34
|
+
path_params, remaining_path = capture_both(path)
|
35
|
+
path_params.each { |name, value| request.update_param(name, value) }
|
36
|
+
remaining_path
|
37
|
+
end
|
38
|
+
|
39
|
+
def capture_both(real_path)
|
40
|
+
real_path = '' if real_path == '/'
|
41
|
+
m = @path_matching_regex.match(real_path)
|
42
|
+
[m.named_captures, m.post_match]
|
43
|
+
end
|
44
|
+
|
45
|
+
def capture_named_params(real_path)
|
46
|
+
capture_both(real_path)[0]
|
47
|
+
end
|
48
|
+
|
49
|
+
def capture_remaining_part(real_path)
|
50
|
+
capture_both(real_path)[1]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|