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,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
+ 面对这两种使用方式,有何使用上的建议呢?我的建议是,你习惯用哪种就用哪种。
@@ -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,3 @@
1
+ # Demo Rack Application
2
+
3
+ 一个示例 Rack 应用,可以看到中间件的用法以及它们如何交互。
@@ -0,0 +1,6 @@
1
+ require 'rack'
2
+ require './hello.rb'
3
+ require './timing.rb'
4
+
5
+ use Timing
6
+ run Hello
@@ -0,0 +1,6 @@
1
+ class Hello
2
+ def self.call(env)
3
+ puts "Timing Start: #{env['Timing-Start']}"
4
+ ['200', {'Content-Type' => 'text/html'}, ['Hello, Rack!']]
5
+ end
6
+ 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,3 @@
1
+ require_relative 'application'
2
+ require_relative 'swagger_doc'
3
+ require_relative 'load_i18n'
@@ -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