json_schemer 1.0.3 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +1 -6
- data/CHANGELOG.md +25 -0
- data/Gemfile.lock +1 -1
- data/README.md +137 -14
- data/json_schemer.gemspec +1 -1
- data/lib/json_schemer/draft201909/meta.rb +335 -0
- data/lib/json_schemer/draft201909/vocab/applicator.rb +104 -0
- data/lib/json_schemer/draft201909/vocab/core.rb +45 -0
- data/lib/json_schemer/draft201909/vocab.rb +31 -0
- data/lib/json_schemer/draft202012/meta.rb +361 -0
- data/lib/json_schemer/draft202012/vocab/applicator.rb +382 -0
- data/lib/json_schemer/draft202012/vocab/content.rb +44 -0
- data/lib/json_schemer/draft202012/vocab/core.rb +154 -0
- data/lib/json_schemer/draft202012/vocab/format_annotation.rb +31 -0
- data/lib/json_schemer/draft202012/vocab/format_assertion.rb +29 -0
- data/lib/json_schemer/draft202012/vocab/meta_data.rb +30 -0
- data/lib/json_schemer/draft202012/vocab/unevaluated.rb +94 -0
- data/lib/json_schemer/draft202012/vocab/validation.rb +286 -0
- data/lib/json_schemer/draft202012/vocab.rb +103 -0
- data/lib/json_schemer/draft4/meta.rb +155 -0
- data/lib/json_schemer/draft4/vocab/validation.rb +39 -0
- data/lib/json_schemer/draft4/vocab.rb +18 -0
- data/lib/json_schemer/draft6/meta.rb +161 -0
- data/lib/json_schemer/draft6/vocab.rb +16 -0
- data/lib/json_schemer/draft7/meta.rb +178 -0
- data/lib/json_schemer/draft7/vocab/validation.rb +69 -0
- data/lib/json_schemer/draft7/vocab.rb +30 -0
- data/lib/json_schemer/errors.rb +1 -0
- data/lib/json_schemer/format/duration.rb +23 -0
- data/lib/json_schemer/format/json_pointer.rb +18 -0
- data/lib/json_schemer/format.rb +52 -26
- data/lib/json_schemer/keyword.rb +41 -0
- data/lib/json_schemer/location.rb +25 -0
- data/lib/json_schemer/openapi.rb +40 -0
- data/lib/json_schemer/openapi30/document.rb +1673 -0
- data/lib/json_schemer/openapi30/meta.rb +26 -0
- data/lib/json_schemer/openapi30/vocab/base.rb +18 -0
- data/lib/json_schemer/openapi30/vocab.rb +12 -0
- data/lib/json_schemer/openapi31/document.rb +1559 -0
- data/lib/json_schemer/openapi31/meta.rb +128 -0
- data/lib/json_schemer/openapi31/vocab/base.rb +89 -0
- data/lib/json_schemer/openapi31/vocab.rb +18 -0
- data/lib/json_schemer/output.rb +55 -0
- data/lib/json_schemer/result.rb +168 -0
- data/lib/json_schemer/schema.rb +390 -0
- data/lib/json_schemer/version.rb +1 -1
- data/lib/json_schemer.rb +197 -24
- metadata +42 -10
- data/lib/json_schemer/schema/base.rb +0 -677
- data/lib/json_schemer/schema/draft4.json +0 -149
- data/lib/json_schemer/schema/draft4.rb +0 -44
- data/lib/json_schemer/schema/draft6.json +0 -155
- data/lib/json_schemer/schema/draft6.rb +0 -25
- data/lib/json_schemer/schema/draft7.json +0 -172
- data/lib/json_schemer/schema/draft7.rb +0 -32
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module JSONSchemer
|
3
|
+
module OpenAPI31
|
4
|
+
BASE_URI = URI('https://spec.openapis.org/oas/3.1/dialect/base')
|
5
|
+
SCHEMA = {
|
6
|
+
'$id' => 'https://spec.openapis.org/oas/3.1/dialect/base',
|
7
|
+
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
|
8
|
+
|
9
|
+
'title' => 'OpenAPI 3.1 Schema Object Dialect',
|
10
|
+
'description' => 'A JSON Schema dialect describing schemas found in OpenAPI documents',
|
11
|
+
|
12
|
+
'$vocabulary' => {
|
13
|
+
'https://json-schema.org/draft/2020-12/vocab/core' => true,
|
14
|
+
'https://json-schema.org/draft/2020-12/vocab/applicator' => true,
|
15
|
+
'https://json-schema.org/draft/2020-12/vocab/unevaluated' => true,
|
16
|
+
'https://json-schema.org/draft/2020-12/vocab/validation' => true,
|
17
|
+
'https://json-schema.org/draft/2020-12/vocab/meta-data' => true,
|
18
|
+
'https://json-schema.org/draft/2020-12/vocab/format-annotation' => true,
|
19
|
+
'https://json-schema.org/draft/2020-12/vocab/content' => true,
|
20
|
+
'https://spec.openapis.org/oas/3.1/vocab/base' => false
|
21
|
+
},
|
22
|
+
|
23
|
+
'$dynamicAnchor' => 'meta',
|
24
|
+
|
25
|
+
'allOf' => [
|
26
|
+
{ '$ref' => 'https://json-schema.org/draft/2020-12/schema' },
|
27
|
+
{ '$ref' => 'https://spec.openapis.org/oas/3.1/meta/base' }
|
28
|
+
]
|
29
|
+
}
|
30
|
+
|
31
|
+
|
32
|
+
module Meta
|
33
|
+
BASE = {
|
34
|
+
'$id' => 'https://spec.openapis.org/oas/3.1/meta/base',
|
35
|
+
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
|
36
|
+
|
37
|
+
'title' => 'OAS Base vocabulary',
|
38
|
+
'description' => 'A JSON Schema Vocabulary used in the OpenAPI Schema Dialect',
|
39
|
+
|
40
|
+
'$vocabulary' => {
|
41
|
+
'https://spec.openapis.org/oas/3.1/vocab/base' => true
|
42
|
+
},
|
43
|
+
|
44
|
+
'$dynamicAnchor' => 'meta',
|
45
|
+
|
46
|
+
'type' => ['object', 'boolean'],
|
47
|
+
'properties' => {
|
48
|
+
'example' => true,
|
49
|
+
'discriminator' => { '$ref' => '#/$defs/discriminator' },
|
50
|
+
'externalDocs' => { '$ref' => '#/$defs/external-docs' },
|
51
|
+
'xml' => { '$ref' => '#/$defs/xml' }
|
52
|
+
},
|
53
|
+
|
54
|
+
'$defs' => {
|
55
|
+
'extensible' => {
|
56
|
+
'patternProperties' => {
|
57
|
+
'^x-' => true
|
58
|
+
}
|
59
|
+
},
|
60
|
+
|
61
|
+
'discriminator' => {
|
62
|
+
'$ref' => '#/$defs/extensible',
|
63
|
+
'type' => 'object',
|
64
|
+
'properties' => {
|
65
|
+
'propertyName' => {
|
66
|
+
'type' => 'string'
|
67
|
+
},
|
68
|
+
'mapping' => {
|
69
|
+
'type' => 'object',
|
70
|
+
'additionalProperties' => {
|
71
|
+
'type' => 'string'
|
72
|
+
}
|
73
|
+
}
|
74
|
+
},
|
75
|
+
'required' => ['propertyName'],
|
76
|
+
'unevaluatedProperties' => false
|
77
|
+
},
|
78
|
+
|
79
|
+
'external-docs' => {
|
80
|
+
'$ref' => '#/$defs/extensible',
|
81
|
+
'type' => 'object',
|
82
|
+
'properties' => {
|
83
|
+
'url' => {
|
84
|
+
'type' => 'string',
|
85
|
+
'format' => 'uri-reference'
|
86
|
+
},
|
87
|
+
'description' => {
|
88
|
+
'type' => 'string'
|
89
|
+
}
|
90
|
+
},
|
91
|
+
'required' => ['url'],
|
92
|
+
'unevaluatedProperties' => false
|
93
|
+
},
|
94
|
+
|
95
|
+
'xml' => {
|
96
|
+
'$ref' => '#/$defs/extensible',
|
97
|
+
'type' => 'object',
|
98
|
+
'properties' => {
|
99
|
+
'name' => {
|
100
|
+
'type' => 'string'
|
101
|
+
},
|
102
|
+
'namespace' => {
|
103
|
+
'type' => 'string',
|
104
|
+
'format' => 'uri'
|
105
|
+
},
|
106
|
+
'prefix' => {
|
107
|
+
'type' => 'string'
|
108
|
+
},
|
109
|
+
'attribute' => {
|
110
|
+
'type' => 'boolean'
|
111
|
+
},
|
112
|
+
'wrapped' => {
|
113
|
+
'type' => 'boolean'
|
114
|
+
}
|
115
|
+
},
|
116
|
+
'unevaluatedProperties' => false
|
117
|
+
}
|
118
|
+
}
|
119
|
+
}
|
120
|
+
|
121
|
+
|
122
|
+
SCHEMAS = Draft202012::Meta::SCHEMAS.merge(
|
123
|
+
Draft202012::BASE_URI => Draft202012::SCHEMA,
|
124
|
+
URI('https://spec.openapis.org/oas/3.1/meta/base') => BASE
|
125
|
+
)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module JSONSchemer
|
3
|
+
module OpenAPI31
|
4
|
+
module Vocab
|
5
|
+
module Base
|
6
|
+
class AllOf < Draft202012::Vocab::Applicator::AllOf
|
7
|
+
attr_accessor :skip_ref_once
|
8
|
+
|
9
|
+
def validate(instance, instance_location, keyword_location, context)
|
10
|
+
nested = []
|
11
|
+
parsed.each_with_index do |subschema, index|
|
12
|
+
if ref_schema = subschema.parsed['$ref']&.ref_schema
|
13
|
+
next if skip_ref_once == ref_schema.absolute_keyword_location
|
14
|
+
ref_schema.parsed['discriminator']&.skip_ref_once = schema.absolute_keyword_location
|
15
|
+
end
|
16
|
+
nested << subschema.validate_instance(instance, instance_location, join_location(keyword_location, index.to_s), context)
|
17
|
+
end
|
18
|
+
result(instance, instance_location, keyword_location, nested.all?(&:valid), nested)
|
19
|
+
ensure
|
20
|
+
self.skip_ref_once = nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class AnyOf < Draft202012::Vocab::Applicator::AnyOf
|
25
|
+
def validate(*)
|
26
|
+
schema.parsed.key?('discriminator') ? nil : super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class OneOf < Draft202012::Vocab::Applicator::OneOf
|
31
|
+
def validate(*)
|
32
|
+
schema.parsed.key?('discriminator') ? nil : super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Discriminator < Keyword
|
37
|
+
include Format::JSONPointer
|
38
|
+
|
39
|
+
attr_accessor :skip_ref_once
|
40
|
+
|
41
|
+
def error(formatted_instance_location:, **)
|
42
|
+
"value at #{formatted_instance_location} does not match `discriminator` schema"
|
43
|
+
end
|
44
|
+
|
45
|
+
def validate(instance, instance_location, keyword_location, context)
|
46
|
+
property_name = value.fetch('propertyName')
|
47
|
+
mapping = value['mapping'] || {}
|
48
|
+
|
49
|
+
return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash) && instance.key?(property_name)
|
50
|
+
|
51
|
+
property = instance.fetch(property_name)
|
52
|
+
ref = mapping.fetch(property, property)
|
53
|
+
|
54
|
+
ref_schema = nil
|
55
|
+
unless ref.start_with?('#') && valid_json_pointer?(ref.delete_prefix('#'))
|
56
|
+
ref_schema = begin
|
57
|
+
root.resolve_ref(URI.join(schema.base_uri, "#/components/schemas/#{ref}"))
|
58
|
+
rescue InvalidRefPointer
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
ref_schema ||= root.resolve_ref(URI.join(schema.base_uri, ref))
|
63
|
+
|
64
|
+
return if skip_ref_once == ref_schema.absolute_keyword_location
|
65
|
+
|
66
|
+
nested = []
|
67
|
+
|
68
|
+
if schema.parsed.key?('anyOf') || schema.parsed.key?('oneOf')
|
69
|
+
subschemas = schema.parsed['anyOf']&.parsed || []
|
70
|
+
subschemas += schema.parsed['oneOf']&.parsed || []
|
71
|
+
subschemas.each do |subschema|
|
72
|
+
if subschema.parsed.fetch('$ref').ref_schema.absolute_keyword_location == ref_schema.absolute_keyword_location
|
73
|
+
nested << subschema.validate_instance(instance, instance_location, keyword_location, context)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
else
|
77
|
+
ref_schema.parsed['allOf']&.skip_ref_once = schema.absolute_keyword_location
|
78
|
+
nested << ref_schema.validate_instance(instance, instance_location, keyword_location, context)
|
79
|
+
end
|
80
|
+
|
81
|
+
result(instance, instance_location, keyword_location, (nested.any? && nested.all?(&:valid)), nested)
|
82
|
+
ensure
|
83
|
+
self.skip_ref_once = nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module JSONSchemer
|
3
|
+
module OpenAPI31
|
4
|
+
module Vocab
|
5
|
+
# https://spec.openapis.org/oas/latest.html#schema-object
|
6
|
+
BASE = {
|
7
|
+
# https://spec.openapis.org/oas/latest.html#discriminator-object
|
8
|
+
'discriminator' => Base::Discriminator,
|
9
|
+
'allOf' => Base::AllOf,
|
10
|
+
'anyOf' => Base::AnyOf,
|
11
|
+
'oneOf' => Base::OneOf
|
12
|
+
# 'xml' => Base::Xml,
|
13
|
+
# 'externalDocs' => Base::ExternalDocs,
|
14
|
+
# 'example' => Base::Example
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module JSONSchemer
|
3
|
+
module Output
|
4
|
+
FRAGMENT_ENCODE_REGEX = /[^\w?\/:@\-.~!$&'()*+,;=]/
|
5
|
+
|
6
|
+
attr_reader :keyword, :schema
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def result(instance, instance_location, keyword_location, valid, nested = nil, type: nil, annotation: nil, details: nil, ignore_nested: false)
|
11
|
+
if valid
|
12
|
+
Result.new(self, instance, instance_location, keyword_location, valid, nested, type, annotation, details, ignore_nested, 'annotations')
|
13
|
+
else
|
14
|
+
Result.new(self, instance, instance_location, keyword_location, valid, nested, type, annotation, details, ignore_nested, 'errors')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def escaped_keyword
|
19
|
+
@escaped_keyword ||= Location.escape_json_pointer_token(keyword)
|
20
|
+
end
|
21
|
+
|
22
|
+
def join_location(location, keyword)
|
23
|
+
Location.join(location, keyword)
|
24
|
+
end
|
25
|
+
|
26
|
+
def fragment_encode(location)
|
27
|
+
Format.percent_encode(location, FRAGMENT_ENCODE_REGEX)
|
28
|
+
end
|
29
|
+
|
30
|
+
# :nocov:
|
31
|
+
if Symbol.method_defined?(:name)
|
32
|
+
def stringify(key)
|
33
|
+
key.is_a?(Symbol) ? key.name : key.to_s
|
34
|
+
end
|
35
|
+
else
|
36
|
+
def stringify(key)
|
37
|
+
key.to_s
|
38
|
+
end
|
39
|
+
end
|
40
|
+
# :nocov:
|
41
|
+
|
42
|
+
def deep_stringify_keys(obj)
|
43
|
+
case obj
|
44
|
+
when Hash
|
45
|
+
obj.each_with_object({}) do |(key, value), out|
|
46
|
+
out[stringify(key)] = deep_stringify_keys(value)
|
47
|
+
end
|
48
|
+
when Array
|
49
|
+
obj.map { |item| deep_stringify_keys(item) }
|
50
|
+
else
|
51
|
+
obj
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module JSONSchemer
|
3
|
+
Result = Struct.new(:source, :instance, :instance_location, :keyword_location, :valid, :nested, :type, :annotation, :details, :ignore_nested, :nested_key) do
|
4
|
+
CLASSIC_ERROR_TYPES = Hash.new do |hash, klass|
|
5
|
+
hash[klass] = klass.name.rpartition('::').last.sub(/\A[[:alpha:]]/, &:downcase)
|
6
|
+
end
|
7
|
+
|
8
|
+
def output(output_format)
|
9
|
+
case output_format
|
10
|
+
when 'classic'
|
11
|
+
classic
|
12
|
+
when 'flag'
|
13
|
+
flag
|
14
|
+
when 'basic'
|
15
|
+
basic
|
16
|
+
when 'detailed'
|
17
|
+
detailed
|
18
|
+
when 'verbose'
|
19
|
+
verbose
|
20
|
+
else
|
21
|
+
raise UnknownOutputFormat, output_format
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def error
|
26
|
+
return @error if defined?(@error)
|
27
|
+
resolved_instance_location = Location.resolve(instance_location)
|
28
|
+
@error = source.error(
|
29
|
+
:formatted_instance_location => resolved_instance_location.empty? ? 'root' : "`#{resolved_instance_location}`",
|
30
|
+
:details => details
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_output_unit
|
35
|
+
out = {
|
36
|
+
'valid' => valid,
|
37
|
+
'keywordLocation' => Location.resolve(keyword_location),
|
38
|
+
'absoluteKeywordLocation' => source.absolute_keyword_location,
|
39
|
+
'instanceLocation' => Location.resolve(instance_location)
|
40
|
+
}
|
41
|
+
out['error'] = error unless valid
|
42
|
+
out['annotation'] = annotation if valid && annotation
|
43
|
+
out
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_classic
|
47
|
+
schema = source.schema
|
48
|
+
out = {
|
49
|
+
'data' => instance,
|
50
|
+
'data_pointer' => Location.resolve(instance_location),
|
51
|
+
'schema' => schema.value,
|
52
|
+
'schema_pointer' => schema.schema_pointer,
|
53
|
+
'root_schema' => schema.root.value,
|
54
|
+
'type' => type || CLASSIC_ERROR_TYPES[source.class]
|
55
|
+
}
|
56
|
+
out['error'] = error
|
57
|
+
out['details'] = details if details
|
58
|
+
out
|
59
|
+
end
|
60
|
+
|
61
|
+
def flag
|
62
|
+
{ 'valid' => valid }
|
63
|
+
end
|
64
|
+
|
65
|
+
def basic
|
66
|
+
out = to_output_unit
|
67
|
+
if nested&.any?
|
68
|
+
out[nested_key] = Enumerator.new do |yielder|
|
69
|
+
results = [self]
|
70
|
+
while result = results.pop
|
71
|
+
if result.ignore_nested || !result.nested&.any?
|
72
|
+
yielder << result.to_output_unit
|
73
|
+
else
|
74
|
+
previous_results_size = results.size
|
75
|
+
result.nested.reverse_each do |nested_result|
|
76
|
+
results << nested_result if nested_result.valid == valid
|
77
|
+
end
|
78
|
+
yielder << result.to_output_unit unless (results.size - previous_results_size) == 1
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
out
|
84
|
+
end
|
85
|
+
|
86
|
+
def detailed
|
87
|
+
return to_output_unit if ignore_nested || !nested&.any?
|
88
|
+
matching_results = nested.select { |nested_result| nested_result.valid == valid }
|
89
|
+
if matching_results.size == 1
|
90
|
+
matching_results.first.detailed
|
91
|
+
else
|
92
|
+
out = to_output_unit
|
93
|
+
if matching_results.any?
|
94
|
+
out[nested_key] = Enumerator.new do |yielder|
|
95
|
+
matching_results.each { |nested_result| yielder << nested_result.detailed }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
out
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def verbose
|
103
|
+
out = to_output_unit
|
104
|
+
if nested&.any?
|
105
|
+
out[nested_key] = Enumerator.new do |yielder|
|
106
|
+
nested.each { |nested_result| yielder << nested_result.verbose }
|
107
|
+
end
|
108
|
+
end
|
109
|
+
out
|
110
|
+
end
|
111
|
+
|
112
|
+
def classic
|
113
|
+
Enumerator.new do |yielder|
|
114
|
+
unless valid
|
115
|
+
results = [self]
|
116
|
+
while result = results.pop
|
117
|
+
if result.ignore_nested || !result.nested&.any?
|
118
|
+
yielder << result.to_classic
|
119
|
+
else
|
120
|
+
previous_results_size = results.size
|
121
|
+
result.nested.reverse_each do |nested_result|
|
122
|
+
results << nested_result if nested_result.valid == valid
|
123
|
+
end
|
124
|
+
yielder << result.to_classic if (results.size - previous_results_size) == 0
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def insert_property_defaults(context)
|
132
|
+
instance_locations = {}
|
133
|
+
|
134
|
+
results = [[self, true]]
|
135
|
+
while (result, valid = results.pop)
|
136
|
+
next if result.source.is_a?(Schema::NOT_KEYWORD_CLASS)
|
137
|
+
|
138
|
+
valid &&= result.valid
|
139
|
+
result.nested&.each { |nested_result| results << [nested_result, valid] }
|
140
|
+
|
141
|
+
if result.source.is_a?(Schema::PROPERTIES_KEYWORD_CLASS) && result.instance.is_a?(Hash)
|
142
|
+
result.source.parsed.each do |property, schema|
|
143
|
+
next if result.instance.key?(property) || !schema.parsed.key?('default')
|
144
|
+
default = schema.parsed.fetch('default')
|
145
|
+
instance_location = Location.join(result.instance_location, property)
|
146
|
+
keyword_location = Location.join(Location.join(result.keyword_location, property), default.keyword)
|
147
|
+
default_result = default.validate(nil, instance_location, keyword_location, nil)
|
148
|
+
instance_locations[result.instance_location] ||= {}
|
149
|
+
instance_locations[result.instance_location][property] ||= []
|
150
|
+
instance_locations[result.instance_location][property] << [default_result, valid]
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
inserted = false
|
156
|
+
|
157
|
+
instance_locations.each do |instance_location, properties|
|
158
|
+
original_instance = context.original_instance(instance_location)
|
159
|
+
properties.each do |property, results_with_tree_validity|
|
160
|
+
property_inserted = yield(original_instance, property, results_with_tree_validity)
|
161
|
+
inserted ||= (property_inserted != false)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
inserted
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|