meta-api 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.autoenv.zsh +1 -0
- data/.gitignore +6 -0
- data/.rubocop.yml +28 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +66 -0
- data/LICENSE.txt +502 -0
- data/README.md +149 -0
- data/Rakefile +3 -0
- data/config/locales/zh-CN.yml +6 -0
- data/docs//345/220/215/347/247/260/347/224/261/346/235/245.md +7 -0
- data/docs//346/225/231/347/250/213.md +1199 -0
- data/docs//347/264/242/345/274/225.md +173 -0
- data/examples/lobster.rb +71 -0
- data/examples/rack_app/README.md +3 -0
- data/examples/rack_app/config.ru +6 -0
- data/examples/rack_app/hello.rb +6 -0
- data/examples/rack_app/timing.rb +15 -0
- data/lib/meta/api.rb +3 -0
- data/lib/meta/application/application.rb +63 -0
- data/lib/meta/application/execution.rb +178 -0
- data/lib/meta/application/meta.rb +71 -0
- data/lib/meta/application/path_matching_mod.rb +53 -0
- data/lib/meta/application/route.rb +58 -0
- data/lib/meta/application.rb +42 -0
- data/lib/meta/entity.rb +59 -0
- data/lib/meta/errors.rb +29 -0
- data/lib/meta/json_schema/builders/array_schema_builder.rb +29 -0
- data/lib/meta/json_schema/builders/object_schema_builder.rb +120 -0
- data/lib/meta/json_schema/builders/schema_builder_tool.rb +29 -0
- data/lib/meta/json_schema/schemas/array_schema.rb +40 -0
- data/lib/meta/json_schema/schemas/base_schema.rb +110 -0
- data/lib/meta/json_schema/schemas/object_schema.rb +161 -0
- data/lib/meta/json_schema/schemas.rb +12 -0
- data/lib/meta/json_schema/support/errors.rb +38 -0
- data/lib/meta/json_schema/support/presenters.rb +35 -0
- data/lib/meta/json_schema/support/schema_options.rb +55 -0
- data/lib/meta/json_schema/support/type_converter.rb +137 -0
- data/lib/meta/json_schema/support/validators.rb +54 -0
- data/lib/meta/load_i18n.rb +8 -0
- data/lib/meta/route_dsl/action_builder.rb +15 -0
- data/lib/meta/route_dsl/application_builder.rb +108 -0
- data/lib/meta/route_dsl/chain_builder.rb +48 -0
- data/lib/meta/route_dsl/helpers.rb +15 -0
- data/lib/meta/route_dsl/meta_builder.rb +57 -0
- data/lib/meta/route_dsl/parameters_builder.rb +24 -0
- data/lib/meta/route_dsl/route_builder.rb +85 -0
- data/lib/meta/route_dsl/uniformed_params_builder.rb +34 -0
- data/lib/meta/swagger_doc.rb +86 -0
- data/lib/meta/utils/path.rb +20 -0
- data/meta-api.gemspec +23 -0
- metadata +96 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'execution'
|
4
|
+
require_relative 'path_matching_mod'
|
5
|
+
require_relative 'meta'
|
6
|
+
|
7
|
+
module Meta
|
8
|
+
class Route
|
9
|
+
include PathMatchingMod.new(path_method: :path, matching_mode: :full)
|
10
|
+
|
11
|
+
attr_reader :path, :method, :meta, :actions
|
12
|
+
|
13
|
+
def initialize(path: '', method: :all, meta: {}, actions: [])
|
14
|
+
@path = Utils::Path.normalize_path(path)
|
15
|
+
@method = method
|
16
|
+
@meta = Meta.new(meta)
|
17
|
+
@actions = actions
|
18
|
+
end
|
19
|
+
|
20
|
+
def execute(execution, remaining_path)
|
21
|
+
path_matching.merge_path_params(remaining_path, execution.request)
|
22
|
+
|
23
|
+
# 依次执行这个环境
|
24
|
+
begin
|
25
|
+
execution.parse_parameters(@meta[:parameters]) if @meta[:parameters]
|
26
|
+
execution.parse_request_body(@meta[:request_body]) if @meta[:request_body]
|
27
|
+
|
28
|
+
actions.each { |b| execution.instance_eval(&b) }
|
29
|
+
|
30
|
+
render_entity(execution) if @meta[:responses]
|
31
|
+
rescue Execution::Abort
|
32
|
+
execution
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def match?(execution, remaining_path)
|
37
|
+
request = execution.request
|
38
|
+
remaining_path = '' if remaining_path == '/'
|
39
|
+
method = request.request_method
|
40
|
+
|
41
|
+
return false unless path_matching.match?(remaining_path)
|
42
|
+
return false unless @method == :all || @method.to_s.upcase == method
|
43
|
+
return true
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def render_entity(execution)
|
49
|
+
responses = @meta[:responses]
|
50
|
+
status = execution.response.status
|
51
|
+
codes = responses.keys
|
52
|
+
return unless codes.include?(status)
|
53
|
+
|
54
|
+
entity_schema = responses[status]
|
55
|
+
execution.render_entity(entity_schema) if entity_schema
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'route_dsl/route_builder'
|
4
|
+
require_relative 'route_dsl/application_builder'
|
5
|
+
require_relative 'application/application'
|
6
|
+
|
7
|
+
# 结构组织如下:
|
8
|
+
# - lib/application.rb: 模块实例
|
9
|
+
# - route_dsl/application_builder.rb: DSL 语法的 Builder
|
10
|
+
# - application.rb(本类): 综合以上两个类q的方法到一个类当中
|
11
|
+
module Meta
|
12
|
+
class Application
|
13
|
+
class << self
|
14
|
+
extend Forwardable
|
15
|
+
|
16
|
+
include Execution::MakeToRackMiddleware
|
17
|
+
|
18
|
+
attr_reader :builder
|
19
|
+
|
20
|
+
def inherited(mod)
|
21
|
+
super
|
22
|
+
|
23
|
+
mod.instance_variable_set(:@builder, RouteDSL::ApplicationBuilder.new)
|
24
|
+
end
|
25
|
+
|
26
|
+
# 读取应用的元信息
|
27
|
+
def_delegators :app, :prefix, :routes, :applications, :execute, :to_swagger_doc
|
28
|
+
|
29
|
+
# DSL 调用委托给内部 Builder
|
30
|
+
builder_methods = (RouteDSL::ApplicationBuilder.public_instance_methods(false) - ['build'])
|
31
|
+
def_delegators :builder, *builder_methods
|
32
|
+
|
33
|
+
def app
|
34
|
+
@app || @app = builder.build
|
35
|
+
end
|
36
|
+
|
37
|
+
def build(**args)
|
38
|
+
@app = builder.build(**args)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/meta/entity.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require_relative 'errors'
|
5
|
+
require_relative 'json_schema/schemas'
|
6
|
+
|
7
|
+
# Meta::Entity 是 ObjectSchemaBuilder 的一个类封装,它不应有自己的逻辑
|
8
|
+
module Meta
|
9
|
+
class Entity
|
10
|
+
class << self
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
attr_reader :schema_builder
|
14
|
+
|
15
|
+
def inherited(base)
|
16
|
+
base.instance_eval do
|
17
|
+
@schema_builder = JsonSchema::ObjectSchemaBuilder.new
|
18
|
+
@schema_builder.schema_name(proc { |locked_scope, stage|
|
19
|
+
generate_schema_name(locked_scope, stage)
|
20
|
+
})
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def_delegators :schema_builder, :property, :param, :expose, :use, :lock, :locked, :schema_name, :to_schema
|
25
|
+
|
26
|
+
def method_missing(method, *args)
|
27
|
+
if method =~ /^lock_(\w+)$/
|
28
|
+
schema_builder.send(method, *args)
|
29
|
+
else
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def generate_schema_name(stage, locked_scopes)
|
37
|
+
# 匿名类不考虑自动生成名称
|
38
|
+
return nil unless self.name
|
39
|
+
|
40
|
+
schema_name = self.name.gsub('::', '_')
|
41
|
+
schema_name = schema_name.delete_suffix('Entity') unless schema_name == 'Entity'
|
42
|
+
|
43
|
+
# 先考虑 stage
|
44
|
+
case stage
|
45
|
+
when :param
|
46
|
+
schema_name += 'Params'
|
47
|
+
when :render
|
48
|
+
schema_name += 'Entity'
|
49
|
+
end
|
50
|
+
|
51
|
+
# 再考虑 locked_scope
|
52
|
+
scope_suffix = locked_scopes.join('_')
|
53
|
+
schema_name = "#{schema_name}_#{scope_suffix}" unless scope_suffix.empty?
|
54
|
+
|
55
|
+
schema_name
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/meta/errors.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'json_schema/schemas'
|
4
|
+
|
5
|
+
module Meta
|
6
|
+
module Errors
|
7
|
+
class NoMatchingRoute < StandardError; end
|
8
|
+
|
9
|
+
class ParameterInvalid < JsonSchema::ValidationErrors
|
10
|
+
def initialize(errors)
|
11
|
+
super(errors, "参数异常:#{errors}")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class RenderingInvalid < JsonSchema::ValidationErrors
|
16
|
+
def initialize(errors)
|
17
|
+
super(errors, "渲染实体异常:#{errors}")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class RenderingError < StandardError; end
|
22
|
+
|
23
|
+
class NotAuthorized < StandardError; end
|
24
|
+
|
25
|
+
class ResourceNotFound < StandardError; end
|
26
|
+
|
27
|
+
class UnsupportedContentType < StandardError; end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Meta
|
4
|
+
module JsonSchema
|
5
|
+
class ArraySchemaBuilder
|
6
|
+
def initialize(options, &block)
|
7
|
+
raise 'type 选项必须是 array' if !options[:type].nil? && options[:type] != 'array'
|
8
|
+
|
9
|
+
options = options.merge(type: 'array')
|
10
|
+
@options = options
|
11
|
+
|
12
|
+
items_options = options.delete(:items) || {}
|
13
|
+
if object_property?(items_options, block)
|
14
|
+
@items = ObjectSchemaBuilder.new(items_options, &block).to_schema
|
15
|
+
else
|
16
|
+
@items = BaseSchema.new(items_options)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_schema
|
21
|
+
ArraySchema.new(@items, @options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def object_property?(options, block)
|
25
|
+
(options && !options[:properties].nil?) || !block.nil?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Meta
|
4
|
+
module JsonSchema
|
5
|
+
class ObjectSchemaBuilder
|
6
|
+
def initialize(options = {}, &)
|
7
|
+
raise 'type 选项必须是 object' if !options[:type].nil? && options[:type] != 'object'
|
8
|
+
|
9
|
+
@properties = {}
|
10
|
+
@required = []
|
11
|
+
@validations = {}
|
12
|
+
|
13
|
+
options = options.merge(type: 'object')
|
14
|
+
properties = options.delete(:properties)
|
15
|
+
@options = options
|
16
|
+
|
17
|
+
properties&.each do |name, property_options|
|
18
|
+
property name, property_options
|
19
|
+
end
|
20
|
+
|
21
|
+
instance_exec(&) if block_given?
|
22
|
+
end
|
23
|
+
|
24
|
+
# 设置 schema_name.
|
25
|
+
#
|
26
|
+
# 一、可以传递一个块,该块会接收 locked_scope 参数,需要返回一个带有 param 和 render 键的 Hash.
|
27
|
+
# 二、可以传递一个 Hash,它包含 param 和 render 键。
|
28
|
+
# 三、可以传递一个字符串。
|
29
|
+
def schema_name(schema_name_resolver)
|
30
|
+
if schema_name_resolver.is_a?(Proc)
|
31
|
+
@schema_name_resolver = schema_name_resolver
|
32
|
+
elsif schema_name_resolver.is_a?(Hash)
|
33
|
+
@schema_name_resolver = proc { |stage, locked_scopes| schema_name_resolver[stage] }
|
34
|
+
elsif schema_name_resolver.is_a?(String)
|
35
|
+
@schema_name_resolver = proc { |stage, locked_scopes| schema_name_resolver }
|
36
|
+
elsif schema_name_resolver.nil?
|
37
|
+
@schema_name_resolver = proc { nil }
|
38
|
+
else
|
39
|
+
raise TypeError, "schema_name_resolver 必须是一个 Proc、Hash 或 String,当前是:#{schema_name_resolver.class}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def property(name, options = {}, &block)
|
44
|
+
name = name.to_sym
|
45
|
+
options = options.dup
|
46
|
+
|
47
|
+
# 能且仅能 ObjectSchemaBuilder 内能使用 using 选项
|
48
|
+
block = options[:using] unless block_given?
|
49
|
+
if block.nil? || block.is_a?(Proc)
|
50
|
+
@properties[name] = SchemaBuilderTool.build(options, &block)
|
51
|
+
elsif block.respond_to?(:to_schema)
|
52
|
+
schema = block.to_schema
|
53
|
+
if options[:type] == 'array'
|
54
|
+
@properties[name] = ArraySchema.new(schema, options)
|
55
|
+
else
|
56
|
+
@properties[name] = schema.dup(options)
|
57
|
+
end
|
58
|
+
else
|
59
|
+
raise "非法的参数。应传递代码块,或通过 using 选项传递 Proc、ObjectScope 或接受 `to_schema` 方法的对象。当前传递:#{block}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
alias expose property
|
64
|
+
alias param property
|
65
|
+
|
66
|
+
# 能且仅能 ObjectSchemaBuilder 内能使用 use 方法
|
67
|
+
def use(proc)
|
68
|
+
proc = proc.to_proc if proc.respond_to?(:to_proc)
|
69
|
+
instance_exec(&proc)
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_schema(locked_options = nil)
|
73
|
+
ObjectSchema.new(properties: @properties, object_validations: @validations, options: @options, locked_options: locked_options, schema_name_resolver: @schema_name_resolver)
|
74
|
+
end
|
75
|
+
|
76
|
+
def lock(key, value)
|
77
|
+
locked(key => value)
|
78
|
+
end
|
79
|
+
|
80
|
+
def locked(options)
|
81
|
+
Locked.new(self, options)
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def apply_array_scope?(options, block)
|
87
|
+
options[:type] == 'array' && (options[:items] || block)
|
88
|
+
end
|
89
|
+
|
90
|
+
def apply_object_scope?(options, block)
|
91
|
+
(options[:type] == 'object' || block) && (options[:properties] || block)
|
92
|
+
end
|
93
|
+
|
94
|
+
def method_missing(method, *args)
|
95
|
+
if method =~ /^lock_(\w+)$/
|
96
|
+
key = Regexp.last_match(1)
|
97
|
+
lock(key.to_sym, *args)
|
98
|
+
else
|
99
|
+
super
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class Locked
|
104
|
+
attr_reader :builder, :locked_options
|
105
|
+
|
106
|
+
def initialize(builder, locked_options)
|
107
|
+
@builder = builder
|
108
|
+
@locked_options = locked_options
|
109
|
+
end
|
110
|
+
|
111
|
+
# 当调用 Entity.locked 方法后,生成 schema 的方法会掉到这里面来。
|
112
|
+
# 在生成 schema 时,locked_options 会覆盖;当生成 schema 文档时,由于缺失 schema_name 的
|
113
|
+
# 信息,故而 schema_name 相关的影响就消失不见了。
|
114
|
+
def to_schema
|
115
|
+
builder.to_schema(locked_options)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Meta
|
4
|
+
module JsonSchema
|
5
|
+
class SchemaBuilderTool
|
6
|
+
class << self
|
7
|
+
def build(options = {}, &block)
|
8
|
+
if apply_array_schema?(options, block)
|
9
|
+
ArraySchemaBuilder.new(options, &block).to_schema
|
10
|
+
elsif apply_object_schema?(options, block)
|
11
|
+
ObjectSchemaBuilder.new(options, &block).to_schema
|
12
|
+
else
|
13
|
+
BaseSchema.new(options)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def apply_array_schema?(options, block)
|
20
|
+
options[:type] == 'array' && (options[:items] || block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def apply_object_schema?(options, block)
|
24
|
+
(options[:type] == 'object' || options[:type].nil?) && (options[:properties] || block)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Meta
|
4
|
+
module JsonSchema
|
5
|
+
class ArraySchema < BaseSchema
|
6
|
+
attr_reader :items
|
7
|
+
|
8
|
+
def initialize(items, options = {})
|
9
|
+
super(options)
|
10
|
+
|
11
|
+
@items = items
|
12
|
+
end
|
13
|
+
|
14
|
+
def filter(array_value, options = {})
|
15
|
+
array_value = super(array_value, options)
|
16
|
+
return nil if array_value.nil?
|
17
|
+
raise ValidationError.new('参数应该传递一个数组') unless array_value.respond_to?(:each_with_index)
|
18
|
+
|
19
|
+
array_value.each_with_index.map do |item, index|
|
20
|
+
begin
|
21
|
+
@items.filter(item, **options)
|
22
|
+
rescue ValidationErrors => e
|
23
|
+
raise e.prepend_root("[#{index}]")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_schema_doc(user_options = {})
|
29
|
+
stage_options = user_options[:stage] == :param ? @param_options : @render_options
|
30
|
+
|
31
|
+
schema = {
|
32
|
+
type: 'array',
|
33
|
+
items: @items ? @items.to_schema_doc(user_options) : {}
|
34
|
+
}
|
35
|
+
schema[:description] = stage_options[:description] if stage_options[:description]
|
36
|
+
schema
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../support/schema_options'
|
4
|
+
|
5
|
+
module Meta
|
6
|
+
module JsonSchema
|
7
|
+
class BaseSchema
|
8
|
+
# `options` 包含了转换器、验证器、文档、选项。
|
9
|
+
#
|
10
|
+
# 由于本对象可继承,基于不同的继承可分别表示基本类型、对象和数组,所以该属
|
11
|
+
# 性可用在不同类型的对象上。需要时刻留意的是,无论是用在哪种类型的对象内,
|
12
|
+
# `options` 属性都是描述该对象的本身,而不是深层的属性。
|
13
|
+
#
|
14
|
+
# 较常出现错误的是数组,`options` 是描述数组的,而不是描述数组内部元素的。
|
15
|
+
attr_reader :param_options, :render_options
|
16
|
+
|
17
|
+
# 传递 path 参数主要是为了渲染 Parameter 文档时需要
|
18
|
+
def initialize(options = {})
|
19
|
+
@param_options, @render_options = SchemaOptions.normalize_to_param_and_render(options)
|
20
|
+
end
|
21
|
+
|
22
|
+
def options(stage, key = nil)
|
23
|
+
stage_options = case stage
|
24
|
+
when :param
|
25
|
+
param_options
|
26
|
+
when :render
|
27
|
+
render_options
|
28
|
+
when nil
|
29
|
+
merged_options
|
30
|
+
else
|
31
|
+
raise "非法的 stage 参数,它只允许取值 :param、:render 或 nil,却收到 #{stage.inspect}"
|
32
|
+
end
|
33
|
+
if key
|
34
|
+
stage_options ? stage_options[key] : nil
|
35
|
+
else
|
36
|
+
stage_options
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# 将 params 和 render 的选项合并
|
41
|
+
def merged_options
|
42
|
+
(param_options || {}).merge(render_options || {})
|
43
|
+
end
|
44
|
+
|
45
|
+
def filter(value, user_options = {})
|
46
|
+
stage_options = options(user_options[:stage])
|
47
|
+
|
48
|
+
value = resolve_value(user_options) if stage_options[:value]
|
49
|
+
value = JsonSchema::Presenters.present(stage_options[:presenter], value) if stage_options[:presenter]
|
50
|
+
value = stage_options[:default] if value.nil? && stage_options.key?(:default)
|
51
|
+
value = stage_options[:convert].call(value) if stage_options[:convert]
|
52
|
+
|
53
|
+
# 这一步转换值。需要注意的是,对象也可能被转换,因为并没有深层次的结构被声明。
|
54
|
+
type = stage_options[:type]
|
55
|
+
unless type.nil? || value.nil?
|
56
|
+
begin
|
57
|
+
value = JsonSchema::TypeConverter.convert_value(value, type)
|
58
|
+
rescue JsonSchema::TypeConvertError => e
|
59
|
+
raise JsonSchema::ValidationError.new(e.message)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
validate!(value, stage_options)
|
64
|
+
|
65
|
+
value
|
66
|
+
end
|
67
|
+
|
68
|
+
def value?(stage)
|
69
|
+
options(stage, :value) != nil
|
70
|
+
end
|
71
|
+
|
72
|
+
def resolve_value(user_options)
|
73
|
+
value_proc = options(user_options[:stage], :value)
|
74
|
+
value_proc_params = (value_proc.lambda? && value_proc.arity == 0) ? [] : [user_options[:object_value]]
|
75
|
+
|
76
|
+
if user_options[:execution]
|
77
|
+
user_options[:execution].instance_exec(*value_proc_params, &value_proc)
|
78
|
+
else
|
79
|
+
value_proc.call(*value_proc_params)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_schema_doc(user_options = {})
|
84
|
+
stage_options = options(user_options[:stage])
|
85
|
+
|
86
|
+
return Presenters.to_schema_doc(stage_options[:presenter], stage_options) if stage_options[:presenter]
|
87
|
+
|
88
|
+
schema = {}
|
89
|
+
schema[:type] = stage_options[:type] if stage_options[:type]
|
90
|
+
schema[:description] = stage_options[:description] if stage_options[:description]
|
91
|
+
schema[:enum] = stage_options[:allowable] if stage_options[:allowable]
|
92
|
+
|
93
|
+
schema
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_schema
|
97
|
+
self
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def validate!(value, stage_options)
|
103
|
+
stage_options.each do |key, option|
|
104
|
+
validator = JsonSchema::Validators[key]
|
105
|
+
validator&.call(value, option, stage_options)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Meta
|
4
|
+
module JsonSchema
|
5
|
+
class ObjectSchema < BaseSchema
|
6
|
+
attr_reader :properties, :object_validations, :locked_options
|
7
|
+
|
8
|
+
def initialize(properties: {}, object_validations: {}, options: {}, locked_options: {}, schema_name_resolver: proc { nil })
|
9
|
+
super(options)
|
10
|
+
|
11
|
+
@properties = properties || {}
|
12
|
+
@object_validations = object_validations || {}
|
13
|
+
@locked_options = locked_options || {}
|
14
|
+
@schema_name_resolver = schema_name_resolver || proc { nil }
|
15
|
+
end
|
16
|
+
|
17
|
+
# 复制一个新的 ObjectSchema,只有 options 不同
|
18
|
+
def dup(options)
|
19
|
+
self.class.new(
|
20
|
+
properties: properties,
|
21
|
+
object_validations: object_validations,
|
22
|
+
options: options,
|
23
|
+
locked_options: locked_options,
|
24
|
+
schema_name_resolver: @schema_name_resolver
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def filter(object_value, user_options = {})
|
29
|
+
# 合并 user_options
|
30
|
+
user_options = user_options.merge(locked_options) if locked_options
|
31
|
+
|
32
|
+
object_value = super(object_value, user_options)
|
33
|
+
return nil if object_value.nil?
|
34
|
+
|
35
|
+
# 第一步,根据 user_options[:scope] 需要过滤一些字段
|
36
|
+
# user_options[:scope] 应是一个数组
|
37
|
+
user_scope = user_options[:scope] || []
|
38
|
+
user_scope = [user_scope] unless user_scope.is_a?(Array)
|
39
|
+
stage = user_options[:stage]
|
40
|
+
exclude = user_options.delete(:exclude) # 这里删除 exclude 选项
|
41
|
+
filtered_properties = @properties.filter do |name, property_schema|
|
42
|
+
# 通过 discard_missing 过滤
|
43
|
+
next false if user_options[:discard_missing] && !object_value.key?(name.to_s)
|
44
|
+
|
45
|
+
# 通过 stage 过滤。
|
46
|
+
property_schema_options = property_schema.options(stage)
|
47
|
+
next false unless property_schema_options
|
48
|
+
|
49
|
+
# 通过 locked_exclude 选项过滤
|
50
|
+
next false if exclude && exclude.include?(name)
|
51
|
+
|
52
|
+
# 通过 scope 过滤
|
53
|
+
scope_option = property_schema_options[:scope]
|
54
|
+
next true if scope_option.empty?
|
55
|
+
next false if user_scope.empty?
|
56
|
+
(user_scope - scope_option).empty? # user_scope 应被消耗殆尽
|
57
|
+
end
|
58
|
+
|
59
|
+
# 第二步,递归过滤每一个属性
|
60
|
+
object = {}
|
61
|
+
errors = {}
|
62
|
+
filtered_properties.each do |name, property_schema|
|
63
|
+
value = resolve_property_value(object_value, name, property_schema, stage)
|
64
|
+
|
65
|
+
begin
|
66
|
+
object[name] = property_schema.filter(value, **user_options, object_value: object_value)
|
67
|
+
rescue JsonSchema::ValidationErrors => e
|
68
|
+
errors.merge! e.prepend_root(name).errors
|
69
|
+
end
|
70
|
+
end.to_h
|
71
|
+
|
72
|
+
if errors.empty?
|
73
|
+
object
|
74
|
+
else
|
75
|
+
raise JsonSchema::ValidationErrors.new(errors)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# 合并其他的属性,并返回一个新的 ObjectSchema
|
80
|
+
def merge(properties)
|
81
|
+
ObjectSchema.new(properties: self.properties.merge(properties))
|
82
|
+
end
|
83
|
+
|
84
|
+
# 生成 Swagger 文档的 schema 格式。
|
85
|
+
#
|
86
|
+
# 选项:
|
87
|
+
# - stage: 传递 :param 或 :render
|
88
|
+
# - schemas: 用于保存已经生成的 Schema
|
89
|
+
# - to_ref: 是否生成 $ref 格式,默认为“是”
|
90
|
+
#
|
91
|
+
# 提示:
|
92
|
+
# > 每个 ObjectSchema 拥有一个 @schema_name_resolver 实例变量,如果由它解析出来的名称不为 nil,
|
93
|
+
# > 则该 Schema 生成文档时会使用 $ref 格式。除非 to_ref 选项设置为 false.
|
94
|
+
#
|
95
|
+
def to_schema_doc(user_options)
|
96
|
+
stage = user_options[:stage]
|
97
|
+
# HACK: 标准化选项的工作进行得怎样?
|
98
|
+
locked_scopes = (locked_options || {})[:scope] || []
|
99
|
+
locked_scopes = [locked_scopes] unless locked_scope.nil? && locked_scopes.is_a?(Array)
|
100
|
+
schema_name = @schema_name_resolver.call(stage, locked_scopes)
|
101
|
+
if schema_name && user_options[:to_ref] != false
|
102
|
+
# 首先将 Schema 写进 schemas 选项中去
|
103
|
+
schemas = user_options[:schemas]
|
104
|
+
unless schemas.key?(schema_name)
|
105
|
+
schemas[schema_name] = nil # 首先设置 schemas 防止出现无限循环
|
106
|
+
schemas[schema_name] = to_schema_doc(**user_options, to_ref: false) # 原地修改 schemas,无妨
|
107
|
+
end
|
108
|
+
|
109
|
+
return { '$ref': "#/components/schemas/#{schema_name}" }
|
110
|
+
end
|
111
|
+
|
112
|
+
stage_options = options(stage)
|
113
|
+
properties = @properties.filter do |name, property_schema|
|
114
|
+
# 根据 stage 过滤
|
115
|
+
next false if stage.nil?
|
116
|
+
next false if stage == :param && !property_schema.options(:param)
|
117
|
+
next false if stage == :render && !property_schema.options(:render)
|
118
|
+
|
119
|
+
# 根据 locked_scope 过滤
|
120
|
+
next true if locked_scopes.empty? # locked_scope 未提供时不过滤
|
121
|
+
property_scope = property_schema.options(stage, :scope)
|
122
|
+
property_scope = [property_scope] unless property_scope.is_a?(Array)
|
123
|
+
next true if property_scope.empty?
|
124
|
+
(locked_scopes - property_scope).empty? # user_scope 应被消耗殆尽
|
125
|
+
end
|
126
|
+
required_keys = properties.filter do |key, property_schema|
|
127
|
+
property_schema.options(stage, :required)
|
128
|
+
end.keys
|
129
|
+
properties = properties.transform_values do |property_schema|
|
130
|
+
property_schema.to_schema_doc(**user_options, to_ref: true)
|
131
|
+
end
|
132
|
+
|
133
|
+
schema = { type: 'object' }
|
134
|
+
schema[:description] = stage_options[:description] if stage_options[:description]
|
135
|
+
schema[:properties] = properties unless properties.empty?
|
136
|
+
schema[:required] = required_keys unless required_keys.empty?
|
137
|
+
schema
|
138
|
+
end
|
139
|
+
|
140
|
+
def locked_scope
|
141
|
+
locked_options && locked_options[:scope]
|
142
|
+
end
|
143
|
+
|
144
|
+
def locked_exclude
|
145
|
+
locked_options && locked_options[:exclude]
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def resolve_property_value(object_value, name, property_schema, stage)
|
151
|
+
if property_schema.value?(stage)
|
152
|
+
nil
|
153
|
+
elsif object_value.is_a?(Hash) || object_value.is_a?(ObjectWrapper)
|
154
|
+
object_value.key?(name.to_s) ? object_value[name.to_s] : object_value[name.to_sym]
|
155
|
+
else
|
156
|
+
raise "不应该还有其他类型了,已经在类型转换中将其转换为 Meta::JsonSchema::ObjectWrapper 了"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'support/errors'
|
4
|
+
require_relative 'support/type_converter'
|
5
|
+
require_relative 'support/validators'
|
6
|
+
require_relative 'support/presenters'
|
7
|
+
require_relative 'schemas/base_schema'
|
8
|
+
require_relative 'schemas/object_schema'
|
9
|
+
require_relative 'schemas/array_schema'
|
10
|
+
require_relative 'builders/object_schema_builder'
|
11
|
+
require_relative 'builders/array_schema_builder'
|
12
|
+
require_relative 'builders/schema_builder_tool'
|