meta-api 0.1.0 → 0.1.2

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: bf3fb639df71a5353328e898711c9758c5e5fd3dd1760409ae823fbd0a7852e2
4
- data.tar.gz: ea5dd728c3691fcf542d1085fd21d787d727c97b588652de3ffdd53b67512437
3
+ metadata.gz: 55ec631feb0038911007c4b1de6ca26fdf595ea84c41eecac046e417ff90a2e2
4
+ data.tar.gz: e384752c6ce459cb38adb4123078f10ad835c8f75a73256fddb1af02a8bac3b0
5
5
  SHA512:
6
- metadata.gz: 4a9f0a02d8091f77e7f9c2ea0ea67b86f1877327a7b75a7f4342fecd7f2822f9bde2ded59c20cb1c3227eb88a4b6b098c9438710a6a8eedcf4c8defb2f37c572
7
- data.tar.gz: 2e7911858914bd1b34ff60007f48757f6afb1183c59d15d9e490fb5fe781244522d5500251dcbdbb02fc5f26a17d48ab82bbc2f6dd4368c2540f97bb351f6da3
6
+ metadata.gz: 19c21dc3c99bebf4adfaea519ffb44d901481c8b88349b1b87677e3155a1c5e75e4c1e639596f589a28c32192e3ed10330f7d3b85dbc4e7238af73b0d8c996ef
7
+ data.tar.gz: 3903b5879a3d4dc1092a32b41aa17891434363089a48225acef22267ad6a56416ba7576c6de6ebb4bb8bbb50ebe2bebc7cb5592cf3265bccafe0e3c98e3c670c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # 更新日志
2
2
 
3
+ ## 0.1.2(2024 年 6 月 5 日)
4
+
5
+ 1. 实现新的基于常量的 Scope 场景化模式。
6
+ 2. 属性的 `scope:` 选项如果传递的是字符串数组,则恢复为 `或` 模式。
7
+ 3. 取消 HTTP method 自动生成对应的 Scope常量。
8
+ 4. 修复 `locked(discard_missing: true)` 时生成文档报错。
9
+ 5. 属性新增 `enum:` 选项,`allowable:` 作为其别名存在。
10
+
11
+ ## 0.1.1(2024 年 6 月 1 日)
12
+
13
+ 1. 添加 `Meta::Entity.with_common_options` 方法,用于更有效地组织字段。
14
+ 2. 临时性地添加 `Meta::Entity.merge` 方法,作为合并其他的实体的暂时性实现。
15
+ 3. scope 分为全局 scope 和局部 scope.
16
+
3
17
  ## 0.1.0(2023 年 8 月 5 日)
4
18
 
5
19
  1. 删除 `on:` 选项。
@@ -6,7 +6,7 @@ zh-CN:
6
6
  errors:
7
7
  required: "未提供"
8
8
  format: "格式不正确"
9
- allowable: "不在允许的值范围内,允许的值包括 [%{allowable_values}]"
9
+ enum: "不在允许的值范围内,允许的值包括 [%{allowable_values}]"
10
10
  type_convert:
11
11
  basic: "类型转化失败,期望得到一个 `%{target_type}` 类型,但值 `%{value}` 无法转化"
12
12
  array: "转化为数组类型时期望对象拥有 `to_a` 方法"
data/lib/meta/api.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require_relative 'config'
2
2
  require_relative 'application'
3
3
  require_relative 'swagger_doc'
4
+ require_relative 'scope/base'
4
5
  require_relative 'load_i18n'
@@ -13,23 +13,21 @@ module Meta
13
13
  end
14
14
 
15
15
  def parse_request_body(execution, discard_missing: false)
16
- method = execution.request.request_method.downcase.to_sym
17
16
  request_body.filter(
18
17
  execution.params(:raw),
19
18
  **Meta.config.json_schema_user_options,
20
19
  **Meta.config.json_schema_param_stage_user_options,
21
- **{ execution: execution, stage: :param, scope: @scope.concat(["$#{method}"]), discard_missing: discard_missing }.compact
20
+ **{ execution: execution, stage: :param, scope: @scope, discard_missing: discard_missing }.compact
22
21
  )
23
22
  rescue JsonSchema::ValidationErrors => e
24
23
  raise Errors::ParameterInvalid.new(e.errors)
25
24
  end
26
25
 
27
26
  def render_entity(execution, entity_schema, value, user_options)
28
- method = execution.request.request_method.downcase.to_sym
29
27
  entity_schema.filter(value,
30
28
  **Meta.config.json_schema_user_options,
31
29
  **Meta.config.json_schema_render_stage_user_options,
32
- **{ execution: execution, stage: :render, scope: @scope.concat([method]) }.compact,
30
+ **{ execution: execution, stage: :render, scope: @scope }.compact,
33
31
  **user_options,
34
32
  )
35
33
  end
@@ -39,7 +39,8 @@ module Meta
39
39
  end
40
40
 
41
41
  def generate_operation_doc(schemas)
42
- meta.generate_operation_doc(schemas, scope: ["$#{method}"])
42
+ # 不再自动添加 $post 等 scope
43
+ meta.generate_operation_doc(schemas, scope: [])
43
44
  end
44
45
  end
45
46
  end
data/lib/meta/entity.rb CHANGED
@@ -14,43 +14,17 @@ 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(self.name) if self.name
17
+ if self.name
18
+ mod_names = self.name.split("::")
19
+ mod_names.shift if mod_names.first == "Entities"
20
+ schema_name = mod_names.join('_')
21
+ @schema_builder.schema_name(schema_name)
22
+ end
18
23
  end
19
24
  end
20
25
 
21
- def_delegators :schema_builder, :property, :param, :expose, :use, :lock, :locked, :schema_name, :to_schema
22
-
23
- def method_missing(method, *args)
24
- if method =~ /^lock_(\w+)$/
25
- schema_builder.send(method, *args)
26
- else
27
- super
28
- end
29
- end
30
-
31
- private
32
-
33
- # TODO: 不需要在 Entity 内自动生成名称了,交给 ObjectSchema::Naming 去做吧
34
- def generate_schema_name(stage, locked_scopes)
35
- # 匿名类不考虑自动生成名称
36
- return nil unless self.name
37
-
38
- schema_name = self.name.gsub('::', '_')
39
- schema_name = schema_name.delete_suffix('Entity') unless schema_name == 'Entity'
40
-
41
- # 先考虑 stage
42
- case stage
43
- when :param
44
- schema_name += 'Params'
45
- when :render
46
- schema_name += 'Entity'
47
- end
48
-
49
- # 再考虑 locked_scope
50
- scope_suffix = locked_scopes.join('_')
51
- schema_name = "#{schema_name}_#{scope_suffix}" unless scope_suffix.empty?
52
-
53
- schema_name
26
+ def method_missing(method, *args, **kwargs, &)
27
+ schema_builder.send(method, *args, **kwargs, &)
54
28
  end
55
29
  end
56
30
  end
@@ -5,6 +5,8 @@ require_relative '../schemas/properties'
5
5
  module Meta
6
6
  module JsonSchema
7
7
  class ObjectSchemaBuilder
8
+ extend Forwardable
9
+
8
10
  module LockedMethodAlias
9
11
  # 我在这里说明一下 lock_scope 的逻辑。
10
12
  # 1. lock_scope 实际上是将 scope 传递到当前的 ObjectSchema 和它的子 Schema 中。
@@ -29,10 +31,65 @@ module Meta
29
31
  end
30
32
  end
31
33
 
34
+ class Locked
35
+ attr_reader :object_schema_builder, :locked_options
36
+
37
+ # locked_options 是用户传递的参数,这个参数会被合并到 object_schema_builder 的 locked_options 中。
38
+ def initialize(builder, scope: nil, discard_missing: nil, exclude: nil)
39
+ @object_schema_builder = builder
40
+ @locked_options = ObjectSchema::USER_OPTIONS_CHECKER.check({ scope: scope, discard_missing: discard_missing, exclude: exclude }.compact)
41
+ end
42
+
43
+ def to_schema
44
+ object_schema_builder.to_schema(locked_options)
45
+ end
46
+
47
+ def locked(options)
48
+ options = ObjectSchema::USER_OPTIONS_CHECKER.check(options)
49
+ options = ObjectSchema.merge_user_options(locked_options, options)
50
+ Locked.new(self.object_schema_builder, **options)
51
+ end
52
+ include LockedMethodAlias
53
+ end
54
+
55
+ class WithCommonOptions
56
+ attr_reader :object_schema_builder, :common_options
57
+
58
+ def initialize(builder, common_options, &)
59
+ @object_schema_builder = builder
60
+ @common_options = common_options
61
+
62
+ instance_exec(&) if block_given?
63
+ end
64
+
65
+ def property(name, options = {}, &block)
66
+ options = merge_options(options)
67
+ object_schema_builder.property(name, options, &block)
68
+ end
69
+
70
+ def merge_options(options)
71
+ self.class.merge_options(common_options, options)
72
+ end
73
+
74
+ def self.merge_options(common_options, options)
75
+ common_options.merge(options) do |key, oldVal, newVal|
76
+ if key == :scope
77
+ # 合并 common_options 和 options 中的 scope 选项
78
+ Scope::Utils.and(oldVal, newVal)
79
+ else
80
+ # 关于 param、render 内部选项的合并问题暂不考虑
81
+ newVal
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ attr_reader :properties
88
+
32
89
  def initialize(options = {}, &)
33
90
  raise 'type 选项必须是 object' if !options[:type].nil? && options[:type] != 'object'
34
91
 
35
- @properties = {}
92
+ @properties = {} # 所有的属性已经生成
36
93
  @required = []
37
94
  @validations = {}
38
95
 
@@ -47,10 +104,13 @@ module Meta
47
104
  instance_exec(&) if block_given?
48
105
  end
49
106
 
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
107
+ def schema_name(schema_base_name = nil)
108
+ if schema_base_name
109
+ raise TypeError, "schema_base_name 必须是一个 String,当前是:#{schema_base_name.class}" unless schema_base_name.is_a?(String)
110
+ @schema_name = schema_base_name
111
+ else
112
+ @schema_name
113
+ end
54
114
  end
55
115
 
56
116
  def property(name, options = {}, &block)
@@ -66,12 +126,50 @@ module Meta
66
126
  instance_exec(&proc)
67
127
  end
68
128
 
129
+ def with_common_options(common_options, &block)
130
+ WithCommonOptions.new(self, common_options, &block)
131
+ end
132
+
133
+ def scope(scope, options = {}, &)
134
+ with_common_options(**options, scope: scope, &)
135
+ end
136
+
137
+ def params(options = {}, &block)
138
+ with_common_options(**options, render: false, &block)
139
+ end
140
+
141
+ def render(options = {}, &block)
142
+ with_common_options(**options, params: false, &block)
143
+ end
144
+
145
+ def merge(schema_builder)
146
+ schema_builder = schema_builder.schema_builder if schema_builder.respond_to?(:schema_builder)
147
+
148
+ @properties.merge!(schema_builder.properties)
149
+ end
150
+
69
151
  def to_schema(locked_options = nil)
70
152
  properties = @schema_name ? NamedProperties.new(@properties, @schema_name) : Properties.new(@properties)
71
153
  ObjectSchema.new(properties: properties, options: @options, locked_options: locked_options)
72
154
  end
73
155
 
74
156
  def locked(options)
157
+ # defined_scopes_mapping = {}
158
+ # # TODO: 将 properties 搞成 Properties 可以吗?
159
+ # defined_scopes = properties.map do |key, property|
160
+ # property.defined_scopes(stage: :param, defined_scopes_mapping: defined_scopes_mapping)
161
+ # end.flatten.uniq
162
+ #
163
+ # user_scopes = options[:scope] || []
164
+ # user_scopes = [user_scopes] unless user_scopes.is_a?(Array)
165
+ #
166
+ # # 判断 user_scopes 中提供的局部 scope 是否在 defined_scopes 中
167
+ # local_user_scopes = user_scopes.reject { |scope| scope.start_with?('$') }
168
+ # if (local_user_scopes - defined_scopes).any?
169
+ # extra_scopes = local_user_scopes - defined_scopes
170
+ # raise ArgumentError, "scope #{extra_scopes.join(',')} 未在实体中定义"
171
+ # end
172
+
75
173
  Locked.new(self, **options)
76
174
  end
77
175
  include LockedMethodAlias
@@ -85,27 +183,6 @@ module Meta
85
183
  def apply_object_scope?(options, block)
86
184
  (options[:type] == 'object' || block) && (options[:properties] || block)
87
185
  end
88
-
89
- class Locked
90
- attr_reader :object_schema_builder, :locked_options
91
-
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)
96
- end
97
-
98
- def to_schema
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)
106
- end
107
- include LockedMethodAlias
108
- end
109
186
  end
110
187
  end
111
- end
188
+ end
@@ -13,9 +13,7 @@ module Meta
13
13
  SCHEMA_BUILDER_OPTIONS = Utils::KeywordArgs::Builder.build do
14
14
  permit_extras true
15
15
 
16
- key :ref, alias_names: [:using], normalizer: ->(entity) {
17
- entity
18
- }
16
+ key :ref, alias_names: [:using], normalizer: ->(entity) { entity }
19
17
  key :dynamic_ref, alias_names: [:dynamic_using], normalizer: ->(value) { value.is_a?(Proc) ? { resolve: value } : value }
20
18
  end
21
19
  def build(options = {}, &block)
@@ -22,7 +22,9 @@ module Meta
22
22
  class BaseSchema
23
23
  OPTIONS_CHECKER = Utils::KeywordArgs::Builder.build do
24
24
  key :type, :items, :description, :presenter, :value, :default, :properties, :convert
25
- key :validate, :required, :format, :allowable
25
+ key :validate, :required, :format
26
+ key :enum, alias_names: [:allowable]
27
+ key :dynamic_ref, alias_names: [:dynamic_using], normalizer: ->(value) { value.is_a?(Proc) ? { resolve: value } : value }
26
28
  key :before, :after
27
29
  key :if
28
30
  end
@@ -89,7 +91,7 @@ module Meta
89
91
  end
90
92
 
91
93
  # 返回能够处理 scope 和 stage 的 schema(可以是 self),否则应返回 UnsupportedStageSchema 或 nil.
92
- def find_schema(scope:, stage:)
94
+ def find_schema(stage:, scope:)
93
95
  staged(stage)&.scoped(scope)
94
96
  end
95
97
 
@@ -103,19 +105,22 @@ module Meta
103
105
  self
104
106
  end
105
107
 
108
+ # defined_scopes_mapping 是一个 Hash,用于缓存已经计算出的 scopes,用于避免重复计算。其主要针对的是具有命名系统的 Schema,如 Meta::Entity
106
109
  def defined_scopes(stage:, defined_scopes_mapping:)
107
110
  []
108
111
  end
109
112
 
110
113
  # 执行 if: 选项,返回 true 或 false
111
- def if?(user_options)
112
- return true if options[:if].nil?
114
+ def if?(object_value, execution = nil)
115
+ if_block = options[:if]
116
+ return true if if_block.nil?
113
117
 
114
- execution = user_options[:execution]
118
+ args_length = if_block.lambda? ? if_block.arity : 1
119
+ args = args_length > 0 ? [object_value] : []
115
120
  if execution
116
- execution.instance_exec(&options[:if])
121
+ execution.instance_exec(*args, &options[:if])
117
122
  else
118
- options[:if]&.call
123
+ options[:if]&.call(*args)
119
124
  end
120
125
  end
121
126
 
@@ -136,7 +141,7 @@ module Meta
136
141
  schema = {}
137
142
  schema[:type] = options[:type] if options[:type]
138
143
  schema[:description] = options[:description] if options[:description]
139
- schema[:enum] = options[:allowable] if options[:allowable]
144
+ schema[:enum] = options[:enum] if options[:enum]
140
145
 
141
146
  schema
142
147
  end
@@ -11,25 +11,37 @@ module Meta
11
11
  # scope:、discard_missing:、exclude: 等
12
12
  attr_reader :locked_options
13
13
 
14
+ normalize_scope = ->(value) {
15
+ raise ArgumentError, 'scope 选项不可传递 nil' if value.nil?
16
+ value = [value] unless value.is_a?(Array)
17
+ value.map do |v|
18
+ # 只要加入了 Meta::Scope::Base 模块,就有与 Meta::Scope 一样的行为
19
+ next v if v.is_a?(Meta::Scope::Base)
20
+
21
+ # 将 v 类名化
22
+ scope_name = v.to_s.split('_').map(&:capitalize).join
23
+ # 如果符号对应的类名不存在,就报错
24
+ if !defined?(::Scopes) || !::Scopes.const_defined?(scope_name)
25
+ raise NameError, "未找到常量 Scopes::#{scope_name}。如果你用的是命名 Scope(字符串或符号),则检查一下是不是拼写错误"
26
+ end
27
+ # 返回对应的常量
28
+ ::Scopes.const_get(scope_name)
29
+ end.compact
30
+ }
14
31
  # stage 和 scope 选项在两个 CHECKER 下都用到了
15
32
  USER_OPTIONS_CHECKER = Utils::KeywordArgs::Builder.build do
16
33
  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
- }
34
+ key :scope, normalizer: normalize_scope
22
35
  key :discard_missing, :exclude, :extra_properties, :type_conversion, :validation
23
36
  key :execution, :user_data, :object_value
24
37
  end
25
38
  TO_SCHEMA_DOC_CHECKER = Utils::KeywordArgs::Builder.build do
26
39
  key :stage
27
- key :scope, normalizer: ->(value) {
28
- raise ArgumentError, 'scope 选项不可传递 nil' if value.nil?
29
- value = [value] unless value.is_a?(Array)
30
- value
31
- }
40
+ key :scope, normalizer: normalize_scope
32
41
  key :schema_docs_mapping, :defined_scopes_mapping
42
+
43
+ # 以下是 filter 用到的选项,讲道理这些选项应该是不需要的,放置这些是为了防止报错
44
+ key :discard_missing
33
45
  end
34
46
 
35
47
  def initialize(properties:, options: {}, locked_options: {})
@@ -42,17 +54,6 @@ module Meta
42
54
  @locked_options = USER_OPTIONS_CHECKER.check(locked_options || {})
43
55
  end
44
56
 
45
- # 复制一个新的 ObjectSchema,只有 options 不同
46
- def dup(options)
47
- raise UnsupportedError, 'dup 不应该再执行了'
48
-
49
- self.class.new(
50
- properties: properties,
51
- options: options,
52
- locked_options: locked_options,
53
- )
54
- end
55
-
56
57
  def filter(object_value, user_options = {})
57
58
  # 合并 user_options
58
59
  user_options = USER_OPTIONS_CHECKER.check(user_options)
@@ -60,35 +61,59 @@ module Meta
60
61
  super
61
62
  end
62
63
 
64
+ def to_schema_doc(user_options = {})
65
+ user_options = TO_SCHEMA_DOC_CHECKER.check(user_options)
66
+ user_options = self.class.merge_user_options(user_options, locked_options) if locked_options
67
+
68
+ schema = { type: 'object' }
69
+ schema[:description] = options[:description] if options[:description]
70
+ properties, required_keys = @properties.to_swagger_doc(**user_options)
71
+ schema[:properties] = properties unless properties.empty?
72
+ schema[:required] = required_keys unless required_keys.empty?
73
+ schema
74
+ end
75
+
63
76
  def naming?
64
77
  properties.is_a?(NamedProperties)
65
78
  end
66
79
 
80
+ def defined_scopes(stage:, defined_scopes_mapping:)
81
+ properties.defined_scopes(stage: stage, defined_scopes_mapping: defined_scopes_mapping)
82
+ end
83
+
84
+ # 解析当前 Schema 的名称
85
+ #
86
+ # 名称解析的规则是:
87
+ # 1. 结合基础的 schema_name,如果它是参数,就加上 Params 后缀;如果它是返回值,就加上 Entity 后缀
88
+ # 2. 而后跟上 locked_scope. 这里,会把多余的 scope 去掉。比如,一个实体内部只处理 Admin 的 scope,但是外部传进来了
89
+ # Admin 和 Detail,那么名称只会包括 Admin
67
90
  def resolve_name(stage, user_scopes, defined_scopes)
68
91
  raise ArgumentError, 'stage 不能为 nil' if stage.nil?
69
92
 
70
93
  # 先合成外面传进来的 scope
71
94
  locked_scopes = (locked_options || {})[:scope] || []
72
95
  user_scopes = (user_scopes + locked_scopes).uniq
73
- scopes = user_scopes & defined_scopes
74
96
 
75
97
  # 再根据 stage 和 scope 生成为当前的 Schema 生成一个合适的名称,要求保证唯一性
76
- schema_name = properties.schema_name(stage)
77
- schema_name += "__#{scopes.join('__')}" unless scopes.empty?
78
- schema_name
79
- end
80
-
81
- def to_schema_doc(user_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
98
+ base_schema_name = properties.schema_name(stage)
84
99
 
85
- schema = { type: 'object' }
86
- schema[:description] = options[:description] if options[:description]
100
+ # 将调用转移到 Scopes 模块下
101
+ resolve_name_helper(base_schema_name, user_scopes, defined_scopes)
102
+ end
87
103
 
88
- properties, required_keys = @properties.to_swagger_doc(**user_options)
89
- schema[:properties] = properties unless properties.empty?
90
- schema[:required] = required_keys unless required_keys.empty?
91
- schema
104
+ # 帮助实体解析名称,这里主要是考虑 scope 的作用
105
+ #
106
+ # base_schema_name: 具有 Params Entity 后缀的基础名称
107
+ # user_scopes: 用户传进来的 scope 数组
108
+ # candidate_scopes: 从实体中找出的能够参与命名的备选的 scope 数组
109
+ def resolve_name_helper(base_schema_name, user_scopes, candidate_scopes)
110
+ # 从备选的 scope 中获取到被利用到的
111
+ scopes = candidate_scopes.filter { |candidate_scope| candidate_scope.match?(user_scopes) }
112
+ scope_names = scopes.map(&:scope_name)
113
+
114
+ # 合成新的名字
115
+ schema_name_parts = [base_schema_name] + scope_names
116
+ schema_name_parts.join('__')
92
117
  end
93
118
 
94
119
  def locked_scope
@@ -30,7 +30,7 @@ module Meta
30
30
  next false if exclude && exclude.include?(name)
31
31
 
32
32
  # 通过 if 选项过滤
33
- next false unless property_schema.if?(user_options)
33
+ next false unless property_schema.if?(object_value, user_options[:execution])
34
34
 
35
35
  # 默认返回 true
36
36
  next true
@@ -71,8 +71,7 @@ module Meta
71
71
 
72
72
  # user_options 包括 stage, scope, schema_docs_mapping, defined_scopes_mapping
73
73
  def to_swagger_doc(scope: [], stage: nil, **user_options)
74
- locked_scopes = scope
75
- properties = filter_by(stage: stage, user_scope: locked_scopes)
74
+ properties = filter_by(stage: stage, user_scope: scope)
76
75
  required_keys = properties.filter do |key, property_schema|
77
76
  property_schema.options[:required]
78
77
  end.keys
@@ -82,6 +81,12 @@ module Meta
82
81
  [properties, required_keys]
83
82
  end
84
83
 
84
+ def defined_scopes(stage:, defined_scopes_mapping:)
85
+ @properties.each_with_object([]) do |(name, property), defined_scopes|
86
+ defined_scopes.concat(property.defined_scopes(stage: stage, defined_scopes_mapping: defined_scopes_mapping))
87
+ end
88
+ end
89
+
85
90
  # 程序中有些地方用到了这三个方法
86
91
  def_delegators :@properties, :empty?, :key?, :[], :each
87
92
 
@@ -95,13 +100,15 @@ module Meta
95
100
 
96
101
  private
97
102
 
98
- def filter_by(stage:, user_scope: false)
99
- @properties.transform_values do |property|
100
- property.find_schema(stage: stage, scope: user_scope)
101
- end.filter do |name, schema|
102
- schema.filter?
103
+ # 根据所示的关键字参数,过滤出符合条件的属性
104
+ # 注意,这里会结合自身的 locked_scope 考量
105
+ def filter_by(stage:, user_scope: false)
106
+ @properties.transform_values do |property|
107
+ property.find_schema(stage: stage, scope: user_scope)
108
+ end.filter do |name, schema|
109
+ schema.filter?
110
+ end
103
111
  end
104
- end
105
112
 
106
113
  def resolve_property_value(object_value, name, property_schema)
107
114
  if property_schema.value?
@@ -40,13 +40,17 @@ module Meta
40
40
  def defined_scopes(stage:, defined_scopes_mapping:)
41
41
  defined_scopes_mapping ||= {}
42
42
 
43
- schema_name = object_schema.properties.schema_name(stage)
44
- return defined_scopes_mapping[schema_name] if defined_scopes_mapping.key?(schema_name)
43
+ if object_schema.properties.respond_to?(:schema_name)
44
+ # 只有命名实体才会被缓存
45
+ schema_name = object_schema.properties.schema_name(stage)
46
+ return defined_scopes_mapping[schema_name] if defined_scopes_mapping.key?(schema_name)
47
+ end
45
48
 
46
49
  defined_scopes_mapping[schema_name] = []
50
+ # 求解 defined_scopes,最终结果去重 + 排序
47
51
  defined_scopes = object_schema.properties.each.map do |name, property|
48
52
  property.defined_scopes(stage: stage, defined_scopes_mapping: defined_scopes_mapping)
49
- end.flatten.uniq.sort
53
+ end.flatten.uniq.sort_by(&:name)
50
54
  defined_scopes_mapping[schema_name] = defined_scopes
51
55
  defined_scopes
52
56
  end
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../support/scope_matcher'
4
+ require_relative '../../scope/utils'
4
5
 
5
6
  module Meta
6
7
  module JsonSchema
8
+ # TODO: 作为模块引入
7
9
  class ScopingSchema < BaseSchema
8
10
  attr_reader :scope_matcher, :schema
9
11
 
10
- def initialize(scope_matcher_options: , schema:)
11
- @scope_matcher = ScopeMatcher.new(scope_matcher_options)
12
+ def initialize(scope_matcher:, schema:)
13
+ # raise ArgumentError, 'scope_matcher 不能是一个数组' if scope_matcher.is_a?(Array)
14
+
15
+ @scope_matcher = Scope::Utils.parse(scope_matcher)
12
16
  @schema = schema
13
17
  end
14
18
 
@@ -17,7 +21,9 @@ module Meta
17
21
  end
18
22
 
19
23
  def defined_scopes(**kwargs)
20
- scope_matcher.defined_scopes
24
+ current = scope_matcher.defined_scopes
25
+ deep = schema.defined_scopes(**kwargs)
26
+ (current + deep).uniq
21
27
  end
22
28
 
23
29
  private
@@ -30,7 +36,7 @@ module Meta
30
36
  options = options.dup
31
37
  scope_matcher_options = options.delete(:scope)
32
38
  schema = build_schema.call(options)
33
- schema = ScopingSchema.new(scope_matcher_options: scope_matcher_options, schema: schema) if scope_matcher_options
39
+ schema = ScopingSchema.new(scope_matcher: scope_matcher_options, schema: schema) if scope_matcher_options
34
40
  schema
35
41
  end
36
42
  end
@@ -7,19 +7,22 @@ module Meta
7
7
 
8
8
  def initialize(query_clause)
9
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)
10
+ query_clause = { all_of: query_clause } if query_clause.is_a?(Array)
11
11
 
12
12
  @match_type, @defined_scopes = query_clause.first
13
13
  end
14
14
 
15
- def match?(scopes)
16
- return false if scopes.empty?
15
+ def match?(providing_scopes)
16
+ # 目前认为空数组就是不做 scope 筛选
17
+ return false if providing_scopes.empty?
17
18
 
18
19
  case @match_type
19
20
  when :some_of
20
- (@defined_scopes & scopes).any?
21
+ # 只要相交就可以
22
+ (@defined_scopes & providing_scopes).any?
21
23
  when :all_of
22
- (@defined_scopes - scopes).empty?
24
+ # @defined_scopes 一定要被包含在 providing_scopes 内
25
+ (@defined_scopes - providing_scopes).empty?
23
26
  else
24
27
  raise "Unknown match type: #{@match_type}"
25
28
  end
@@ -26,9 +26,9 @@ module Meta
26
26
  next if value.nil?
27
27
  raise JsonSchema::ValidationError, I18n.t(:'json_schema.errors.format') unless value =~ format
28
28
  },
29
- allowable: proc { |value, allowable_values|
29
+ enum: proc { |value, allowable_values|
30
30
  next if value.nil?
31
- raise JsonSchema::ValidationError, I18n.t(:'json_schema.errors.allowable', allowable_values: allowable_values) unless allowable_values.include?(value)
31
+ raise JsonSchema::ValidationError, I18n.t(:'json_schema.errors.enum', allowable_values: allowable_values) unless allowable_values.include?(value)
32
32
  }
33
33
  }
34
34
 
@@ -43,7 +43,13 @@ module Meta
43
43
  codes.each { |code| @meta[:responses][code] = entity_schema }
44
44
  end
45
45
 
46
- [:tags, :title, :description, :scope].each do |method_name|
46
+ def scope(scope)
47
+ scope = [scope] unless scope.is_a?(Array)
48
+
49
+ @meta[:scope] = scope
50
+ end
51
+
52
+ [:tags, :title, :description].each do |method_name|
47
53
  define_method(method_name) do |value|
48
54
  @meta[method_name] = value
49
55
  end
@@ -24,6 +24,7 @@ module Meta
24
24
  end
25
25
 
26
26
  in_op = options.delete(:in)
27
+ raise ArgumentError, "in 选项只能是 path, query, header, body" unless %w[path query header body].include?(in_op)
27
28
  @parameter_options[name] = { in: in_op, schema: JsonSchema::BaseSchema.new(options) }
28
29
  end
29
30
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
- require_relative '../entity'
4
+ require_relative '../entity' # TODO: 放在这里不太合适
5
5
  require_relative '../application/route'
6
6
  require_relative 'chain_builder'
7
7
  require_relative 'action_builder'
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Meta
4
+ class Scope
5
+ module Errors
6
+ class NameNotSet < StandardError; end
7
+ end
8
+
9
+ # 基本的 Scope 方法,引入该模块即可获取系列方法
10
+ module Base
11
+ def match?(scopes)
12
+ scopes = [scopes] unless scopes.is_a?(Array)
13
+ match_scopes?(scopes)
14
+ end
15
+
16
+ def match_scopes?(scopes)
17
+ return true if @forwarded_scope&.match?(scopes)
18
+ scopes.include?(self)
19
+ end
20
+
21
+ def defined_scopes
22
+ [self]
23
+ end
24
+
25
+ def scope_name
26
+ scope_name = @scope_name || self.name
27
+ raise Errors::NameNotSet, '未设置 scope 名称' if scope_name.nil?
28
+
29
+ scope_name.split('::').last
30
+ end
31
+
32
+ def scope_name=(name)
33
+ @scope_name = name.to_s
34
+ end
35
+
36
+ def include_scope(*scopes)
37
+ @forwarded_scope = Composite.concat(@forwarded_scope, *scopes)
38
+ end
39
+
40
+ # 既作为类方法,也作为实例方法
41
+ def and(*scopes)
42
+ scopes = [self, *scopes] if self != Meta::Scope
43
+ And.new(*scopes)
44
+ end
45
+ alias_method :&, :and
46
+
47
+ # 既可以是类方法,也可以是实例方法
48
+ def or(*scopes)
49
+ scopes = [self, *scopes] if self != Meta::Scope
50
+ Or.new(*scopes)
51
+ end
52
+ alias_method :|, :or
53
+
54
+ def inspect
55
+ scope_name || super
56
+ end
57
+ end
58
+
59
+ # 将 Scope 类的子类作为 Scope 实例
60
+ class << self
61
+ include Base
62
+
63
+ def new(*args)
64
+ raise NoMethodError, 'Meta::Scope 类不能实例化'
65
+ end
66
+
67
+ def inherited(subclass)
68
+ # subclass.instance_variable_set(:@forwarded_scope, Or.new)
69
+
70
+ # 如果是 Meta::Scope 的具体子类被继承,该子类加入到 @included_scopes 原语中
71
+ subclass.include_scope(self) if self != Meta::Scope
72
+ end
73
+
74
+ def include(*mods)
75
+ scopes = mods.filter { |m| m < Meta::Scope }
76
+ mods = mods - scopes
77
+
78
+ include_scope(*scopes) if scopes.any?
79
+ super(*mods) if mods.any?
80
+ end
81
+ end
82
+
83
+ # 组合式 Scope 实例,用于表示多个 Scope 的逻辑操作
84
+ class Composite
85
+ include Base
86
+
87
+ def self.new(*scopes)
88
+ if scopes.length == 0
89
+ @empty || (@empty = self.new)
90
+ elsif scopes.length == 1
91
+ scopes[0]
92
+ else
93
+ super(*scopes)
94
+ end
95
+ end
96
+
97
+ def self.concat(*scopes)
98
+ composite_classes = scopes.filter { |scope| scope.is_a?(Composite) }
99
+ .map(&:class).uniq
100
+ raise ArgumentError, "不能执行 concat,参数中包含了多个逻辑运算符:#{composite_classes.join(',')}" if composite_classes.length > 1
101
+
102
+ if composite_classes.empty?
103
+ Or.new(*scopes)
104
+ else
105
+ composite_classes[0].new(*scopes)
106
+ end
107
+ end
108
+
109
+ def initialize(*scopes)
110
+ @scopes = scopes.compact.map do |scope|
111
+ scope.is_a?(self.class) ? scope.defined_scopes : scope
112
+ end.flatten.freeze
113
+ end
114
+
115
+ def defined_scopes
116
+ @scopes
117
+ end
118
+
119
+ def scope_name
120
+ @scope_name || @scopes.map(&:scope_name).sort.join('_')
121
+ end
122
+ end
123
+
124
+ # 逻辑 And 操作
125
+ class And < Composite
126
+ # scopes 需要包含所有的 @scopes
127
+ def match_scopes?(scopes)
128
+ @scopes.all? { |scope| scope.match?(scopes) }
129
+ end
130
+
131
+ # 重定义 scope_name,如果用得上的话
132
+ def scope_name
133
+ @scope_name || @scopes.map(&:scope_name).sort.join('_and_')
134
+ end
135
+ end
136
+
137
+ # 另一种 Scope 实例,用于表示多个 Scope 的逻辑 Or 操作
138
+ class Or < Composite
139
+ include Base
140
+
141
+ # scopes 只需要包含一个 @scopes
142
+ def match_scopes?(scopes)
143
+ @scopes.any? { |scope| scope.match?(scopes) }
144
+ end
145
+
146
+ # 重定义 scope_name,如果用得上的话
147
+ def scope_name
148
+ @scope_name || @scopes.map(&:scope_name).sort.join('_or_')
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ # 兼容以前的字符串 scope
6
+ module Meta
7
+ class Scope
8
+ module Utils
9
+ class << self
10
+ # 根据选项构建 Scope 实例,这是兼容之前的字符串写法。
11
+ def parse(scope)
12
+ scope = [scope] if scope.is_a?(String) || scope.is_a?(Symbol)
13
+
14
+ # 只会有两种类型:Array[String] 和 Scope 子类
15
+ if scope.is_a?(Meta::Scope::Base)
16
+ scope
17
+ elsif scope.is_a?(Array)
18
+ scopes = scope.map { |s| parse_string(s) }
19
+ Or.new(*scopes)
20
+ else
21
+ raise ArgumentError, 'scope 参数必须是一个数组或者 Scope 子类'
22
+ end
23
+ end
24
+
25
+ def and(*scopes)
26
+ scopes = scopes.map { |s| parse(s) }
27
+ And.new(*scopes)
28
+ end
29
+
30
+ private
31
+
32
+ def parse_string(str)
33
+ # 确保全局存在一个 Scopes 模块
34
+ unless defined?(::Scopes)
35
+ scopes = Module.new
36
+ Object.const_set(:Scopes, scopes)
37
+ end
38
+
39
+ # 获取类名化的 scope 名称
40
+ scope_name = str.to_s.split('_').map(&:capitalize).join
41
+
42
+ # 如果 Scopes 模块中已经存在该 scope 类,直接返回
43
+ return ::Scopes.const_get(scope_name) if ::Scopes.const_defined?(scope_name)
44
+
45
+ # 如果不存在,创建一个新的类
46
+ scope_class = Class.new(Meta::Scope)
47
+ scope_class.scope_name = str
48
+ ::Scopes.const_set(scope_name, scope_class)
49
+
50
+ # 返回结果
51
+ scope_class
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -45,7 +45,7 @@ module Meta
45
45
  final_args.merge!(args)
46
46
  else
47
47
  extras = args.keys
48
- raise "不接受额外的关键字参数:#{extras.join(', ')}" unless extras.empty?
48
+ raise ArgumentError, "不接受额外的关键字参数:#{extras.join(', ')}" unless extras.empty?
49
49
  end
50
50
  end
51
51
 
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.1.0"
3
+ spec.version = "0.1.2"
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.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - yetrun
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-05 00:00:00.000000000 Z
11
+ date: 2024-06-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hash_to_struct
@@ -81,6 +81,8 @@ files:
81
81
  - lib//meta/route_dsl/parameters_builder.rb
82
82
  - lib//meta/route_dsl/route_builder.rb
83
83
  - lib//meta/route_dsl/uniformed_params_builder.rb
84
+ - lib//meta/scope/base.rb
85
+ - lib//meta/scope/utils.rb
84
86
  - lib//meta/swagger_doc.rb
85
87
  - lib//meta/utils/kwargs/builder.rb
86
88
  - lib//meta/utils/kwargs/check.rb