meta-api 0.0.6 → 0.0.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b4385e82bdaec80e15adc4b62b1259f30125720452e579d84a45c79471c2d9cb
4
- data.tar.gz: f3b9baa683d4a757eabbcc9c13e86f133741f941f75c92bf8e28999d17f29653
3
+ metadata.gz: c114ab0d95b902ecefcd5e720f0c38fbcc420b2839194842c2974d2d073505fd
4
+ data.tar.gz: 994b7e975626f9907467edfe4283875aa113957edc860b74ed1b29e27560f196
5
5
  SHA512:
6
- metadata.gz: 29e7fa33cae933c185cad5ea6bc5e0a9e460f549b5ea8bd12a6217b93e473d61a9cd5eaba2f803c28a5a55710a3668db80c42ad656b526cfb8604e50cdb42721
7
- data.tar.gz: c7774b6feb64e9646017a7034082ab81b9fcf9a3928450edd725192671126a472941da314f64eb5c798a253eec6c82a99ff0701db6eacbda31cff42b6a605de0
6
+ metadata.gz: 6b62b24879f309c6cc67e45bc9bfd250b47ec8ad6c25f5fe1e9f5098b970ff8466f239b8799b4e653ebdc5568cbbb87fdd7c086703a99ef8525945ce24c44617
7
+ data.tar.gz: edec25f4ea37588611af7dceca41a443c5cb6d83586096f51f9f27ce75fdf290ec7085e4f27bf87043779e1ed289f7cbaa0a7e9568839e2ed206eef1d7d4f4b7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # 更新日志
2
2
 
3
+ ## 0.0.7(2023 年 7 月 14 日)
4
+
5
+ 1. 定义 parameters 宏时能够自动识别 `path` 参数。
6
+ 2. 定义 params 宏时能够自动识别 `GET` 路由,此时参数的 `in` 默认为 `query`.
7
+ 3. JsonSchema: `default:` 选项可以是一个块。
8
+ 4. 有且只有一个 `status` 宏定义时,不需要显示地设置 `response.status`.
9
+ 5. `Meta.config` 添加一个新的选析 `default_locked_scope`,借助它可以设置一个默认的 `locked_scope` 值。
10
+ 6. `JsonSchema` 的 `filter` 方法添加一个新的选项 `extra_properties:`,当设定值为 `:ignore` 时可以允许额外的属性。
11
+ 7. 添加新的选项 `config.json_schema_user_options`、`config.json_schema_param_stage_options`、`config.json_schema_render_stage_options`. 借助这三个选项可以对 `JsonSchema#filter` 方法的选项进行设置。同时废弃了 `render_type_conversion`、`render_validation` 等零散的选项。
12
+ 8. `meta` 宏的父级、子级的合并规则调整:parameters、params、responses 都有所合并。
13
+
3
14
  ## 0.0.6(2023 年 5 月 26 日)
4
15
 
5
16
  1. 添加了 Meta::Execution#abort_execution! 方法。
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- meta-api (0.0.5)
4
+ meta-api (0.0.7)
5
5
 
6
6
  GEM
7
7
  remote: https://gems.ruby-china.com/
data/README.md CHANGED
@@ -17,7 +17,7 @@ $ git clone https://github.com/yetrun/web-frame-example.git
17
17
  在 Gemfile 中添加:
18
18
 
19
19
  ```ruby
20
- gem 'meta-api', '~> 0.0.5' # Meta 框架处于快速开发阶段,引入时应尽量固定版本
20
+ gem 'meta-api', '~> 0.0.7' # Meta 框架处于快速开发阶段,引入时应尽量固定版本
21
21
  ```
22
22
 
23
23
  然后在 Ruby 代码中引用:
@@ -6,7 +6,7 @@ zh-CN:
6
6
  errors:
7
7
  required: "未提供"
8
8
  format: "格式不正确"
9
- allowable: "不在允许的值范围内"
9
+ allowable: "不在允许的值范围内,允许的值包括 [%{allowable_values}]"
10
10
  type_convert:
11
11
  basic: "类型转化失败,期望得到一个 `%{target_type}` 类型,但值 `%{value}` 无法转化"
12
12
  array: "转化为数组类型时期望对象拥有 `to_a` 方法"
@@ -1064,7 +1064,8 @@ end
1064
1064
 
1065
1065
  ```ruby
1066
1066
  params do
1067
- param :age, default: 18
1067
+ param :age, default: 18 # 通过值设定
1068
+ param :name, default: -> { 'Jim' } # 通过块设定
1068
1069
  end
1069
1070
  ```
1070
1071
 
@@ -1451,15 +1452,62 @@ DemoApp.to_swagger_doc(
1451
1452
 
1452
1453
  ## 全局配置
1453
1454
 
1454
- ### 关闭渲染时验证
1455
+ ### 定义默认的 `locked_scope`
1455
1456
 
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` 方案**
1457
1494
 
1458
1495
  ```ruby
1459
- Meta.config.render_type_conversion = false # 渲染时不执行类型转换
1460
- Meta.config.render_validation = false # 渲染时不执行数据验证
1496
+ Meta.config.json_schema_user_options = {
1497
+ discard_missing: true
1498
+ }
1461
1499
  ```
1462
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
+
1463
1511
  ## 特殊用法举例
1464
1512
 
1465
1513
  ### 路由中实体定义的特殊用法
@@ -171,3 +171,13 @@ end
171
171
  ```
172
172
 
173
173
  面对这两种使用方式,有何使用上的建议呢?我的建议是,你习惯用哪种就用哪种。
174
+
175
+ ## `JsonSchema#filter` 方法的用户选项
176
+
177
+ ### `discard_missing:`
178
+
179
+ 规定是否忽略缺失的属性。所谓缺失的属性,是指在 `ObjectSchema` 实体宏中定义,但数据中不包含这个键值的属性。你可以将 `discard_missing: true` 视为 HTTP Patch 方法,`discard_missing: false` 视为 HTTP Put 方法。
180
+
181
+ ### `extra_properties:`
182
+
183
+ 规定多余的属性的处理办法。所谓多余的属性,是指未在 `ObjectSchema` 实体宏中定义,但数据中依然存在这个键值的属性。一般来讲我们允许前端传递一些多余的属性,但可能在内部测试时设定为更严格的条件。
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: /home/run/workspace/personal/web-frame
3
3
  specs:
4
- meta-api (0.0.4)
4
+ meta-api (0.0.6)
5
5
 
6
6
  GEM
7
7
  remote: https://gems.ruby-china.com/
@@ -9,7 +9,7 @@ module Meta
9
9
 
10
10
  def initialize(request)
11
11
  @request = request
12
- @response = Rack::Response.new
12
+ @response = Rack::Response.new([], 0) # 状态码初始为 0,代表一个未设置状态
13
13
  @parameters = {}
14
14
  end
15
15
 
@@ -75,11 +75,6 @@ module Meta
75
75
  self.parameters = parameters_meta.filter(request).freeze
76
76
  end
77
77
 
78
- # parse_params 不再解析参数了,而只是设置 @params_schema,并清理父路由解析的变量
79
- def parse_params(params_schema)
80
- @params_schema = params_schema
81
- end
82
-
83
78
  def parse_request_body(schema)
84
79
  @request_body_schema = schema
85
80
  end
@@ -99,7 +94,12 @@ module Meta
99
94
  options = {}
100
95
  end
101
96
 
102
- new_hash = entity_schema.filter(hash, **options, execution: self, stage: :render, validation: ::Meta.config.render_validation, type_conversion: ::Meta.config.render_type_conversion)
97
+ new_hash = entity_schema.filter(hash,
98
+ **Meta.config.json_schema_user_options,
99
+ **Meta.config.json_schema_render_stage_options,
100
+ **options,
101
+ execution: self, stage: :render
102
+ )
103
103
  response.content_type = 'application/json' if response.content_type.nil?
104
104
  response.body = [JSON.generate(new_hash)]
105
105
  else
@@ -109,7 +109,12 @@ module Meta
109
109
  renders.each do |key, render_content|
110
110
  raise Errors::RenderingError, "渲染的键名 `#{key}` 不存在,请检查实体定义以确认是否有拼写错误" unless entity_schema.properties.key?(key)
111
111
  schema = entity_schema.properties[key].schema(:render)
112
- final_value[key] = schema.filter(render_content[:value], **render_content[:options], execution: self, stage: :render)
112
+ final_value[key] = schema.filter(render_content[:value],
113
+ **Meta.config.json_schema_user_options,
114
+ **Meta.config.json_schema_render_stage_options,
115
+ **render_content[:options],
116
+ execution: self, stage: :render
117
+ )
113
118
  rescue JsonSchema::ValidationErrors => e
114
119
  # 错误信息再度绑定 key
115
120
  errors.merge! e.errors.transform_keys! { |k| k.empty? ? key : "#{key}.#{k}" }
@@ -148,13 +153,23 @@ module Meta
148
153
  end
149
154
 
150
155
  def parse_request_body_for_replacing
151
- request_body_schema.filter(params(:raw), stage: :param)
156
+ request_body_schema.filter(
157
+ params(:raw),
158
+ **Meta.config.json_schema_user_options,
159
+ **Meta.config.json_schema_param_stage_options,
160
+ execution: self, stage: :param
161
+ )
152
162
  rescue JsonSchema::ValidationErrors => e
153
163
  raise Errors::ParameterInvalid.new(e.errors)
154
164
  end
155
165
 
156
166
  def parse_request_body_for_updating
157
- request_body_schema.filter(params(:raw), stage: :param, discard_missing: true)
167
+ request_body_schema.filter(
168
+ params(:raw),
169
+ **Meta.config.json_schema_user_options,
170
+ **Meta.config.json_schema_param_stage_options,
171
+ execution: self, stage: :param, discard_missing: true
172
+ )
158
173
  rescue JsonSchema::ValidationErrors => e
159
174
  raise Errors::ParameterInvalid.new(e.errors)
160
175
  end
@@ -3,16 +3,14 @@
3
3
  # 洋葱圈模型的链式调用,需要结合 Meta::RouteDSL::AroundActionBuilder 才可以看到它奇效。
4
4
 
5
5
  module Meta
6
- module RouteDSL
7
- class LinkedAction
8
- def initialize(current_proc, next_action)
9
- @current_proc = current_proc
10
- @next_action = next_action
11
- end
6
+ class LinkedAction
7
+ def initialize(current_proc, next_action)
8
+ @current_proc = current_proc
9
+ @next_action = next_action
10
+ end
12
11
 
13
- def execute(execution)
14
- execution.instance_exec(@next_action, &@current_proc)
15
- end
12
+ def execute(execution)
13
+ execution.instance_exec(@next_action, &@current_proc)
16
14
  end
17
15
  end
18
16
  end
@@ -11,7 +11,7 @@ module Meta
11
11
  @tags = tags
12
12
  @parameters = parameters.is_a?(Parameters) ? parameters : Parameters.new(parameters)
13
13
  @request_body = request_body
14
- @responses = responses || { 204 => nil }
14
+ @responses = responses || {} # || { 204 => nil }
15
15
  end
16
16
 
17
17
  def [](key)
@@ -54,8 +54,10 @@ module Meta
54
54
  operation_object.compact
55
55
  end
56
56
 
57
- def self.new(meta = {})
58
- meta.is_a?(Metadata) ? meta : super(**meta)
57
+ class << self
58
+ def new(meta = {})
59
+ meta.is_a?(Metadata) ? meta : super(**meta)
60
+ end
59
61
  end
60
62
  end
61
63
  end
@@ -7,7 +7,7 @@ module Meta
7
7
  attr_reader :parameters
8
8
 
9
9
  def initialize(parameters)
10
- @parameters = parameters
10
+ @parameters = parameters.dup
11
11
  end
12
12
 
13
13
  def filter(request)
@@ -10,6 +10,7 @@ module Meta
10
10
 
11
11
  attr_reader :path, :method, :meta, :action
12
12
 
13
+ # path 是局部 path,不包含由父级定义的前缀
13
14
  def initialize(path: '', method: :all, meta: {}, action: nil)
14
15
  @path = Utils::Path.normalize_path(path)
15
16
  @method = method
@@ -25,9 +26,10 @@ module Meta
25
26
 
26
27
  action.execute(execution) if action
27
28
 
29
+ set_status(execution)
28
30
  render_entity(execution) if @meta[:responses]
29
31
  rescue Execution::Abort
30
- nil
32
+ execution.response.status = 200 if execution.response.status == 0
31
33
  end
32
34
 
33
35
  def match?(execution, remaining_path)
@@ -42,14 +44,24 @@ module Meta
42
44
 
43
45
  private
44
46
 
47
+ def set_status(execution)
48
+ response_definitions = @meta[:responses]
49
+ response = execution.response
50
+ if response.status == 0
51
+ response.status = (response_definitions&.length > 0) ? response_definitions.keys[0] : 200
52
+ end
53
+ end
54
+
45
55
  def render_entity(execution)
46
- responses = @meta[:responses]
47
- status = execution.response.status
48
- codes = responses.keys
49
- return unless codes.include?(status)
56
+ response_definitions = @meta[:responses]
57
+ return if response_definitions.empty? # 未设定 status 宏时不需要执行 render_entity 方法
58
+
59
+ # 查找 entity schema
60
+ entity_schema = response_definitions[execution.response.status]
61
+ return if entity_schema.nil?
50
62
 
51
- entity_schema = responses[status]
52
- execution.render_entity(entity_schema) if entity_schema
63
+ # 执行面向 schema 的渲染
64
+ execution.render_entity(entity_schema)
53
65
  end
54
66
  end
55
67
  end
data/lib/meta/config.rb CHANGED
@@ -2,11 +2,16 @@
2
2
 
3
3
  module Meta
4
4
  class Config
5
- attr_accessor :render_validation, :render_type_conversion
5
+ attr_accessor :default_locked_scope,
6
+ :json_schema_user_options,
7
+ :json_schema_param_stage_options,
8
+ :json_schema_render_stage_options
6
9
 
7
10
  def initialize
8
- @render_type_conversion = true
9
- @render_validation = true
11
+ @default_locked_scope = nil
12
+ @json_schema_user_options = {}
13
+ @json_schema_param_stage_options = {}
14
+ @json_schema_render_stage_options = {}
10
15
  end
11
16
  end
12
17
 
@@ -12,7 +12,13 @@ module Meta
12
12
  SCHEMA_BUILDER_OPTIONS = Utils::KeywordArgs::Builder.build do
13
13
  permit_extras true
14
14
 
15
- key :ref, alias_names: [:using]
15
+ key :ref, alias_names: [:using], normalizer: ->(entity) {
16
+ if Meta.config.default_locked_scope && entity.is_a?(Class) && entity < Meta::Entity
17
+ entity.locked(scope: Meta.config.default_locked_scope)
18
+ else
19
+ entity
20
+ end
21
+ }
16
22
  key :dynamic_ref, alias_names: [:dynamic_using], normalizer: ->(value) { value.is_a?(Proc) ? { resolve: value } : value }
17
23
  end
18
24
  def build(options = {}, &block)
@@ -25,14 +25,16 @@ module Meta
25
25
  options = OPTIONS_CHECKER.check(options)
26
26
  raise '不允许 BaseSchema 直接接受 array 类型,必须通过继承使用 ArraySchema' if options[:type] == 'array' && self.class == BaseSchema
27
27
 
28
- @options = SchemaOptions.normalize(options)
28
+ @options = SchemaOptions.normalize(options).freeze
29
29
  end
30
30
 
31
31
  USER_OPTIONS_CHECKER = Utils::KeywordArgs::Builder.build do
32
- key :stage, :execution, :object_value, :type_conversion, :validation, :user_data
32
+ key :execution, :object_value, :type_conversion, :validation, :user_data
33
+ key :stage, validator: ->(value) { raise ArgumentError, "stage 只能取值为 :param 或 :render" unless [:param, :render].include?(value) }
33
34
 
34
- # 以下三个是 ObjectSchema 需要的选项
35
- key :discard_missing, :exclude, :scope
35
+ # 以下是 ObjectSchema 需要的选项
36
+ # extra_properties 只能取值为 :ignore、:raise_error
37
+ key :discard_missing, :extra_properties, :exclude, :scope
36
38
  end
37
39
 
38
40
  def filter(value, user_options = {})
@@ -40,7 +42,7 @@ module Meta
40
42
 
41
43
  value = resolve_value(user_options) if options[:value]
42
44
  value = JsonSchema::Presenters.present(options[:presenter], value) if options[:presenter]
43
- value = options[:default] if value.nil? && options.key?(:default)
45
+ value = resolve_default_value(options[:default]) if value.nil? && options.key?(:default)
44
46
  value = options[:convert].call(value) if options[:convert]
45
47
 
46
48
  # 第一步,转化值。
@@ -110,6 +112,16 @@ module Meta
110
112
  validator&.call(value, option, stage_options)
111
113
  end
112
114
  end
115
+
116
+ def resolve_default_value(default_resolver)
117
+ if default_resolver.respond_to?(:call)
118
+ default_resolver.call
119
+ elsif default_resolver.respond_to?(:dup)
120
+ default_resolver.dup
121
+ else
122
+ default_resolver
123
+ end
124
+ end
113
125
  end
114
126
  end
115
127
  end
@@ -48,7 +48,7 @@ module Meta
48
48
  end
49
49
 
50
50
  # 合并其他的属性,并返回一个新的 ObjectSchema
51
- def merge(properties)
51
+ def merge_other_properties(properties)
52
52
  ObjectSchema.new(properties: self.properties.merge(properties))
53
53
  end
54
54
 
@@ -92,7 +92,7 @@ module Meta
92
92
  object = {}
93
93
  errors = {}
94
94
  filtered_properties.each do |name, property_schema|
95
- value = resolve_property_value(object_value, name, property_schema, stage)
95
+ value = resolve_property_value(object_value, name, property_schema)
96
96
 
97
97
  begin
98
98
  object[name] = property_schema.filter(value, **user_options, object_value: object_value)
@@ -101,6 +101,11 @@ module Meta
101
101
  end
102
102
  end.to_h
103
103
 
104
+ # 第三步,检测是否有剩余的属性
105
+ if user_options[:extra_properties] == :raise_error && !(object_value.keys.map(&:to_sym) - properties.keys).empty?
106
+ raise JsonSchema::ValidationError, '遇到多余的属性'
107
+ end
108
+
104
109
  if errors.empty?
105
110
  object
106
111
  else
@@ -122,6 +127,10 @@ module Meta
122
127
  # 程序中有些地方用到了这三个方法
123
128
  def_delegators :@properties, :empty?, :key?, :[]
124
129
 
130
+ def merge(properties)
131
+ self.class.new(@properties.merge(properties.instance_eval { @properties }))
132
+ end
133
+
125
134
  def self.build_property(*args)
126
135
  StagingProperty.build(*args)
127
136
  end
@@ -143,7 +152,7 @@ module Meta
143
152
  end
144
153
  end
145
154
 
146
- def resolve_property_value(object_value, name, property_schema, stage)
155
+ def resolve_property_value(object_value, name, property_schema)
147
156
  if property_schema.value?
148
157
  nil
149
158
  elsif object_value.is_a?(Hash) || object_value.is_a?(ObjectWrapper)
@@ -28,7 +28,7 @@ module Meta
28
28
  },
29
29
  allowable: proc { |value, allowable_values|
30
30
  next if value.nil?
31
- raise JsonSchema::ValidationError, I18n.t(:'json_schema.errors.allowable') unless allowable_values.include?(value)
31
+ raise JsonSchema::ValidationError, I18n.t(:'json_schema.errors.allowable', allowable_values: allowable_values) unless allowable_values.include?(value)
32
32
  }
33
33
  }
34
34
 
@@ -2,39 +2,36 @@
2
2
 
3
3
  require_relative 'route_builder'
4
4
  require_relative 'meta_builder'
5
+ require_relative '../utils/route_dsl_builders'
5
6
 
6
7
  module Meta
7
8
  module RouteDSL
8
9
  class ApplicationBuilder
9
10
  include MetaBuilder::Delegator
10
11
 
11
- def initialize(prefix = nil, &block)
12
- @mod_prefix = prefix
12
+ def initialize(full_prefix = '/', &block)
13
+ @full_prefix = full_prefix
13
14
  @callbacks = []
14
15
  @error_guards = []
15
- @meta_builder = MetaBuilder.new
16
+ @meta_builder = MetaBuilder.new(route_full_path: full_prefix)
16
17
  @mod_builders = []
17
18
  @shared_mods = []
18
19
 
19
20
  instance_exec &block if block_given?
20
21
  end
21
22
 
22
- def build(parent_path: '', meta: {}, callbacks: [])
23
- # 合并 meta 时不仅仅是覆盖,比如 parameters 参数需要合并
24
- meta2 = (meta || {}).merge(@meta_builder.build)
25
- if meta[:parameters] && meta2[:parameters]
26
- meta2[:parameters] = meta[:parameters].merge(meta2[:parameters])
27
- end
28
-
29
- # 构建子模块
30
- # 合并父级传递过来的 callbacks,将 before around 放在前面,after 放在后面
31
- parent_before = callbacks.filter { |cb| cb[:lifecycle] == :before || cb[:lifecycle] == :around }
32
- parent_after = callbacks.filter { |cb| cb[:lifecycle] == :after }
33
- callbacks = parent_before + @callbacks + parent_after
34
- mods = @mod_builders.map { |builder| builder.build(parent_path: Utils::Path.join(parent_path, @mod_prefix), meta: meta2, callbacks: callbacks) }
23
+ # meta callbacks 是父级传递过来的,需要合并到当前模块或子模块中。
24
+ #
25
+ # 为什么一定要动态传递 meta_options 参数?由于 OpenAPI 文档是面向路由的,parameters、request_body、
26
+ # responses 都存在于路由文档中,对应地 Metadata 对象最终只存在于路由文档中。因此,在构建过程中,需要将父
27
+ # 级传递过来的 Metadata 对象合并到当前模块,再层层合并到子模块。
28
+ def build(meta_options: {}, callbacks: [])
29
+ meta_options = Utils::RouteDSLBuilders.merge_meta_options(meta_options, @meta_builder.build)
30
+ callbacks = Utils::RouteDSLBuilders.merge_callbacks(callbacks, @callbacks)
31
+ mods = @mod_builders.map { |builder| builder.build(meta_options: meta_options, callbacks: callbacks) }
35
32
 
36
33
  Application.new(
37
- prefix: @mod_prefix,
34
+ prefix: @full_prefix,
38
35
  mods: mods,
39
36
  shared_mods: @shared_mods,
40
37
  error_guards: @error_guards
@@ -48,7 +45,7 @@ module Meta
48
45
 
49
46
  # 定义路由块
50
47
  def route(path, method = nil, &block)
51
- route_builder = RouteDSL::RouteBuilder.new(path, method, &block)
48
+ route_builder = RouteBuilder.new(path, method, parent_path: @full_prefix, &block)
52
49
  @mod_builders << route_builder
53
50
  route_builder
54
51
  end
@@ -108,13 +105,9 @@ module Meta
108
105
  @meta = meta
109
106
  end
110
107
 
111
- def build(parent_path: '', meta: {}, **kwargs)
112
- # 合并 meta 时不仅仅是覆盖,比如 parameters 参数需要合并
113
- meta2 = (meta || {}).merge(@meta)
114
- if meta[:parameters] && meta2[:parameters]
115
- meta2[:parameters] = meta[:parameters].merge(meta2[:parameters])
116
- end
117
- @builder.build(parent_path: parent_path, meta: meta2, **kwargs)
108
+ def build(meta_options: {}, **kwargs)
109
+ meta_options = Utils::RouteDSLBuilders.merge_meta_options(meta_options, @meta)
110
+ @builder.build(meta_options: meta_options, **kwargs)
118
111
  end
119
112
  end
120
113
  end
@@ -45,35 +45,45 @@ module Meta
45
45
  end
46
46
  end
47
47
 
48
- # 使用 before、after、around 系列和当前 action 共同构建洋葱圈模型。
49
- # Note: 该方法可能被废弃!
50
- #
51
- # 构建成功后,执行顺序是:
52
- #
53
- # - before 序列
54
- # - around 序列的前半部分
55
- # - action
56
- # - around 序列的后半部分
57
- # - after 序列
58
- #
59
- def self.build(before: [], after: [], around: [], action: nil)
60
- builder = AroundActionBuilder.new
48
+ class << self
49
+ # 使用 before、after、around 系列和当前 action 共同构建洋葱圈模型。
50
+ # Note: 该方法已被废弃!
51
+ #
52
+ # 构建成功后,执行顺序是:
53
+ #
54
+ # - before 序列
55
+ # - around 序列的前半部分
56
+ # - action
57
+ # - around 序列的后半部分
58
+ # - after 序列
59
+ #
60
+ def build(before: [], after: [], around: [], action: nil)
61
+ builder = AroundActionBuilder.new
61
62
 
62
- # 首先构建 before 序列,保证它最先执行
63
- builder.around do |next_action|
64
- before.each { |p| self.instance_exec(&p) }
65
- next_action.execute(self)
66
- end unless before.empty?
67
- # 然后构建 after 序列,保证它最后执行
68
- builder.around do |next_action|
69
- next_action.execute(self)
70
- after.each { |p| self.instance_exec(&p) }
71
- end unless after.empty?
72
- # 接着应用洋葱圈模型,依次构建 around 序列、action
73
- around.each { |p| builder.around(&p) }
74
- builder.around { self.instance_exec(&action) } unless action.nil?
63
+ # 首先构建 before 序列,保证它最先执行
64
+ builder.around do |next_action|
65
+ before.each { |p| self.instance_exec(&p) }
66
+ next_action.execute(self)
67
+ end unless before.empty?
68
+ # 然后构建 after 序列,保证它最后执行
69
+ builder.around do |next_action|
70
+ next_action.execute(self)
71
+ after.each { |p| self.instance_exec(&p) }
72
+ end unless after.empty?
73
+ # 接着应用洋葱圈模型,依次构建 around 序列、action
74
+ around.each { |p| builder.around(&p) }
75
+ builder.around { self.instance_exec(&action) } unless action.nil?
75
76
 
76
- builder.build
77
+ builder.build
78
+ end
79
+
80
+ def build_from_callbacks(callbacks: [])
81
+ around_action_builder = AroundActionBuilder.new
82
+ callbacks.each do |cb|
83
+ around_action_builder.send(cb[:lifecycle], &cb[:proc]) if cb[:proc]
84
+ end
85
+ around_action_builder.build
86
+ end
77
87
  end
78
88
  end
79
89
  end
@@ -6,32 +6,39 @@ require_relative 'uniformed_params_builder'
6
6
  module Meta
7
7
  module RouteDSL
8
8
  class MetaBuilder
9
- def initialize(&block)
9
+ def initialize(route_full_path:, route_method: :all, &block)
10
+ @route_full_path = route_full_path
11
+ @method = route_method
10
12
  @meta = {}
13
+ @parameters_builder = ParametersBuilder.new(route_full_path: route_full_path, route_method: route_method) # 默认给一个空的参数构建器,它只会处理 path 参数
11
14
 
12
15
  instance_exec &block if block_given?
13
16
  end
14
17
 
15
18
  def build
16
- @meta
19
+ meta = @meta
20
+ if @meta[:parameters].nil? && @route_full_path =~ /[:*].+/
21
+ meta[:parameters] = ParametersBuilder.new(route_full_path: @route_full_path, route_method: @method).build
22
+ end
23
+ meta
17
24
  end
18
25
 
19
26
  def parameters(&block)
20
- @meta[:parameters] = ParametersBuilder.new(&block).build
27
+ @meta[:parameters] = ParametersBuilder.new(route_full_path: @route_full_path, route_method: @method, &block).build
21
28
  end
22
29
 
23
30
  def request_body(options = {}, &block)
24
- @meta[:request_body] = JsonSchema::SchemaBuilderTool.build(options, &block)
31
+ @meta[:request_body] = JsonSchema::SchemaBuilderTool.build(options, &block).to_schema
25
32
  end
26
33
 
27
34
  # params 宏是一个遗留的宏,它在一个宏定义块内同时定义 parameters 和 request_body
28
35
  def params(&block)
29
- @meta[:parameters], @meta[:request_body] = UniformedParamsBuilder.new(&block).build
36
+ @meta[:parameters], @meta[:request_body] = UniformedParamsBuilder.new(route_full_path: @route_full_path, route_method: @method, &block).build
30
37
  end
31
38
 
32
- def status(code, *other_codes, &block)
39
+ def status(code, *other_codes, **options, &block)
33
40
  codes = [code, *other_codes]
34
- entity_schema = JsonSchema::SchemaBuilderTool.build(&block)
41
+ entity_schema = JsonSchema::SchemaBuilderTool.build(options, &block)
35
42
  @meta[:responses] = @meta[:responses] || {}
36
43
  codes.each { |code| @meta[:responses][code] = entity_schema }
37
44
  end
@@ -47,8 +54,8 @@ module Meta
47
54
  module Delegator
48
55
  method_names = MetaBuilder.public_instance_methods(false) - ['build']
49
56
  method_names.each do |method_name|
50
- define_method(method_name) do |*args, &block|
51
- @meta_builder.send(method_name, *args, &block) and self
57
+ define_method(method_name) do |*args, **kwargs, &block|
58
+ @meta_builder.send(method_name, *args, **kwargs, &block) and self
52
59
  end
53
60
  end
54
61
  end
@@ -1,24 +1,47 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require_relative '../application/parameters'
4
+ require_relative '../utils/kwargs/checker'
3
5
 
4
6
  module Meta
5
7
  module RouteDSL
6
8
  class ParametersBuilder
7
- def initialize(&block)
8
- @parameters = {}
9
+ def initialize(route_full_path:, route_method:, &block)
10
+ @route_full_path = route_full_path || ''
11
+ @route_method = route_method
12
+ @parameter_options = {}
9
13
 
10
14
  instance_exec &block if block_given?
11
15
  end
12
16
 
13
- def param(name, options)
17
+ def param(name, options = {})
18
+ # 修正 path 参数的选项
14
19
  options = options.dup
15
- op_in = options.delete(:in) || 'query'
20
+ if path_param_names.include?(name) # path 参数
21
+ options = Utils::KeywordArgs::Checker.fix!(options, in: 'path', required: true)
22
+ else
23
+ options = Utils::KeywordArgs::Checker.merge_defaults!(options, in: 'query')
24
+ end
16
25
 
17
- @parameters[name] = { in: op_in, schema: JsonSchema::BaseSchema.new(options) }
26
+ in_op = options.delete(:in)
27
+ @parameter_options[name] = { in: in_op, schema: JsonSchema::BaseSchema.new(options) }
18
28
  end
19
29
 
20
30
  def build
21
- Parameters.new(@parameters)
31
+ # 补充未声明的 path 参数
32
+ (path_param_names - @parameter_options.keys).each do |name|
33
+ @parameter_options[name] = { in: 'path', schema: JsonSchema::BaseSchema.new(required: true) }
34
+ end
35
+
36
+ Parameters.new(@parameter_options)
37
+ end
38
+
39
+ private
40
+
41
+ def path_param_names
42
+ @_path_param_names ||= @route_full_path.split('/')
43
+ .filter { |part| part =~ /[:*].+/ }
44
+ .map { |part| part[1..-1].to_sym }
22
45
  end
23
46
  end
24
47
  end
@@ -3,7 +3,6 @@
3
3
  require 'json'
4
4
  require_relative '../entity'
5
5
  require_relative '../application/route'
6
- require_relative 'helpers'
7
6
  require_relative 'chain_builder'
8
7
  require_relative 'action_builder'
9
8
  require_relative 'meta_builder'
@@ -16,52 +15,27 @@ module Meta
16
15
 
17
16
  alias :if_status :status
18
17
 
19
- def initialize(path = '', method = :all, &block)
18
+ # 这里的 path 局部的路径,也就是由 route 宏命令定义的路径
19
+ def initialize(path, method = :all, parent_path: '',&block)
20
+ route_full_path = Utils::Path.join(parent_path, path)
21
+
20
22
  @path = path || ''
21
23
  @method = method || :all
22
24
  @action_builder = nil
23
- @meta_builder = MetaBuilder.new
25
+ @meta_builder = MetaBuilder.new(route_full_path: route_full_path, route_method: method)
24
26
 
25
27
  instance_exec &block if block_given?
26
28
  end
27
29
 
28
- def build(parent_path: '', meta: {}, callbacks: {})
29
- # 合并 meta 时不仅仅是覆盖,比如 parameters 参数需要合并
30
- meta2 = (meta || {}).merge(@meta_builder.build)
31
- if meta[:parameters] && meta2[:parameters]
32
- meta2[:parameters] = meta[:parameters].merge(meta2[:parameters])
33
- end
34
-
35
- # 合并 parameters 参数
36
- meta2[:parameters] ||= {}
37
- path_params = Utils::Path.join(parent_path, @path).split('/')
38
- .filter { |part| part =~ /[:*].+/ }
39
- .map { |part| part[1..-1].to_sym }
40
- path_params.each do |name|
41
- unless meta2[:parameters].key?(name)
42
- meta2[:parameters][name] = {
43
- in: 'path',
44
- schema: JsonSchema::BaseSchema.new(required: true)
45
- }
46
- end
47
- end
48
-
49
- # 构建洋葱圈模型的 LinkedAction
50
- # 合并父级传递过来的 callbacks,将 before 和 around 放在前面,after 放在后面
51
- parent_before = callbacks.filter { |cb| cb[:lifecycle] == :before || cb[:lifecycle] == :around }
52
- parent_after = callbacks.filter { |cb| cb[:lifecycle] == :after }
53
- callbacks = parent_before + [{ lifecycle: :before, proc: @action_builder&.build }] + parent_after
54
- # 使用 AroundActionBuilder 构建洋葱圈模型
55
- around_action_builder = AroundActionBuilder.new
56
- callbacks.each do |cb|
57
- around_action_builder.send(cb[:lifecycle], &cb[:proc]) if cb[:proc]
58
- end
59
- action = around_action_builder.build
30
+ def build(meta_options: {}, callbacks: {})
31
+ meta_options = Utils::RouteDSLBuilders.merge_meta_options(meta_options, @meta_builder.build)
32
+ callbacks = Utils::RouteDSLBuilders.merge_callbacks(callbacks, [{ lifecycle: :before, proc: @action_builder&.build }])
33
+ action = AroundActionBuilder.build_from_callbacks(callbacks: callbacks)
60
34
 
61
35
  Route.new(
62
36
  path: @path,
63
37
  method: @method,
64
- meta: meta2,
38
+ meta: meta_options,
65
39
  action: action
66
40
  )
67
41
  end
@@ -81,14 +55,6 @@ module Meta
81
55
  self
82
56
  end
83
57
  end
84
-
85
- private
86
-
87
- def clone_meta(meta)
88
- meta = meta.clone
89
- meta[:responses] = meta[:responses].clone if meta[:responses]
90
- meta
91
- end
92
58
  end
93
59
  end
94
60
  end
@@ -3,31 +3,49 @@
3
3
  module Meta
4
4
  module RouteDSL
5
5
  class UniformedParamsBuilder
6
- def initialize(&block)
7
- @parameters = {}
8
- @request_body_builder = JsonSchema::ObjectSchemaBuilder.new
6
+ def initialize(route_full_path:, route_method:, &block)
7
+ @route_full_path = route_full_path
8
+ @route_method = route_method
9
+ @parameters_builder = ParametersBuilder.new(route_full_path: @route_full_path, route_method: @route_method)
10
+
11
+ @parameter_options = {}
9
12
 
10
13
  instance_exec &block if block_given?
11
14
  end
12
15
 
13
16
  def param(name, options = {}, &block)
14
- options = options.dup
15
- op_in = options.delete(:in) || 'body'
17
+ options = (options || {}).dup
18
+ in_op = options.delete(:in) || \
19
+ if path_param_names.include?(name)
20
+ 'path'
21
+ elsif @route_method == :get
22
+ 'query'
23
+ else
24
+ 'body'
25
+ end
16
26
 
17
- if op_in == 'body'
27
+ if in_op == 'body'
18
28
  property name, options, &block
19
29
  else
20
- @parameters[name] = { in: op_in, schema: JsonSchema::BaseSchema.new(options) }
30
+ @parameters_builder.param name, options
21
31
  end
22
32
  end
23
33
 
24
34
  def property(name, options = {}, &block)
35
+ @request_body_builder ||= JsonSchema::ObjectSchemaBuilder.new
25
36
  @request_body_builder.property name, options, &block
26
37
  end
27
38
 
28
39
  def build
29
- request_body = @request_body_builder.to_schema
30
- [@parameters, request_body.properties.empty? ? nil : request_body]
40
+ [@parameters_builder.build, @request_body_builder&.to_schema]
41
+ end
42
+
43
+ private
44
+
45
+ def path_param_names
46
+ @_path_param_names ||= @route_full_path.split('/')
47
+ .filter { |part| part =~ /[:*].+/ }
48
+ .map { |part| part[1..-1].to_sym }
31
49
  end
32
50
  end
33
51
  end
@@ -47,13 +47,13 @@ module Meta
47
47
  # ]
48
48
  def get_paths_and_routes!(application, prefix = '', store_routes = [])
49
49
  if (application.is_a?(Class) && application < Application) || application.is_a?(Application)
50
- prefix = RouteDSL::Helpers.join_path(prefix, application.prefix)
50
+ prefix = Utils::Path.join(prefix, application.prefix)
51
51
  (application.routes + application.applications).each do |mod|
52
52
  get_paths_and_routes!(mod, prefix, store_routes)
53
53
  end
54
54
  elsif application.is_a?(Route)
55
55
  route = application
56
- route_path = route.path == :all ? prefix : RouteDSL::Helpers.join_path(prefix, route.path)
56
+ route_path = route.path == :all ? prefix : Utils::Path.join(prefix, route.path)
57
57
  store_routes << [route_path, route] unless route.method == :all
58
58
  else
59
59
  raise "Param application must be a Application instance, Application module or a Route instance, but it got a `#{application}`"
@@ -55,10 +55,11 @@ module Meta
55
55
  class Argument
56
56
  DEFAULT_TRANSFORMER = ->(value) { value }
57
57
 
58
- def initialize(name:, normalizer: DEFAULT_TRANSFORMER, alias_names: [])
58
+ def initialize(name:, normalizer: DEFAULT_TRANSFORMER, validator: nil, default: nil, alias_names: [])
59
59
  @key_name = name
60
60
  @consumer_names = [name] + alias_names
61
- @normalizer = normalizer
61
+ @normalizer = default ? ->(value) { normalizer.call(value || default) } : normalizer
62
+ @validator = validator
62
63
  end
63
64
 
64
65
  def consume(final_args, args)
@@ -71,6 +72,7 @@ module Meta
71
72
  def consume_name(final_args, args, consumer_name)
72
73
  if args.key?(consumer_name)
73
74
  value = @normalizer.call(args.delete(consumer_name))
75
+ @validator.call(value) if @validator
74
76
  final_args[@key_name] = value
75
77
  true
76
78
  else
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Meta
4
+ module Utils
5
+ class KeywordArgs
6
+ module Checker
7
+ class << self
8
+ # 将 options 内的值修正为固定值,该方法会原地修改 options 选项。
9
+ # 如果 options 中的缺失相应的值,则使用 fixed_values 中的值补充;如果 options 中的值不等于 fixed_values 中对应的值,则抛出异常。
10
+ # 示例:
11
+ # (1)fix!({}, { a: 1, b: 2 }) # => { a: 1, b: 2 }
12
+ # (2)fix!({ a: 1 }, { a: 2 }) # raise error
13
+ def fix!(options, fixed_values)
14
+ fixed_values.each do |key, value|
15
+ if options.include?(key)
16
+ if options[key] != value
17
+ raise ArgumentError, "关键字参数 #{key} 的值不正确,必须为 #{value}"
18
+ end
19
+ else
20
+ options[key] = value
21
+ end
22
+ end
23
+ options
24
+ end
25
+
26
+ def merge_defaults!(options, defaults)
27
+ defaults.each do |key, value|
28
+ options[key] = value unless options[key]
29
+ end
30
+ options
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
+ #
2
3
 
3
4
  module Meta
4
5
  module Utils
5
6
  class Path
6
7
  class << self
8
+ # 规范化 path 结构,确保 path 以 '/' 开头,不以 '/' 结尾。
9
+ # 仅有一个例外,如果 path 为 nil 或空字符串,则返回空字符串 ''.
7
10
  def normalize_path(path)
8
11
  path = '/' unless path
9
12
  path = '/' + path unless path.start_with?('/')
@@ -11,8 +14,11 @@ module Meta
11
14
  path
12
15
  end
13
16
 
14
- def join(p1, p2)
15
- normalize_path(normalize_path(p1) + '/' + normalize_path(p2))
17
+ # 合并两个 path. 有且只有一个例外,如果 p1 p2 其中之一为 '/',则返回另一个。
18
+ def join(*parts)
19
+ parts = parts.map { |p| (p || '').delete_prefix('/').delete_suffix('/') }
20
+ parts = parts.reject { |p| p.nil? || p.empty? }
21
+ '/' + parts.join('/')
16
22
  end
17
23
  end
18
24
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Meta
4
+ module Utils
5
+ class RouteDSLBuilders
6
+ class << self
7
+ def merge_meta_options(options1, options2)
8
+ final_options = (options1 || {}).merge(options2 || {})
9
+ if options1[:parameters] && options2[:parameters]
10
+ final_options[:parameters] = options1[:parameters].merge(options2[:parameters])
11
+ end
12
+ if options1[:request_body].is_a?(Meta::JsonSchema::ObjectSchema) && options2[:request_body].is_a?(Meta::JsonSchema::ObjectSchema)
13
+ final_options[:request_body] = options1[:request_body].merge_other_properties(options2[:request_body].properties)
14
+ end
15
+ if options1[:responses] && options2[:responses]
16
+ final_options[:responses] = options1[:responses].merge(options2[:responses])
17
+ end
18
+ final_options
19
+ end
20
+
21
+ def merge_callbacks(parent_callbacks, current_callbacks)
22
+ # 合并父级传递过来的 callbacks,将 before 和 around 放在前面,after 放在后面
23
+ parent_before = parent_callbacks.filter { |cb| cb[:lifecycle] == :before || cb[:lifecycle] == :around }
24
+ parent_after = parent_callbacks.filter { |cb| cb[:lifecycle] == :after }
25
+ parent_before + current_callbacks + parent_after
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
data/meta-api.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "meta-api"
3
- spec.version = "0.0.6"
3
+ spec.version = "0.0.7"
4
4
  spec.authors = ["yetrun"]
5
5
  spec.email = ["yetrun@foxmail.com"]
6
6
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: meta-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - yetrun
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-26 00:00:00.000000000 Z
11
+ date: 2023-07-14 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 一个 Web API 框架,该框架采用定义元信息的方式编写 API,并同步生成 API 文档
14
14
  email:
@@ -113,7 +113,6 @@ files:
113
113
  - lib/meta/route_dsl/application_builder.rb
114
114
  - lib/meta/route_dsl/around_action_builder.rb
115
115
  - lib/meta/route_dsl/chain_builder.rb
116
- - lib/meta/route_dsl/helpers.rb
117
116
  - lib/meta/route_dsl/meta_builder.rb
118
117
  - lib/meta/route_dsl/parameters_builder.rb
119
118
  - lib/meta/route_dsl/route_builder.rb
@@ -121,7 +120,9 @@ files:
121
120
  - lib/meta/swagger_doc.rb
122
121
  - lib/meta/utils/kwargs/builder.rb
123
122
  - lib/meta/utils/kwargs/check.rb
123
+ - lib/meta/utils/kwargs/checker.rb
124
124
  - lib/meta/utils/path.rb
125
+ - lib/meta/utils/route_dsl_builders.rb
125
126
  - meta-api.gemspec
126
127
  homepage: https://github.com/yetrun/web-frame
127
128
  licenses:
@@ -130,7 +131,7 @@ metadata:
130
131
  allowed_push_host: https://rubygems.org
131
132
  homepage_uri: https://github.com/yetrun/web-frame
132
133
  source_code_uri: https://github.com/yetrun/web-frame.git
133
- post_install_message:
134
+ post_install_message:
134
135
  rdoc_options: []
135
136
  require_paths:
136
137
  - lib
@@ -145,8 +146,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
145
146
  - !ruby/object:Gem::Version
146
147
  version: '0'
147
148
  requirements: []
148
- rubygems_version: 3.3.26
149
- signing_key:
149
+ rubygems_version: 3.4.15
150
+ signing_key:
150
151
  specification_version: 4
151
152
  summary: 一个 Web API 框架
152
153
  test_files: []
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Meta
4
- module RouteDSL
5
- module Helpers
6
- class << self
7
- def join_path(*parts)
8
- parts = parts.map { |p| (p || '').delete_prefix('/').delete_suffix('/') }
9
- parts = parts.reject { |p| p.nil? || p.empty? }
10
- '/' + parts.join('/')
11
- end
12
- end
13
- end
14
- end
15
- end