meta-api 0.0.9 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3394a5e2d3b6dfd45f1478d8477426a4a947253d3252a050dc9c1d7cafd3c3d1
4
- data.tar.gz: 98d12919b2adf824bec651ba5625076ddb7571962cdc66cee188b3b250fd1b24
3
+ metadata.gz: bf3fb639df71a5353328e898711c9758c5e5fd3dd1760409ae823fbd0a7852e2
4
+ data.tar.gz: ea5dd728c3691fcf542d1085fd21d787d727c97b588652de3ffdd53b67512437
5
5
  SHA512:
6
- metadata.gz: c1e80fecb3cf0bce148345660f19a8f44136b1e1647471e89aa9fb4d1c4f55a0ddf7ccb11fdc26ca7880c318173ed4f3254e0835d5bfd96e77048a301804d729
7
- data.tar.gz: 16d7fdf0f82b89155eb0fc655e424d54ba5bf880c984dd322266afdb2952c4c34ff1ce2c83f8d174adcb086057d6c63aa7534b75defd4c3f7ff84f8a7f948ec0
6
+ metadata.gz: 4a9f0a02d8091f77e7f9c2ea0ea67b86f1877327a7b75a7f4342fecd7f2822f9bde2ded59c20cb1c3227eb88a4b6b098c9438710a6a8eedcf4c8defb2f37c572
7
+ data.tar.gz: 2e7911858914bd1b34ff60007f48757f6afb1183c59d15d9e490fb5fe781244522d5500251dcbdbb02fc5f26a17d48ab82bbc2f6dd4368c2540f97bb351f6da3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # 更新日志
2
2
 
3
+ ## 0.1.0(2023 年 8 月 5 日)
4
+
5
+ 1. 删除 `on:` 选项。
6
+ 2. `type_conversion` 为 `false` 时不影响对象和数组类型的转化。
7
+ 3. 修复 `ref:` 嵌套造成的文档问题。
8
+ 4. 将 HTTP Method 的 scope 添加 `$` 符号前缀,如 `$get`、`$post` 等。
9
+ 5. `Meta.config` 去掉了 `default_locked_scope` 的配置项。
10
+
3
11
  ## 0.0.9(2023 年 7 月 22 日)
4
12
 
5
13
  1. JsonSchema 添加 before:、after: 选项,用于在过滤前后执行一些操作。
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'parameters'
4
+ require_relative 'responses'
4
5
 
5
6
  module Meta
6
7
  class Metadata
7
8
  module ExecutionMethods
8
9
  def parse_parameters(execution)
9
10
  parameters.filter(execution.request)
10
- # TODO: 未捕获 JsonSchema::ValidationErrors 异常
11
+ rescue JsonSchema::ValidationErrors => e
12
+ raise Errors::ParameterInvalid.new(e.errors)
11
13
  end
12
14
 
13
15
  def parse_request_body(execution, discard_missing: false)
@@ -16,7 +18,7 @@ module Meta
16
18
  execution.params(:raw),
17
19
  **Meta.config.json_schema_user_options,
18
20
  **Meta.config.json_schema_param_stage_user_options,
19
- **{ execution: execution, stage: :param, scope: @scope.concat([method]), discard_missing: discard_missing }.compact
21
+ **{ execution: execution, stage: :param, scope: @scope.concat(["$#{method}"]), discard_missing: discard_missing }.compact
20
22
  )
21
23
  rescue JsonSchema::ValidationErrors => e
22
24
  raise Errors::ParameterInvalid.new(e.errors)
@@ -70,7 +72,7 @@ module Meta
70
72
  @tags = tags
71
73
  @parameters = parameters.is_a?(Parameters) ? parameters : Parameters.new(parameters)
72
74
  @request_body = request_body
73
- @responses = responses || {} # || { 204 => nil }
75
+ @responses = responses.is_a?(Responses) ? responses : Responses.new(responses)
74
76
  @scope = scope
75
77
  end
76
78
 
@@ -88,7 +90,7 @@ module Meta
88
90
  operation_object[:parameters] = parameters.to_swagger_doc
89
91
 
90
92
  if request_body
91
- schema = request_body.to_schema_doc(stage: :param, scope: self.scope + scope, schemas: schemas)
93
+ schema = request_body.to_schema_doc(stage: :param, scope: self.scope + scope, schema_docs_mapping: schemas)
92
94
  if schema || true
93
95
  operation_object[:requestBody] = {
94
96
  content: {
@@ -100,16 +102,7 @@ module Meta
100
102
  end
101
103
  end
102
104
 
103
- operation_object[:responses] = responses.transform_values do |schema|
104
- {
105
- description: '', # description 属性必须存在
106
- content: schema ? {
107
- 'application/json' => {
108
- schema: schema.to_schema_doc(stage: :render, scope: self.scope + scope, schemas: schemas)
109
- }
110
- } : nil
111
- }.compact
112
- end unless responses.empty?
105
+ operation_object[:responses] = responses.to_swagger_doc(schemas, scope: self.scope + scope)
113
106
 
114
107
  operation_object.compact
115
108
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Meta
4
+ class Responses
5
+ extend Forwardable
6
+
7
+ attr_reader :responses
8
+
9
+ def initialize(responses = {})
10
+ @responses = responses || {}
11
+ end
12
+
13
+ def_delegators :@responses, :[], :empty?, :length, :keys
14
+
15
+ def to_swagger_doc(schemas, scope:)
16
+ if responses.empty?
17
+ { '200' => { description: '' } }
18
+ else
19
+ responses.transform_values do |schema|
20
+ {
21
+ description: '', # description 属性必须存在
22
+ content: schema ? {
23
+ 'application/json' => {
24
+ schema: schema.to_schema_doc(stage: :render, scope: scope, schema_docs_mapping: schemas)
25
+ }
26
+ } : nil
27
+ }.compact
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -39,7 +39,7 @@ module Meta
39
39
  end
40
40
 
41
41
  def generate_operation_doc(schemas)
42
- meta.generate_operation_doc(schemas, scope: [method])
42
+ meta.generate_operation_doc(schemas, scope: ["$#{method}"])
43
43
  end
44
44
  end
45
45
  end
data/lib/meta/config.rb CHANGED
@@ -4,7 +4,6 @@ require 'hash_to_struct'
4
4
 
5
5
  module Meta
6
6
  DEFAULT_OPTIONS = {
7
- default_locked_scope: nil,
8
7
  json_schema_user_options: {},
9
8
  json_schema_param_stage_user_options: {},
10
9
  json_schema_render_stage_user_options: {}
data/lib/meta/entity.rb CHANGED
@@ -14,9 +14,7 @@ module Meta
14
14
  def inherited(base)
15
15
  base.instance_eval do
16
16
  @schema_builder = JsonSchema::ObjectSchemaBuilder.new
17
- @schema_builder.schema_name(proc { |locked_scope, stage|
18
- generate_schema_name(locked_scope, stage)
19
- })
17
+ @schema_builder.schema_name(self.name) if self.name
20
18
  end
21
19
  end
22
20
 
@@ -32,6 +30,7 @@ module Meta
32
30
 
33
31
  private
34
32
 
33
+ # TODO: 不需要在 Entity 内自动生成名称了,交给 ObjectSchema::Naming 去做吧
35
34
  def generate_schema_name(stage, locked_scopes)
36
35
  # 匿名类不考虑自动生成名称
37
36
  return nil unless self.name
@@ -5,6 +5,30 @@ require_relative '../schemas/properties'
5
5
  module Meta
6
6
  module JsonSchema
7
7
  class ObjectSchemaBuilder
8
+ module LockedMethodAlias
9
+ # 我在这里说明一下 lock_scope 的逻辑。
10
+ # 1. lock_scope 实际上是将 scope 传递到当前的 ObjectSchema 和它的子 Schema 中。
11
+ # 2. lock_scope 会叠加,也就是如果子 schema 也有 lock_scope,那么子 Schema 会将两个 Schema 合并起来。
12
+ # 3. 调用 filter(scope:) 和 to_schema_doc(scope:) 时,可以传递 scope 参数,这个 scope 遇到 lock_scope 时会合并。
13
+ # 4. 这也就是说,在路由级别定义的 scope 宏会传递到下面的 Schema 中去。
14
+ def add_scope(scope)
15
+ lock_scope(scope)
16
+ end
17
+
18
+ def lock(key, value)
19
+ locked(key => value)
20
+ end
21
+
22
+ def method_missing(method, *args)
23
+ if method =~ /^lock_(\w+)$/
24
+ key = Regexp.last_match(1)
25
+ lock(key.to_sym, *args)
26
+ else
27
+ super
28
+ end
29
+ end
30
+ end
31
+
8
32
  def initialize(options = {}, &)
9
33
  raise 'type 选项必须是 object' if !options[:type].nil? && options[:type] != 'object'
10
34
 
@@ -23,23 +47,10 @@ module Meta
23
47
  instance_exec(&) if block_given?
24
48
  end
25
49
 
26
- # 设置 schema_name.
27
- #
28
- # 一、可以传递一个块,该块会接收 locked_scope 参数,需要返回一个带有 param 和 render 键的 Hash.
29
- # 二、可以传递一个 Hash,它包含 param 和 render 键。
30
- # 三、可以传递一个字符串。
31
- def schema_name(schema_name_resolver)
32
- if schema_name_resolver.is_a?(Proc)
33
- @schema_name_resolver = schema_name_resolver
34
- elsif schema_name_resolver.is_a?(Hash)
35
- @schema_name_resolver = proc { |stage, locked_scopes| schema_name_resolver[stage] }
36
- elsif schema_name_resolver.is_a?(String)
37
- @schema_name_resolver = proc { |stage, locked_scopes| schema_name_resolver }
38
- elsif schema_name_resolver.nil?
39
- @schema_name_resolver = proc { nil }
40
- else
41
- raise TypeError, "schema_name_resolver 必须是一个 Proc、Hash 或 String,当前是:#{schema_name_resolver.class}"
42
- end
50
+ def schema_name(schema_base_name)
51
+ raise TypeError, "schema_base_name 必须是一个 String,当前是:#{schema_base_name.class}" unless schema_base_name.is_a?(String)
52
+
53
+ @schema_name = schema_base_name
43
54
  end
44
55
 
45
56
  def property(name, options = {}, &block)
@@ -56,16 +67,14 @@ module Meta
56
67
  end
57
68
 
58
69
  def to_schema(locked_options = nil)
59
- ObjectSchema.new(properties: @properties, options: @options, locked_options: locked_options, schema_name_resolver: @schema_name_resolver)
60
- end
61
-
62
- def lock(key, value)
63
- locked(key => value)
70
+ properties = @schema_name ? NamedProperties.new(@properties, @schema_name) : Properties.new(@properties)
71
+ ObjectSchema.new(properties: properties, options: @options, locked_options: locked_options)
64
72
  end
65
73
 
66
74
  def locked(options)
67
- Locked.new(self, options)
75
+ Locked.new(self, **options)
68
76
  end
77
+ include LockedMethodAlias
69
78
 
70
79
  private
71
80
 
@@ -77,29 +86,25 @@ module Meta
77
86
  (options[:type] == 'object' || block) && (options[:properties] || block)
78
87
  end
79
88
 
80
- def method_missing(method, *args)
81
- if method =~ /^lock_(\w+)$/
82
- key = Regexp.last_match(1)
83
- lock(key.to_sym, *args)
84
- else
85
- super
86
- end
87
- end
88
-
89
89
  class Locked
90
- attr_reader :builder, :locked_options
90
+ attr_reader :object_schema_builder, :locked_options
91
91
 
92
- def initialize(builder, locked_options)
93
- @builder = builder
94
- @locked_options = locked_options
92
+ # locked_options 是用户传递的参数,这个参数会被合并到 object_schema_builder 的 locked_options 中。
93
+ def initialize(builder, scope: nil, discard_missing: nil, exclude: nil)
94
+ @object_schema_builder = builder
95
+ @locked_options = ObjectSchema::USER_OPTIONS_CHECKER.check({ scope: scope, discard_missing: discard_missing, exclude: exclude }.compact)
95
96
  end
96
97
 
97
- # 当调用 Entity.locked 方法后,生成 schema 的方法会掉到这里面来。
98
- # 在生成 schema 时,locked_options 会覆盖;当生成 schema 文档时,由于缺失 schema_name 的
99
- # 信息,故而 schema_name 相关的影响就消失不见了。
100
98
  def to_schema
101
- builder.to_schema(locked_options)
99
+ object_schema_builder.to_schema(locked_options)
100
+ end
101
+
102
+ def locked(options)
103
+ options = ObjectSchema::USER_OPTIONS_CHECKER.check(options)
104
+ options = ObjectSchema.merge_user_options(locked_options, options)
105
+ Locked.new(self.object_schema_builder, **options)
102
106
  end
107
+ include LockedMethodAlias
103
108
  end
104
109
  end
105
110
  end
@@ -14,11 +14,7 @@ module Meta
14
14
  permit_extras true
15
15
 
16
16
  key :ref, alias_names: [:using], normalizer: ->(entity) {
17
- if Meta.config.default_locked_scope && entity.is_a?(Class) && entity < Meta::Entity
18
- entity.locked(scope: Meta.config.default_locked_scope)
19
- else
20
- entity
21
- end
17
+ entity
22
18
  }
23
19
  key :dynamic_ref, alias_names: [:dynamic_using], normalizer: ->(value) { value.is_a?(Proc) ? { resolve: value } : value }
24
20
  end
@@ -34,7 +30,7 @@ module Meta
34
30
  elsif apply_object_schema?(options, block)
35
31
  ObjectSchemaBuilder.new(options, &block).to_schema
36
32
  else
37
- StagingSchema.build_from_options(options)
33
+ BaseSchema.new(options)
38
34
  end
39
35
  end
40
36
 
@@ -3,6 +3,8 @@
3
3
  module Meta
4
4
  module JsonSchema
5
5
  class ArraySchema < BaseSchema
6
+ extend Forwardable
7
+
6
8
  attr_reader :items
7
9
 
8
10
  def initialize(items, options = {})
@@ -22,10 +24,19 @@ module Meta
22
24
  schema
23
25
  end
24
26
 
27
+ def_delegator :@items, :defined_scopes
28
+
25
29
  private
26
30
 
27
31
  def filter_internal(array_value, user_options)
28
- raise ValidationError.new('参数应该传递一个数组') unless array_value.respond_to?(:each_with_index)
32
+ if array_value.respond_to?(:each_with_index)
33
+ array_value = array_value
34
+ elsif array_value.respond_to?(:to_a)
35
+ array_value = array_value.to_a
36
+ else
37
+ raise ValidationError.new('参数应该传递一个数组或者数组 Like 的对象(实现了 each_with_index 或者 to_a 方法)')
38
+ end
39
+
29
40
  array_value.each_with_index.map do |item, index|
30
41
  begin
31
42
  @items.filter(item, user_options)
@@ -103,6 +103,10 @@ module Meta
103
103
  self
104
104
  end
105
105
 
106
+ def defined_scopes(stage:, defined_scopes_mapping:)
107
+ []
108
+ end
109
+
106
110
  # 执行 if: 选项,返回 true 或 false
107
111
  def if?(user_options)
108
112
  return true if options[:if].nil?
@@ -123,7 +127,8 @@ module Meta
123
127
  #
124
128
  # 选项:
125
129
  # - stage: 传递 :param 或 :render
126
- # - schemas: 用于保存已经生成的 Schema
130
+ # - schema_docs_mapping: 用于保存已经生成的 Schema
131
+ # - defined_scopes_mapping: 用于缓存已经定义的 scopes
127
132
  # - presenter: 兼容 Grape 框架的实体类
128
133
  def to_schema_doc(**user_options)
129
134
  return Presenters.to_schema_doc(options[:presenter], options) if options[:presenter]
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require_relative 'properties'
5
+
6
+ module Meta
7
+ module JsonSchema
8
+ class NamedProperties < Properties
9
+ attr_reader :schema_base_name
10
+
11
+ def initialize(properties, schema_base_name)
12
+ super(properties)
13
+
14
+ raise TypeError, "schema_name 必须是一个 String,当前是:#{schema_base_name.class}" unless schema_base_name.is_a?(String)
15
+ raise ArgumentError, 'schema_name 不能为 nil 或空字符串' if schema_base_name.nil? || schema_base_name.empty?
16
+
17
+ # 修正 base_name,确保其不包含 Entity 后缀
18
+ schema_base_name = schema_base_name.delete_suffix('Entity') if schema_base_name&.end_with?('Entity')
19
+ @schema_base_name = schema_base_name
20
+ end
21
+
22
+ def schema_name(stage)
23
+ if stage == :render
24
+ "#{schema_base_name}Entity"
25
+ elsif stage == :param
26
+ "#{schema_base_name}Params"
27
+ else
28
+ raise ArgumentError, "stage 必须是 :render 或 :param,当前是:#{stage}"
29
+ end
30
+ end
31
+
32
+ def merge(other_properties)
33
+ raise UnsupportedError, 'NamedProperties 不支持 merge 操作'
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,61 +1,86 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../../utils/kwargs/check'
4
+ require_relative 'named_properties'
4
5
 
5
6
  module Meta
6
7
  module JsonSchema
7
8
  class ObjectSchema < BaseSchema
8
- attr_reader :properties, :locked_options
9
+ attr_reader :properties
10
+ # 只有 ObjectSchema 对象才有 locked_options,因为 locked_options 多是用来锁定属性的行为的,包括:
11
+ # scope:、discard_missing:、exclude: 等
12
+ attr_reader :locked_options
9
13
 
14
+ # stage 和 scope 选项在两个 CHECKER 下都用到了
10
15
  USER_OPTIONS_CHECKER = Utils::KeywordArgs::Builder.build do
11
- permit_extras true
12
-
16
+ key :stage
17
+ key :scope, normalizer: ->(value) {
18
+ raise ArgumentError, 'scope 选项不可传递 nil' if value.nil?
19
+ value = [value] unless value.is_a?(Array)
20
+ value
21
+ }
22
+ key :discard_missing, :exclude, :extra_properties, :type_conversion, :validation
23
+ key :execution, :user_data, :object_value
24
+ end
25
+ TO_SCHEMA_DOC_CHECKER = Utils::KeywordArgs::Builder.build do
26
+ key :stage
13
27
  key :scope, normalizer: ->(value) {
14
28
  raise ArgumentError, 'scope 选项不可传递 nil' if value.nil?
15
29
  value = [value] unless value.is_a?(Array)
16
30
  value
17
31
  }
32
+ key :schema_docs_mapping, :defined_scopes_mapping
18
33
  end
19
34
 
20
- def initialize(properties: nil, options: {}, locked_options: {}, schema_name_resolver: proc { nil })
35
+ def initialize(properties:, options: {}, locked_options: {})
36
+ raise ArgumentError, 'properties 必须是 Properties 实例' unless properties.is_a?(Properties)
37
+
21
38
  super(options)
22
39
 
23
40
  @properties = properties || Properties.new({}) # property 包含 stage,stage 包含 scope、schema
24
41
  @properties = Properties.new(@properties) if @properties.is_a?(Hash)
25
42
  @locked_options = USER_OPTIONS_CHECKER.check(locked_options || {})
26
- @schema_name_resolver = schema_name_resolver || proc { nil }
27
43
  end
28
44
 
29
45
  # 复制一个新的 ObjectSchema,只有 options 不同
30
46
  def dup(options)
47
+ raise UnsupportedError, 'dup 不应该再执行了'
48
+
31
49
  self.class.new(
32
50
  properties: properties,
33
51
  options: options,
34
52
  locked_options: locked_options,
35
- schema_name_resolver: @schema_name_resolver
36
53
  )
37
54
  end
38
55
 
39
56
  def filter(object_value, user_options = {})
40
57
  # 合并 user_options
41
58
  user_options = USER_OPTIONS_CHECKER.check(user_options)
42
- user_options = merge_user_options(user_options, locked_options) if locked_options
59
+ user_options = self.class.merge_user_options(user_options, locked_options) if locked_options
43
60
  super
44
61
  end
45
62
 
46
- # 合并其他的属性,并返回一个新的 ObjectSchema
47
- def merge_other_properties(properties)
48
- ObjectSchema.new(properties: self.properties.merge(properties))
63
+ def naming?
64
+ properties.is_a?(NamedProperties)
49
65
  end
50
66
 
51
- def resolve_name(stage)
67
+ def resolve_name(stage, user_scopes, defined_scopes)
68
+ raise ArgumentError, 'stage 不能为 nil' if stage.nil?
69
+
70
+ # 先合成外面传进来的 scope
52
71
  locked_scopes = (locked_options || {})[:scope] || []
53
- @schema_name_resolver.call(stage, locked_scopes)
72
+ user_scopes = (user_scopes + locked_scopes).uniq
73
+ scopes = user_scopes & defined_scopes
74
+
75
+ # 再根据 stage 和 scope 生成为当前的 Schema 生成一个合适的名称,要求保证唯一性
76
+ schema_name = properties.schema_name(stage)
77
+ schema_name += "__#{scopes.join('__')}" unless scopes.empty?
78
+ schema_name
54
79
  end
55
80
 
56
81
  def to_schema_doc(user_options = {})
57
- user_options = USER_OPTIONS_CHECKER.check(user_options)
58
- user_options = merge_user_options(user_options, locked_options) if locked_options
82
+ user_options = TO_SCHEMA_DOC_CHECKER.check(user_options)
83
+ user_options = self.class.merge_user_options(user_options, locked_options) if locked_options
59
84
 
60
85
  schema = { type: 'object' }
61
86
  schema[:description] = options[:description] if options[:description]
@@ -80,7 +105,7 @@ module Meta
80
105
  @properties.filter(object_value, user_options)
81
106
  end
82
107
 
83
- def merge_user_options(user_options, locked_options)
108
+ def self.merge_user_options(user_options, locked_options)
84
109
  user_options.merge(locked_options) do |key, user_value, locked_value|
85
110
  if key == :scope
86
111
  user_value + locked_value
@@ -11,7 +11,11 @@ module Meta
11
11
  @properties = properties
12
12
  end
13
13
 
14
+ # user_options 包括 stage, scope, extra_properties, discard_missing, exclude、execution、user_data
14
15
  def filter(object_value, user_options = {})
16
+ # 首先,要将 object_value 转化为 ObjectWrapper
17
+ object_value = JsonObject.wrap(object_value)
18
+
15
19
  # 第一步,根据 user_options[:scope] 需要过滤一些字段
16
20
  stage = user_options[:stage]
17
21
  # 传递一个数字;因为 scope 不能包含数字,这里传递一个数字,使得凡是配置 scope 的属性都会被过滤
@@ -65,6 +69,7 @@ module Meta
65
69
  end
66
70
  end
67
71
 
72
+ # user_options 包括 stage, scope, schema_docs_mapping, defined_scopes_mapping
68
73
  def to_swagger_doc(scope: [], stage: nil, **user_options)
69
74
  locked_scopes = scope
70
75
  properties = filter_by(stage: stage, user_scope: locked_scopes)
@@ -78,7 +83,7 @@ module Meta
78
83
  end
79
84
 
80
85
  # 程序中有些地方用到了这三个方法
81
- def_delegators :@properties, :empty?, :key?, :[]
86
+ def_delegators :@properties, :empty?, :key?, :[], :each
82
87
 
83
88
  def merge(properties)
84
89
  self.class.new(@properties.merge(properties.instance_eval { @properties }))
@@ -101,7 +106,7 @@ module Meta
101
106
  def resolve_property_value(object_value, name, property_schema)
102
107
  if property_schema.value?
103
108
  nil
104
- elsif object_value.is_a?(Hash) || object_value.is_a?(ObjectWrapper)
109
+ elsif object_value.is_a?(Hash) || object_value.is_a?(JsonObject)
105
110
  object_value.key?(name.to_s) ? object_value[name.to_s] : object_value[name.to_sym]
106
111
  else
107
112
  raise "不应该还有其他类型了,已经在类型转换中将其转换为 Meta::JsonSchema::ObjectWrapper 了"
@@ -5,31 +5,58 @@ require_relative 'base_schema'
5
5
  module Meta
6
6
  module JsonSchema
7
7
  class RefSchema < BaseSchema
8
- attr_reader :schema
8
+ attr_reader :object_schema
9
+
10
+ def initialize(object_schema, options = {})
11
+ raise ArgumentError, 'object_schema 必须是一个 ObjectSchema' unless object_schema.is_a?(ObjectSchema)
9
12
 
10
- def initialize(schema, options = {})
11
13
  super(options)
12
- @schema = schema
14
+ @object_schema = object_schema
13
15
  end
14
16
 
15
17
  def filter(value, user_options = {})
16
18
  value = super
17
- schema.filter(value, user_options)
19
+ object_schema.filter(value, user_options)
18
20
  end
19
21
 
20
22
  def to_schema_doc(user_options)
21
- schema_name = schema.resolve_name(user_options[:stage])
23
+ raise '引用的 ObjectSchema 没有包含命名逻辑,无法生成文档' unless object_schema.naming?
22
24
 
23
- # 首先将 Schema 写进 schemas 选项中去
24
- schema_components = user_options[:schemas]
25
+ # 首先,要求出 defined_scopes
26
+ defined_scopes = self.defined_scopes(stage: user_options[:stage], defined_scopes_mapping: user_options[:defined_scopes_mapping])
27
+ # 然后,求出 schema_name
28
+ schema_name = object_schema.resolve_name(user_options[:stage], user_options[:scope], defined_scopes)
29
+ # 接着将 Schema 写进 schemas 选项中去
30
+ schema_components = user_options[:schema_docs_mapping] || {}
25
31
  unless schema_components.key?(schema_name)
26
32
  schema_components[schema_name] = nil # 首先设置 schemas 防止出现无限循环
27
- schema_components[schema_name] = schema.to_schema_doc(**user_options) # 原地修改 schemas,无妨
33
+ schema_components[schema_name] = object_schema.to_schema_doc(**user_options) # 原地修改 schemas,无妨
28
34
  end
29
35
 
30
- # 返回的是 $ref 结构
36
+ # 最后,返回这个 $ref 结构
31
37
  { '$ref': "#/components/schemas/#{schema_name}" }
32
38
  end
39
+
40
+ def defined_scopes(stage:, defined_scopes_mapping:)
41
+ defined_scopes_mapping ||= {}
42
+
43
+ schema_name = object_schema.properties.schema_name(stage)
44
+ return defined_scopes_mapping[schema_name] if defined_scopes_mapping.key?(schema_name)
45
+
46
+ defined_scopes_mapping[schema_name] = []
47
+ defined_scopes = object_schema.properties.each.map do |name, property|
48
+ property.defined_scopes(stage: stage, defined_scopes_mapping: defined_scopes_mapping)
49
+ end.flatten.uniq.sort
50
+ defined_scopes_mapping[schema_name] = defined_scopes
51
+ defined_scopes
52
+ end
53
+
54
+ private
55
+
56
+ # # TODO: 这种带有组合方式的 Schema,让我联想到,每次 BaseSchema 新增一个方法都要在子 Schema 中加一遍,很烦!
57
+ # def defined_scopes
58
+ # schema.defined_scopes
59
+ # end
33
60
  end
34
61
  end
35
62
  end
@@ -1,39 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../support/scope_matcher'
4
+
3
5
  module Meta
4
6
  module JsonSchema
5
7
  class ScopingSchema < BaseSchema
6
- attr_reader :on, :schema
7
-
8
- def initialize(required_scope: [], schema:)
9
- raise ArgumentError, 'required_scope 选项不可传递 nil' if required_scope.nil?
10
- required_scope = [required_scope] unless required_scope.is_a?(Array)
8
+ attr_reader :scope_matcher, :schema
11
9
 
12
- @on = required_scope
10
+ def initialize(scope_matcher_options: , schema:)
11
+ @scope_matcher = ScopeMatcher.new(scope_matcher_options)
13
12
  @schema = schema
14
13
  end
15
14
 
16
15
  def scoped(user_scopes)
17
- return schema if (on - user_scopes).empty? # required_scopes 应被消耗殆尽
16
+ @scope_matcher.match?(user_scopes) ? schema : unsupported_schema(user_scopes)
17
+ end
18
18
 
19
- UnsupportedSchema.new(:on, user_scopes)
19
+ def defined_scopes(**kwargs)
20
+ scope_matcher.defined_scopes
20
21
  end
21
22
 
22
- STAGING_SCHEMA_OPTIONS = Utils::KeywordArgs::Builder.build do
23
- permit_extras true
23
+ private
24
24
 
25
- # TODO: 如果我想把 on 改名为 require_scopes,关键字参数的机制是否支持?
26
- key :on, alias_names: [:scope], default: [], normalizer: ->(required_scopes) {
27
- required_scopes = [] if required_scopes.nil?
28
- required_scopes = [required_scopes] unless required_scopes.is_a?(Array)
29
- required_scopes
30
- }
25
+ def unsupported_schema(user_scopes)
26
+ UnsupportedSchema.new(:scope, user_scopes)
31
27
  end
28
+
32
29
  def self.build_from_options(options, build_schema)
33
- options = STAGING_SCHEMA_OPTIONS.check(options)
34
- required_scope = options.delete(:on) || []
30
+ options = options.dup
31
+ scope_matcher_options = options.delete(:scope)
35
32
  schema = build_schema.call(options)
36
- schema = ScopingSchema.new(required_scope: required_scope, schema: schema) unless required_scope.empty?
33
+ schema = ScopingSchema.new(scope_matcher_options: scope_matcher_options, schema: schema) if scope_matcher_options
37
34
  schema
38
35
  end
39
36
  end
@@ -9,16 +9,14 @@ module Meta
9
9
  module JsonSchema
10
10
  # 内含 param_schema, render_schema, default_schema,分别用于不同的阶段。
11
11
  class StagingSchema < BaseSchema
12
- attr_reader :param_schema, :render_schema, :default_schema
12
+ attr_reader :param_schema, :render_schema
13
13
 
14
- def initialize(param_schema:, render_schema:, default_schema:)
14
+ def initialize(param_schema:, render_schema:)
15
15
  raise ArgumentError, 'param_schema 选项重复提交为 StagingSchema' if param_schema.is_a?(StagingSchema)
16
16
  raise ArgumentError, 'render_schema 选项重复提交为 StagingSchema' if render_schema.is_a?(StagingSchema)
17
- raise ArgumentError, 'default_schema 选项重复提交为 StagingSchema' if default_schema.is_a?(StagingSchema)
18
17
 
19
18
  @param_schema = param_schema
20
19
  @render_schema = render_schema
21
- @default_schema = default_schema
22
20
  end
23
21
 
24
22
  def filter(value, user_options = {})
@@ -27,7 +25,7 @@ module Meta
27
25
  elsif user_options[:stage] == :render
28
26
  render_schema.filter(value, user_options)
29
27
  else
30
- default_schema.filter(value, user_options)
28
+ raise ArgumentError, "stage 选项必须是 :param 或 :render"
31
29
  end
32
30
  end
33
31
 
@@ -37,10 +35,14 @@ module Meta
37
35
  elsif stage == :render
38
36
  render_schema
39
37
  else
40
- default_schema
38
+ raise ArgumentError, "stage 选项必须是 :param 或 :render"
41
39
  end
42
40
  end
43
41
 
42
+ def defined_scopes(stage:, **kwargs)
43
+ staged(stage).defined_scopes(stage: stage, **kwargs)
44
+ end
45
+
44
46
  def self.build_from_options(options, build_schema = ->(opts) { BaseSchema.new(opts) })
45
47
  param_opts, render_opts, common_opts = SchemaOptions.divide_to_param_and_render(options)
46
48
  if param_opts == common_opts && render_opts == common_opts
@@ -48,9 +50,8 @@ module Meta
48
50
  else
49
51
  StagingSchema.new(
50
52
  param_schema: param_opts ? ScopingSchema.build_from_options(param_opts, build_schema) : UnsupportedSchema.new(:stage, :param),
51
- render_schema: render_opts ? ScopingSchema.build_from_options(render_opts, build_schema) : UnsupportedSchema.new(:stage, :render),
52
- default_schema: ScopingSchema.build_from_options(common_opts, build_schema),
53
- )
53
+ render_schema: render_opts ? ScopingSchema.build_from_options(render_opts, build_schema) : UnsupportedSchema.new(:stage, :render)
54
+ )
54
55
  end
55
56
  end
56
57
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # 将 Ruby 类型包装成 JsonObject 类型,以便可以通过 [key] 访问。同时,保留其他方法的调用,将其转发到原始对象上。
4
+ module Meta
5
+ module JsonSchema
6
+ class JsonObject
7
+ def initialize(target)
8
+ @target = target
9
+ end
10
+
11
+ def __target__
12
+ @target
13
+ end
14
+
15
+ def key?(key)
16
+ @target.respond_to?(key)
17
+ end
18
+
19
+ def [](key)
20
+ @target.__send__(key)
21
+ end
22
+
23
+ def method_missing(method, *args)
24
+ @target.__send__(method, *args)
25
+ end
26
+
27
+ def self.wrap(target)
28
+ case target
29
+ when JsonObject, Hash
30
+ target
31
+ else
32
+ new(target)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Meta
4
+ module JsonSchema
5
+ class ScopeMatcher
6
+ attr_reader :defined_scopes
7
+
8
+ def initialize(query_clause)
9
+ query_clause = [query_clause] if query_clause.is_a?(String) || query_clause.is_a?(Symbol)
10
+ query_clause = { some_of: query_clause } if query_clause.is_a?(Array)
11
+
12
+ @match_type, @defined_scopes = query_clause.first
13
+ end
14
+
15
+ def match?(scopes)
16
+ return false if scopes.empty?
17
+
18
+ case @match_type
19
+ when :some_of
20
+ (@defined_scopes & scopes).any?
21
+ when :all_of
22
+ (@defined_scopes - scopes).empty?
23
+ else
24
+ raise "Unknown match type: #{@match_type}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,31 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bigdecimal'
4
+ require_relative 'json_object'
4
5
 
5
6
  module Meta
6
7
  module JsonSchema
7
- class ObjectWrapper
8
- def initialize(target)
9
- @target = target
10
- end
11
-
12
- def __target__
13
- @target
14
- end
15
-
16
- def key?(key)
17
- @target.respond_to?(key)
18
- end
19
-
20
- def [](key)
21
- @target.__send__(key)
22
- end
23
-
24
- def method_missing(method, *args)
25
- @target.__send__(method, *args)
26
- end
27
- end
28
-
29
8
  module TypeConverter
30
9
  # 定义客户类型对应的 Ruby 类型
31
10
  @definity_types = {
@@ -34,7 +13,7 @@ module Meta
34
13
  'number' => [Integer, Float, BigDecimal],
35
14
  'string' => [String],
36
15
  'array' => [Array],
37
- 'object' => [Hash, ObjectWrapper]
16
+ 'object' => [Hash, JsonObject]
38
17
  }
39
18
 
40
19
  # 定义从 Ruby 类型转化为对应类型的逻辑
@@ -101,7 +80,7 @@ module Meta
101
80
  raise TypeConvertError, I18n.t(:'json_schema.errors.type_convert.object', value: value, real_type: I18n.t(:'json_schema.type_names.array'))
102
81
  end
103
82
 
104
- ObjectWrapper.new(value)
83
+ JsonObject.new(value)
105
84
  end
106
85
  }
107
86
 
@@ -5,7 +5,7 @@ module Meta
5
5
  class << self
6
6
  def generate(application, info: {}, servers: [])
7
7
  paths_and_routes = get_paths_and_routes!(application)
8
- return generate_from_paths_and_routes(paths_and_routes, info: info, servers: servers)
8
+ generate_from_paths_and_routes(paths_and_routes, info: info, servers: servers)
9
9
  end
10
10
 
11
11
  def generate_from_paths_and_routes(paths_and_routes, info: {}, servers: [])
@@ -62,28 +62,5 @@ module Meta
62
62
  store_routes
63
63
  end
64
64
  end
65
-
66
- class Path
67
- def initialize(parts = [])
68
- @parts = parts.freeze
69
- end
70
-
71
- def append(part)
72
- part = part[1..-1] if part.start_with?('/')
73
- parts = part.split('/')
74
-
75
- self.class.new(@parts + parts)
76
- end
77
-
78
- def to_s
79
- '/' + @parts.join('/')
80
- end
81
-
82
- def self.from_string(path)
83
- path = path[1..-1] if path.start_with?('/')
84
- parts = path.split('/')
85
- self.class.new(parts)
86
- end
87
- end
88
65
  end
89
66
  end
@@ -58,20 +58,20 @@ module Meta
58
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
- # TODO: 当且仅当 value 是 nil 时,才使用 default
62
- @normalizer = default ? ->(value) { normalizer.call(value || default) } : normalizer
61
+ @default_value = default
62
+ @normalizer = normalizer
63
63
  @validator = validator
64
64
  end
65
65
 
66
66
  def consume(final_args, args)
67
67
  @consumer_names.each do |name|
68
- return true if consume_name(final_args, args, name)
68
+ return if consume_name(final_args, args, name)
69
69
  end
70
- return false
70
+
71
+ final_args[@key_name] = @default_value unless @default_value.nil?
71
72
  end
72
73
 
73
74
  def consume_name(final_args, args, consumer_name)
74
- # TODO: default 未起作用
75
75
  if args.key?(consumer_name)
76
76
  value = @normalizer.call(args.delete(consumer_name))
77
77
  @validator.call(value) if @validator
@@ -10,7 +10,7 @@ module Meta
10
10
  final_options[:parameters] = options1[:parameters].merge(options2[:parameters])
11
11
  end
12
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)
13
+ final_options[:request_body] = options1[:request_body].properties.merge(options2[:request_body].properties)
14
14
  end
15
15
  if options1[:responses] && options2[:responses]
16
16
  final_options[:responses] = options1[:responses].merge(options2[:responses])
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.9"
3
+ spec.version = "0.1.0"
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.9
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - yetrun
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-22 00:00:00.000000000 Z
11
+ date: 2023-08-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hash_to_struct
@@ -42,6 +42,7 @@ files:
42
42
  - lib//meta/application/metadata.rb
43
43
  - lib//meta/application/parameters.rb
44
44
  - lib//meta/application/path_matching_mod.rb
45
+ - lib//meta/application/responses.rb
45
46
  - lib//meta/application/route.rb
46
47
  - lib//meta/config.rb
47
48
  - lib//meta/entity.rb
@@ -56,6 +57,7 @@ files:
56
57
  - lib//meta/json_schema/schemas/array_schema.rb
57
58
  - lib//meta/json_schema/schemas/base_schema.rb
58
59
  - lib//meta/json_schema/schemas/dynamic_schema.rb
60
+ - lib//meta/json_schema/schemas/named_properties.rb
59
61
  - lib//meta/json_schema/schemas/object_schema.rb
60
62
  - lib//meta/json_schema/schemas/properties.rb
61
63
  - lib//meta/json_schema/schemas/ref_schema.rb
@@ -63,8 +65,10 @@ files:
63
65
  - lib//meta/json_schema/schemas/staging_schema.rb
64
66
  - lib//meta/json_schema/schemas/unsupported_schema.rb
65
67
  - lib//meta/json_schema/support/errors.rb
68
+ - lib//meta/json_schema/support/json_object.rb
66
69
  - lib//meta/json_schema/support/presenters.rb
67
70
  - lib//meta/json_schema/support/schema_options.rb
71
+ - lib//meta/json_schema/support/scope_matcher.rb
68
72
  - lib//meta/json_schema/support/type_converter.rb
69
73
  - lib//meta/json_schema/support/validators.rb
70
74
  - lib//meta/load_i18n.rb