oas_parser_reborn 0.25.5
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/CHANGELOG.md +4 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +114 -0
- data/Rakefile +6 -0
- data/lib/oas_parser_reborn/abstract_attribute.rb +89 -0
- data/lib/oas_parser_reborn/callback.rb +20 -0
- data/lib/oas_parser_reborn/definition.rb +61 -0
- data/lib/oas_parser_reborn/endpoint.rb +137 -0
- data/lib/oas_parser_reborn/errors.rb +8 -0
- data/lib/oas_parser_reborn/parameter.rb +44 -0
- data/lib/oas_parser_reborn/parser.rb +77 -0
- data/lib/oas_parser_reborn/path.rb +45 -0
- data/lib/oas_parser_reborn/payload.rb +21 -0
- data/lib/oas_parser_reborn/pointer.rb +64 -0
- data/lib/oas_parser_reborn/property.rb +54 -0
- data/lib/oas_parser_reborn/raw_accessor.rb +26 -0
- data/lib/oas_parser_reborn/request_body.rb +61 -0
- data/lib/oas_parser_reborn/response.rb +18 -0
- data/lib/oas_parser_reborn/response_parser.rb +227 -0
- data/lib/oas_parser_reborn/version.rb +3 -0
- data/lib/oas_parser_reborn/webhook.rb +9 -0
- data/lib/oas_parser_reborn.rb +22 -0
- metadata +264 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
module OasParser
|
2
|
+
class Pointer
|
3
|
+
def initialize(raw_pointer)
|
4
|
+
@raw_pointer = raw_pointer
|
5
|
+
end
|
6
|
+
|
7
|
+
def resolve(document)
|
8
|
+
return document if escaped_pointer == ""
|
9
|
+
|
10
|
+
tokens.reduce(document) do |nested_doc, token|
|
11
|
+
nested_doc.fetch(token) unless !nested_doc.key?(token)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Detect circular reference by checking whether the ref exists in current path.
|
16
|
+
#
|
17
|
+
# Example:
|
18
|
+
# components:
|
19
|
+
# schemas:
|
20
|
+
# Pet:
|
21
|
+
# type: object
|
22
|
+
# properties:
|
23
|
+
# name:
|
24
|
+
# type: string
|
25
|
+
# children:
|
26
|
+
# type: array
|
27
|
+
# items: # <--- parsing here
|
28
|
+
# - $ref: '#/components/schemas/Pet'
|
29
|
+
#
|
30
|
+
# path: "/components/schemas/Pet/properties/children/items"
|
31
|
+
# raw_pointer: "#/components/schemas/Pet"
|
32
|
+
#
|
33
|
+
# It'd return true when we're parsing the pet children items where the ref points back to itself.
|
34
|
+
def circular_reference?(path)
|
35
|
+
path.include?("#{escaped_pointer}/")
|
36
|
+
end
|
37
|
+
|
38
|
+
def escaped_pointer
|
39
|
+
if @raw_pointer.start_with?("#")
|
40
|
+
Addressable::URI.unencode(@raw_pointer[1..-1])
|
41
|
+
else
|
42
|
+
@raw_pointer
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def parse_token(token)
|
49
|
+
token.gsub("~0", "~").gsub("~1", "/")
|
50
|
+
end
|
51
|
+
|
52
|
+
def tokens
|
53
|
+
tokens = escaped_pointer[1..-1].split("/")
|
54
|
+
|
55
|
+
if @raw_pointer.end_with?("/")
|
56
|
+
tokens << ""
|
57
|
+
end
|
58
|
+
|
59
|
+
tokens.map do |token|
|
60
|
+
parse_token(token)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module OasParser
|
2
|
+
class Property < AbstractAttribute
|
3
|
+
raw_keys :description, :type, :format, :minimum, :maximum,
|
4
|
+
:example, :default, :items, :nullable
|
5
|
+
|
6
|
+
attr_accessor :owner, :schema, :raw
|
7
|
+
attr_writer :name
|
8
|
+
|
9
|
+
def initialize(owner, schema, name, raw)
|
10
|
+
super(name)
|
11
|
+
@owner = owner
|
12
|
+
@schema = schema
|
13
|
+
@raw = raw
|
14
|
+
end
|
15
|
+
|
16
|
+
def required
|
17
|
+
return false unless schema['required']
|
18
|
+
schema['required'].include? name
|
19
|
+
end
|
20
|
+
|
21
|
+
def nullable?
|
22
|
+
return true if raw['nullable']
|
23
|
+
return false unless schema['nullable']
|
24
|
+
end
|
25
|
+
|
26
|
+
def convert_property_schema_to_properties(schema)
|
27
|
+
if schema['allOf']
|
28
|
+
schema['properties'] = {}
|
29
|
+
schema['allOf'].each do |p|
|
30
|
+
schema['properties'].deep_merge!(p['properties'])
|
31
|
+
end
|
32
|
+
schema.delete('allOf')
|
33
|
+
end
|
34
|
+
|
35
|
+
if schema['oneOf']
|
36
|
+
schema['oneOf'].map do |subschema|
|
37
|
+
subschema['properties'] = convert_property_schema_to_properties(subschema)
|
38
|
+
subschema['subschema_property'] = true
|
39
|
+
subschema
|
40
|
+
end
|
41
|
+
elsif schema['subschema_property']
|
42
|
+
schema = schema['properties'] if schema['properties']
|
43
|
+
schema.map do |definition|
|
44
|
+
OasParser::Property.new(self, raw, definition.name, definition.raw)
|
45
|
+
end
|
46
|
+
else
|
47
|
+
schema = schema['properties'] if schema['properties']
|
48
|
+
schema.map do |key, definition|
|
49
|
+
OasParser::Property.new(self, raw, key, definition)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module OasParser
|
2
|
+
module RawAccessor
|
3
|
+
def self.included(klass)
|
4
|
+
klass.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
def method_missing(method_name, *args, &block)
|
8
|
+
super unless self.class.get_raw_keys.include? method_name
|
9
|
+
raw[method_name.to_s]
|
10
|
+
end
|
11
|
+
|
12
|
+
def respond_to_missing?(method_name, include_private = false)
|
13
|
+
self.class.get_raw_keys.include?(method_name) || super
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def raw_keys(*args)
|
18
|
+
@raw_keys = args
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_raw_keys
|
22
|
+
@raw_keys || []
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module OasParser
|
2
|
+
class RequestBody < Payload
|
3
|
+
include OasParser::RawAccessor
|
4
|
+
raw_keys :description, :required, :content
|
5
|
+
|
6
|
+
attr_accessor :endpoint, :raw
|
7
|
+
|
8
|
+
def initialize(endpoint, raw)
|
9
|
+
@endpoint = endpoint
|
10
|
+
@raw = raw
|
11
|
+
end
|
12
|
+
|
13
|
+
def properties_for_format(format)
|
14
|
+
s = schema(format)
|
15
|
+
s = handle_all_of(s)
|
16
|
+
s['properties'].map do |name, definition|
|
17
|
+
OasParser::Property.new(self, s, name, definition)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def split_properties_for_format(format)
|
22
|
+
split_schemas(format).map do |schema|
|
23
|
+
schema = handle_all_of(schema)
|
24
|
+
schema['properties'].map do |name, definition|
|
25
|
+
OasParser::Property.new(self, schema, name, definition)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def handle_all_of(schema)
|
31
|
+
newSchema = {}
|
32
|
+
if schema['allOf']
|
33
|
+
schema['allOf'].each do |p|
|
34
|
+
newSchema = deep_safe_merge(newSchema, p)
|
35
|
+
if newSchema['allOf']
|
36
|
+
newSchema = deep_safe_merge(newSchema, handle_all_of(newSchema))
|
37
|
+
newSchema.delete('allOf')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
else
|
41
|
+
newSchema = schema
|
42
|
+
end
|
43
|
+
|
44
|
+
newSchema
|
45
|
+
end
|
46
|
+
|
47
|
+
def deep_safe_merge(source_hash, new_hash)
|
48
|
+
source_hash.merge(new_hash) do |key, old, new|
|
49
|
+
if new.respond_to?(:blank) && new.blank?
|
50
|
+
old
|
51
|
+
elsif (old.kind_of?(Hash) and new.kind_of?(Hash))
|
52
|
+
old.deep_merge(new)
|
53
|
+
elsif (old.kind_of?(Array) and new.kind_of?(Array))
|
54
|
+
old.concat(new).uniq
|
55
|
+
else
|
56
|
+
new
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module OasParser
|
2
|
+
class Response < Payload
|
3
|
+
include OasParser::RawAccessor
|
4
|
+
raw_keys :description, :content
|
5
|
+
|
6
|
+
attr_accessor :endpoint, :code, :raw
|
7
|
+
|
8
|
+
def initialize(endpoint, code, raw)
|
9
|
+
@endpoint = endpoint
|
10
|
+
@code = code
|
11
|
+
@raw = raw
|
12
|
+
end
|
13
|
+
|
14
|
+
def success?
|
15
|
+
code.match?(/^2\d\d$/)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
module OasParser
|
2
|
+
class ResponseParser
|
3
|
+
attr_accessor :raw
|
4
|
+
|
5
|
+
def initialize(raw)
|
6
|
+
@raw = raw
|
7
|
+
end
|
8
|
+
|
9
|
+
def parse(mode = nil)
|
10
|
+
@mode = mode
|
11
|
+
route(@raw)
|
12
|
+
end
|
13
|
+
|
14
|
+
def json
|
15
|
+
parse('json').to_json
|
16
|
+
end
|
17
|
+
|
18
|
+
def xml(xml_options = {})
|
19
|
+
xml_options ||= {}
|
20
|
+
xml_options = default_xml_options.merge(xml_options)
|
21
|
+
|
22
|
+
raw_xml = parse('xml').to_xml(xml_options)
|
23
|
+
|
24
|
+
xml_document = Nokogiri::XML(raw_xml)
|
25
|
+
|
26
|
+
xml_document.xpath('//__attributes').each do |attributes|
|
27
|
+
attributes.children.each do |attribute|
|
28
|
+
next unless attribute.class == Nokogiri::XML::Element
|
29
|
+
attribute.parent.parent.css("> #{attribute.name}").remove
|
30
|
+
attribute.parent.parent[attribute.name] = attribute.content
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
xml_document.xpath('//__array_attributes').each do |attributes|
|
35
|
+
attributes.children.each do |attribute|
|
36
|
+
next unless attribute.class == Nokogiri::XML::Element
|
37
|
+
|
38
|
+
parameter = {}
|
39
|
+
parameter['example'] = attribute.css('example').text if attribute.css('example')
|
40
|
+
parameter['type'] = attribute.css('type').text if attribute.css('type')
|
41
|
+
|
42
|
+
attribute.parent.parent.parent[attribute.name] = parameter_value(parameter)
|
43
|
+
end
|
44
|
+
attributes.parent.remove
|
45
|
+
end
|
46
|
+
|
47
|
+
xml_document.xpath('//__text').each do |attribute|
|
48
|
+
value = attribute.children.last.content
|
49
|
+
attribute.parent.content = value
|
50
|
+
end
|
51
|
+
|
52
|
+
xml_document.xpath('//__attributes').each(&:remove)
|
53
|
+
|
54
|
+
xml_document.to_xml.each_line.reject { |x| x.strip == '' }.join
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def default_xml_options
|
60
|
+
{
|
61
|
+
dasherize: false,
|
62
|
+
skip_types: true,
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def route(root_object)
|
67
|
+
case root_object['type']
|
68
|
+
when 'object'
|
69
|
+
parse_object(root_object)
|
70
|
+
when 'array' then parse_array(root_object)
|
71
|
+
when 'string' then root_object['example']
|
72
|
+
when 'integer' then root_object['example']
|
73
|
+
when 'number' then root_object['example']
|
74
|
+
when nil
|
75
|
+
return nil if root_object['additionalProperties'] == false
|
76
|
+
return nil if root_object['properties'] == {}
|
77
|
+
|
78
|
+
if treat_as_object?(root_object)
|
79
|
+
# Handle objects with missing type
|
80
|
+
return parse_object(root_object.merge({ 'type' => 'object' }))
|
81
|
+
end
|
82
|
+
|
83
|
+
raise StandardError.new("Unhandled object #{root_object} with missing type")
|
84
|
+
else
|
85
|
+
raise StandardError.new("Don't know how to parse #{root_object['type']}")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def parse_object(object)
|
90
|
+
raise StandardError.new("Not a hash") unless object.class == Hash
|
91
|
+
raise StandardError.new("Not an object") unless treat_as_object?(object)
|
92
|
+
|
93
|
+
if object['allOf']
|
94
|
+
merged_object = { 'type' => 'object' }
|
95
|
+
object['allOf'].each { |o| merged_object.deep_merge!(o) }
|
96
|
+
return parameter_value(merged_object)
|
97
|
+
elsif object['properties']
|
98
|
+
o = {}
|
99
|
+
object['properties'].each do |key, value|
|
100
|
+
if @mode == 'xml'
|
101
|
+
if is_xml_attribute?(value)
|
102
|
+
o['__attributes'] ||= {}
|
103
|
+
o['__attributes'][key] = parameter_value(value)
|
104
|
+
end
|
105
|
+
|
106
|
+
if is_xml_text?(value)
|
107
|
+
o['__text'] = parameter_value(value)
|
108
|
+
end
|
109
|
+
|
110
|
+
if has_xml_name?(value)
|
111
|
+
key = xml_name(value)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
o[key] = parameter_value(value)
|
116
|
+
end
|
117
|
+
|
118
|
+
return o
|
119
|
+
end
|
120
|
+
|
121
|
+
{}
|
122
|
+
end
|
123
|
+
|
124
|
+
def parse_array(object)
|
125
|
+
raise StandardError.new("Not an array") unless object['type'] == 'array'
|
126
|
+
|
127
|
+
attributes = {}
|
128
|
+
|
129
|
+
if object['properties']
|
130
|
+
if @mode == 'xml'
|
131
|
+
object['properties'].each do |key, value|
|
132
|
+
if is_xml_attribute?(value)
|
133
|
+
attributes[key] = value
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
if object['items'] && (object['items']['oneOf'] || object['items']['anyOf'])
|
140
|
+
key = object['items']['oneOf'] ? 'oneOf' : 'anyOf'
|
141
|
+
items = object['items'][key].map do |obj|
|
142
|
+
route(obj)
|
143
|
+
end
|
144
|
+
|
145
|
+
items.push({ '__array_attributes' => attributes }) if attributes.any? && @mode == 'xml'
|
146
|
+
return items
|
147
|
+
end
|
148
|
+
|
149
|
+
case object['items']['type']
|
150
|
+
when 'object'
|
151
|
+
if attributes.any? && @mode == 'xml'
|
152
|
+
[parse_object(object['items']), { '__array_attributes' => attributes }]
|
153
|
+
else
|
154
|
+
[parse_object(object['items'])]
|
155
|
+
end
|
156
|
+
else
|
157
|
+
if object['items']
|
158
|
+
# Handle objects with missing type
|
159
|
+
object['items']['type'] = 'object'
|
160
|
+
if @mode == 'xml'
|
161
|
+
[parse_object(object['items']), { '__array_attributes' => attributes }]
|
162
|
+
else
|
163
|
+
[parse_object(object['items'])]
|
164
|
+
end
|
165
|
+
else
|
166
|
+
raise StandardError.new("parse_array: Don't know how to parse object")
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def parameter_value(parameter)
|
172
|
+
return parameter['example'] if parameter['example']
|
173
|
+
case (parameter['schema'] ? parameter['schema']['type'] : parameter['type'])
|
174
|
+
when 'integer' then return 1
|
175
|
+
when 'number' then return 1.0
|
176
|
+
when 'string' then return 'abc123'
|
177
|
+
when 'boolean' then return false
|
178
|
+
when 'object' then return parse_object(parameter)
|
179
|
+
when 'array' then return parse_array(parameter)
|
180
|
+
else
|
181
|
+
if treat_as_object?(parameter)
|
182
|
+
return parse_object(parameter)
|
183
|
+
end
|
184
|
+
|
185
|
+
if parameter['type']
|
186
|
+
raise StandardError.new("Can not resolve parameter type of #{parameter['type']}")
|
187
|
+
else
|
188
|
+
raise StandardError.new("Parameter #{parameter} is missing type.")
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
def treat_as_object?(object)
|
195
|
+
return true if object['type'] == 'object'
|
196
|
+
return true if object['allOf']
|
197
|
+
return true if object['oneOf']
|
198
|
+
return true if object['properties']
|
199
|
+
false
|
200
|
+
end
|
201
|
+
|
202
|
+
def has_xml_options?(object)
|
203
|
+
object['xml'].present?
|
204
|
+
end
|
205
|
+
|
206
|
+
def is_xml_attribute?(object)
|
207
|
+
return false unless has_xml_options?(object)
|
208
|
+
object['xml']['attribute'] || false
|
209
|
+
end
|
210
|
+
|
211
|
+
def is_xml_text?(object)
|
212
|
+
# See: https://github.com/OAI/OpenAPI-Specification/issues/630#issuecomment-350680346
|
213
|
+
return false unless has_xml_options?(object)
|
214
|
+
return true if object['xml']['text'] || false
|
215
|
+
object['xml']['x-text'] || false
|
216
|
+
end
|
217
|
+
|
218
|
+
def has_xml_name?(object)
|
219
|
+
return false unless has_xml_options?(object)
|
220
|
+
xml_name(object) || false
|
221
|
+
end
|
222
|
+
|
223
|
+
def xml_name(object)
|
224
|
+
object['xml']['name']
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'uri'
|
3
|
+
require 'yaml'
|
4
|
+
require 'active_support/core_ext/object/blank'
|
5
|
+
require 'active_support/core_ext/hash/conversions'
|
6
|
+
require 'nokogiri'
|
7
|
+
|
8
|
+
require 'addressable/uri'
|
9
|
+
require 'deep_merge'
|
10
|
+
|
11
|
+
require_relative 'oas_parser_reborn/path.rb'
|
12
|
+
require_relative 'oas_parser_reborn/payload.rb'
|
13
|
+
require_relative 'oas_parser_reborn/raw_accessor.rb'
|
14
|
+
require_relative 'oas_parser_reborn/abstract_attribute.rb'
|
15
|
+
|
16
|
+
Dir["#{File.dirname(__FILE__)}/oas_parser_reborn/**/*.rb"].each do |file|
|
17
|
+
require file
|
18
|
+
end
|
19
|
+
|
20
|
+
module OasParser
|
21
|
+
# Your code goes here...
|
22
|
+
end
|