reynard 0.9.0 → 0.10.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 +4 -4
- data/lib/reynard/context.rb +3 -5
- data/lib/reynard/external.rb +14 -4
- data/lib/reynard/grouped_parameters.rb +18 -7
- data/lib/reynard/http/response.rb +1 -1
- data/lib/reynard/model.rb +1 -1
- data/lib/reynard/object_builder.rb +0 -2
- data/lib/reynard/schema/model_naming.rb +127 -0
- data/lib/reynard/schema.rb +5 -60
- data/lib/reynard/specification/finder.rb +31 -0
- data/lib/reynard/specification/query.rb +14 -0
- data/lib/reynard/specification.rb +23 -12
- data/lib/reynard/version.rb +1 -1
- data/lib/reynard.rb +1 -1
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c3c34bb5538fc723eb3089aed2fcc9297caecd7c12802081fb1c0ffd5b930885
|
4
|
+
data.tar.gz: fd266bc7821ef67989261e66923605b3ea0b249f19c028ada40815928c070b7b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: edd450cc95fc83f6f16c8e8a77d7600f98d8524fe75a1abc78b383cd6442458acf3351e7ea0513cb539a00870bea79729a57120a4c0b556e54c937dcc0275e63
|
7
|
+
data.tar.gz: b00d56c46b6c29f5ccf01a83a6fc8f62e53a44953787441bb13af36a7025a0712f75ad537507f917644acff2f134edf71502461cb930e0bbe1c5a4a1b0ff9f6f
|
data/lib/reynard/context.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'ostruct'
|
4
|
-
|
5
3
|
class Reynard
|
6
4
|
# Exposes a public interface to build a request context.
|
7
5
|
class Context
|
@@ -15,7 +13,7 @@ class Reynard
|
|
15
13
|
end
|
16
14
|
|
17
15
|
def base_url(base_url)
|
18
|
-
copy(base_url:
|
16
|
+
copy(base_url:)
|
19
17
|
end
|
20
18
|
|
21
19
|
def operation(operation_name)
|
@@ -44,7 +42,7 @@ class Reynard
|
|
44
42
|
end
|
45
43
|
|
46
44
|
def logger(logger)
|
47
|
-
copy(logger:
|
45
|
+
copy(logger:)
|
48
46
|
end
|
49
47
|
|
50
48
|
def execute
|
@@ -74,7 +72,7 @@ class Reynard
|
|
74
72
|
specification: @specification,
|
75
73
|
inflector: @inflector,
|
76
74
|
request_context: @request_context,
|
77
|
-
http_response:
|
75
|
+
http_response:
|
78
76
|
)
|
79
77
|
end
|
80
78
|
end
|
data/lib/reynard/external.rb
CHANGED
@@ -3,9 +3,16 @@
|
|
3
3
|
require 'rack'
|
4
4
|
|
5
5
|
class Reynard
|
6
|
-
# Loads external references.
|
6
|
+
# Loads data for external references from disk.
|
7
7
|
class External
|
8
|
-
|
8
|
+
# Build a new external reference loader.
|
9
|
+
#
|
10
|
+
# @param basepath [String] base path of the OpenAPI specification, we never load any files
|
11
|
+
# higher in the directory tree
|
12
|
+
# @param path [String] base path of the current file, used to resolve relative paths
|
13
|
+
# @param ref [String] the $ref value we actually resolve
|
14
|
+
def initialize(basepath:, path:, ref:)
|
15
|
+
@basepath = basepath
|
9
16
|
@path = path
|
10
17
|
@relative_path, @anchor = ref.split('#', 2)
|
11
18
|
@filename = File.expand_path(@relative_path, @path)
|
@@ -14,7 +21,10 @@ class Reynard
|
|
14
21
|
def path
|
15
22
|
return [] unless @anchor
|
16
23
|
|
17
|
-
|
24
|
+
# Remove explicit absolute (ie. /) and explicitly relative (ie. ./) instructions from the
|
25
|
+
# path expression because we're starting at the root of an external file. We currently don't
|
26
|
+
# support the // syntax.
|
27
|
+
@anchor.to_s.gsub(%r{\A\.?/}, '').split('/')
|
18
28
|
end
|
19
29
|
|
20
30
|
def data
|
@@ -30,7 +40,7 @@ class Reynard
|
|
30
40
|
private
|
31
41
|
|
32
42
|
def filename
|
33
|
-
return @filename if @filename.start_with?(@
|
43
|
+
return @filename if @filename.start_with?(@basepath)
|
34
44
|
|
35
45
|
raise 'You are only allowed to reference files relative to the specification file.'
|
36
46
|
end
|
@@ -3,14 +3,21 @@
|
|
3
3
|
class Reynard
|
4
4
|
# Groups parameters based on the parameters specification.
|
5
5
|
class GroupedParameters
|
6
|
-
def initialize(specification
|
7
|
-
@specification =
|
6
|
+
def initialize(specification:, node:, params:)
|
7
|
+
@specification = specification
|
8
|
+
@node = node
|
8
9
|
@params = params
|
9
10
|
end
|
10
11
|
|
12
|
+
def constraints
|
13
|
+
return @constraints if defined?(@constraints)
|
14
|
+
|
15
|
+
@constraints = actualize(*@node, 'parameters') || actualize(*@node[..-2], 'parameters') || {}
|
16
|
+
end
|
17
|
+
|
11
18
|
def to_h
|
12
19
|
@params.each_with_object({}) do |(name, value), grouped|
|
13
|
-
group_name =
|
20
|
+
group_name = constraints.dig(name, 'in') || 'query'
|
14
21
|
grouped[group_name] ||= {}
|
15
22
|
grouped[group_name].merge!({ name => value })
|
16
23
|
end
|
@@ -18,12 +25,16 @@ class Reynard
|
|
18
25
|
|
19
26
|
private
|
20
27
|
|
21
|
-
def
|
22
|
-
|
28
|
+
def actualize(*node)
|
29
|
+
parameters = @specification.dig(*node)
|
30
|
+
return unless parameters
|
23
31
|
|
24
|
-
|
25
|
-
|
32
|
+
pivot = {}
|
33
|
+
parameters.each.with_index do |attributes, index|
|
34
|
+
attributes = @specification.dig(*node, index) if attributes.key?('$ref')
|
35
|
+
pivot[attributes['name']] = attributes
|
26
36
|
end
|
37
|
+
pivot
|
27
38
|
end
|
28
39
|
end
|
29
40
|
end
|
data/lib/reynard/model.rb
CHANGED
@@ -76,7 +76,7 @@ class Reynard
|
|
76
76
|
property = schema.property_schema(name)
|
77
77
|
return value unless property
|
78
78
|
|
79
|
-
::Reynard::ObjectBuilder.new(schema: property, inflector
|
79
|
+
::Reynard::ObjectBuilder.new(schema: property, inflector:, parsed_body: value).call
|
80
80
|
end
|
81
81
|
|
82
82
|
def self.inflector
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Reynard
|
4
|
+
class Schema
|
5
|
+
# Rummages through the specification to find a suitable model name for a response object.
|
6
|
+
class ModelNaming
|
7
|
+
def initialize(specification:, node:)
|
8
|
+
@specification = specification
|
9
|
+
@node = node
|
10
|
+
end
|
11
|
+
|
12
|
+
def model_name
|
13
|
+
model_name = determine_model_name
|
14
|
+
if class_type == :array
|
15
|
+
"#{model_name}Collection"
|
16
|
+
else
|
17
|
+
model_name
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.title_model_name(model_name)
|
22
|
+
return unless model_name
|
23
|
+
|
24
|
+
model_name
|
25
|
+
.gsub(/[^[:alpha:]]/, ' ')
|
26
|
+
.gsub(/\s{2,}/, ' ')
|
27
|
+
.gsub(/(\s+)([[:alpha:]])/) { Regexp.last_match(2).upcase }
|
28
|
+
.strip
|
29
|
+
end
|
30
|
+
|
31
|
+
# Extracts a model name from a ref when there is a usable value.
|
32
|
+
#
|
33
|
+
# ref_model_name("#/components/schemas/Library") => "Library"
|
34
|
+
def self.ref_model_name(ref)
|
35
|
+
return unless ref
|
36
|
+
|
37
|
+
normalize_ref_model_name(ref.split('/')&.last)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.normalize_ref_model_name(model_name)
|
41
|
+
# 1. Unescape encoded characters to create an UTF-8 string
|
42
|
+
# 2. Remove extensions for regularly used external schema files
|
43
|
+
# 3. Replace all non-alphabetic characters with a space (not allowed in Ruby constant)
|
44
|
+
# 4. Camelcase
|
45
|
+
Rack::Utils.unescape_path(model_name)
|
46
|
+
.gsub(/(.yml|.yaml|.json)\Z/, '')
|
47
|
+
.gsub(/[^[:alpha:]]/, ' ')
|
48
|
+
.gsub(/(\s+)([[:alpha:]])/) { Regexp.last_match(2).upcase }
|
49
|
+
.gsub(/\A(.)/) { Regexp.last_match(1).upcase }
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.singularize(name)
|
53
|
+
case name
|
54
|
+
when /ies$/
|
55
|
+
"#{name[0..-4]}y"
|
56
|
+
else
|
57
|
+
name.chomp('s')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def class_type
|
64
|
+
@specification.dig(*@node).key?('items') ? :array : :object
|
65
|
+
end
|
66
|
+
|
67
|
+
def determine_model_name
|
68
|
+
title_model_name || ref_model_name || node_model_name
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns a model name when it was explicitly set using the title property in the specification.
|
72
|
+
def title_model_name
|
73
|
+
title = @specification.dig(*@node, 'title')
|
74
|
+
return unless title
|
75
|
+
|
76
|
+
self.class.title_model_name(title)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns a model name based on the schema's $ref value, usually this contains a usable
|
80
|
+
# identifier at the end like /books.yml or /Books.
|
81
|
+
def ref_model_name
|
82
|
+
parent = @specification.dig(*@node[..-2])
|
83
|
+
ref = parent.dig('schema', '$ref') || parent.dig('items', '$ref')
|
84
|
+
return unless ref
|
85
|
+
|
86
|
+
self.class.ref_model_name(ref)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns a model name based on the node path to schema in the specification.
|
90
|
+
def node_model_name
|
91
|
+
self.class.title_model_name(node_path_name.capitalize.gsub(/[_-]/, ' '))
|
92
|
+
end
|
93
|
+
|
94
|
+
def node_path_name
|
95
|
+
if node_anyonymous?
|
96
|
+
request_path_model_name
|
97
|
+
elsif @node.last == 'items'
|
98
|
+
# Use the property name as the model name for its items, for example in the case of
|
99
|
+
# schema > properties > birds > items => bird.
|
100
|
+
self.class.singularize(@node.at(-2))
|
101
|
+
else
|
102
|
+
# Usually this means we are dealing with a property's name a not a model, for example in
|
103
|
+
# the case of schema > properties > color => color.
|
104
|
+
@node.last
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns true when the node path doesn't have identifyable segments other than the request
|
109
|
+
# path for the resource.
|
110
|
+
#
|
111
|
+
# For example, when the last part of the path looks like this:
|
112
|
+
# get > responses > 200 > content > application|json > schema
|
113
|
+
def node_anyonymous?
|
114
|
+
@node.last == 'schema' || @node.last(2) == %w[schema items]
|
115
|
+
end
|
116
|
+
|
117
|
+
# Finds the first segment starting from the end of the request path that is not a parameter
|
118
|
+
# and transforms that to make a model name.
|
119
|
+
#
|
120
|
+
# For example:
|
121
|
+
# /books/{id} => "book"
|
122
|
+
def request_path_model_name
|
123
|
+
self.class.singularize(@node[1].split('/').reverse.find { |part| !part.start_with?('{') })
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/reynard/schema.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
class Reynard
|
4
4
|
# Holds a references to a schema definition in the specification.
|
5
5
|
class Schema
|
6
|
+
autoload :ModelNaming, 'reynard/schema/model_naming'
|
7
|
+
|
6
8
|
attr_reader :node, :namespace
|
7
9
|
|
8
10
|
def initialize(specification:, node:, namespace: nil)
|
@@ -18,9 +20,7 @@ class Reynard
|
|
18
20
|
end
|
19
21
|
|
20
22
|
def model_name
|
21
|
-
|
22
|
-
|
23
|
-
@model_name = find_model_name
|
23
|
+
@model_name || model_naming.model_name
|
24
24
|
end
|
25
25
|
|
26
26
|
# Returns the schema for items when the current schema is an array.
|
@@ -46,65 +46,10 @@ class Reynard
|
|
46
46
|
)
|
47
47
|
end
|
48
48
|
|
49
|
-
def self.title_model_name(model_name)
|
50
|
-
return unless model_name
|
51
|
-
|
52
|
-
model_name
|
53
|
-
.gsub(/[^[:alpha:]]/, ' ')
|
54
|
-
.gsub(/\s{2,}/, ' ')
|
55
|
-
.gsub(/(\s+)([[:alpha:]])/) { Regexp.last_match(2).upcase }
|
56
|
-
.strip
|
57
|
-
end
|
58
|
-
|
59
|
-
# Extracts a model name from a ref when there is a usable value.
|
60
|
-
#
|
61
|
-
# ref_model_name("#/components/schemas/Library") => "Library"
|
62
|
-
def self.ref_model_name(ref)
|
63
|
-
return unless ref
|
64
|
-
|
65
|
-
normalize_ref_model_name(ref.split('/')&.last)
|
66
|
-
end
|
67
|
-
|
68
|
-
def self.normalize_ref_model_name(model_name)
|
69
|
-
# 1. Unescape encoded characters to create an UTF-8 string
|
70
|
-
# 2. Remove extensions for regularly used external schema files
|
71
|
-
# 3. Replace all non-alphabetic characters with a space (not allowed in Ruby constant)
|
72
|
-
# 4. Camelcase
|
73
|
-
Rack::Utils.unescape_path(model_name)
|
74
|
-
.gsub(/(.yml|.yaml|.json)\Z/, '')
|
75
|
-
.gsub(/[^[:alpha:]]/, ' ')
|
76
|
-
.gsub(/(\s+)([[:alpha:]])/) { Regexp.last_match(2).upcase }
|
77
|
-
.gsub(/\A(.)/) { Regexp.last_match(1).upcase }
|
78
|
-
end
|
79
|
-
|
80
49
|
private
|
81
50
|
|
82
|
-
|
83
|
-
|
84
|
-
title_model_name || ref_model_name || node_model_name
|
85
|
-
end
|
86
|
-
|
87
|
-
def title_model_name
|
88
|
-
title = @specification.dig(*node, 'title')
|
89
|
-
return unless title
|
90
|
-
|
91
|
-
self.class.title_model_name(title)
|
92
|
-
end
|
93
|
-
|
94
|
-
def ref_model_name
|
95
|
-
parent = @specification.dig(*node[..-2])
|
96
|
-
ref = parent.dig('schema', '$ref') || parent.dig('items', '$ref')
|
97
|
-
return unless ref
|
98
|
-
|
99
|
-
self.class.ref_model_name(ref)
|
100
|
-
end
|
101
|
-
|
102
|
-
def node_model_name
|
103
|
-
self.class.title_model_name(node_property_name.capitalize.gsub(/[_-]/, ' '))
|
104
|
-
end
|
105
|
-
|
106
|
-
def node_property_name
|
107
|
-
node.last == 'items' ? node.at(-2).chomp('s') : node.last
|
51
|
+
def model_naming
|
52
|
+
ModelNaming.new(specification: @specification, node: @node)
|
108
53
|
end
|
109
54
|
end
|
110
55
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Reynard
|
4
|
+
class Specification
|
5
|
+
# Finds nodes in a specification that match a query.
|
6
|
+
class Finder
|
7
|
+
def initialize(specification:, query:)
|
8
|
+
@specification = specification
|
9
|
+
@query = query
|
10
|
+
end
|
11
|
+
|
12
|
+
def find_each(&)
|
13
|
+
find_into([], &)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def find_into(path, &block)
|
19
|
+
data = @specification.dig(*path)
|
20
|
+
|
21
|
+
yield path if data.respond_to?(:key?) && data.key?('type') && (@query.type == data['type'])
|
22
|
+
|
23
|
+
return unless data.respond_to?(:each_key)
|
24
|
+
|
25
|
+
data.each_key do |key|
|
26
|
+
find_into([*path, key], &block)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -5,6 +5,9 @@ require 'rack'
|
|
5
5
|
class Reynard
|
6
6
|
# Wraps the YAML representation of an OpenAPI specification.
|
7
7
|
class Specification
|
8
|
+
autoload :Finder, 'reynard/specification/finder'
|
9
|
+
autoload :Query, 'reynard/specification/query'
|
10
|
+
|
8
11
|
VERBS = %w[get put post delete options head patch trace].freeze
|
9
12
|
|
10
13
|
def initialize(filename:)
|
@@ -17,6 +20,15 @@ class Reynard
|
|
17
20
|
dig_into(@data, @data, path.dup, File.dirname(@filename))
|
18
21
|
end
|
19
22
|
|
23
|
+
# Yields all nodes in the specification matching the specified type.
|
24
|
+
#
|
25
|
+
# specification.find_each(type: 'object') {}
|
26
|
+
#
|
27
|
+
# Please don't use this in a hot paths in production, primarily meant for testing and tooling.
|
28
|
+
def find_each(type:, &block)
|
29
|
+
Finder.new(specification: self, query: Query.new(type:)).find_each(&block)
|
30
|
+
end
|
31
|
+
|
20
32
|
def servers
|
21
33
|
dig('servers').map { |attributes| Server.new(attributes) }
|
22
34
|
end
|
@@ -35,13 +47,9 @@ class Reynard
|
|
35
47
|
return {} unless params
|
36
48
|
|
37
49
|
GroupedParameters.new(
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
dig(*operation_node, 'parameters'),
|
42
|
-
dig(*operation_node[..-2], 'parameters')
|
43
|
-
].compact.flatten,
|
44
|
-
params
|
50
|
+
specification: self,
|
51
|
+
node: operation_node,
|
52
|
+
params:
|
45
53
|
).to_h
|
46
54
|
end
|
47
55
|
|
@@ -52,7 +60,8 @@ class Reynard
|
|
52
60
|
end
|
53
61
|
|
54
62
|
def operation(operation_name)
|
55
|
-
dig('paths').
|
63
|
+
dig('paths').each_key do |path|
|
64
|
+
operations = dig('paths', path)
|
56
65
|
operations.slice(*VERBS).each do |verb, operation|
|
57
66
|
return Operation.new(node: ['paths', path, verb]) if operation_name == operation['operationId']
|
58
67
|
end
|
@@ -101,7 +110,6 @@ class Reynard
|
|
101
110
|
end
|
102
111
|
|
103
112
|
# rubocop:disable Metrics/AbcSize
|
104
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
105
113
|
# rubocop:disable Metrics/MethodLength
|
106
114
|
def dig_into(data, cursor, path, filesystem_path)
|
107
115
|
while path.length.positive?
|
@@ -117,8 +125,8 @@ class Reynard
|
|
117
125
|
path = Rack::Utils.unescape_path(cursor['$ref'][2..]).split('/') + path
|
118
126
|
cursor = data
|
119
127
|
# References another file, with an optional anchor to an element in the data.
|
120
|
-
|
121
|
-
external = External.new(path: filesystem_path, ref: cursor['$ref'])
|
128
|
+
else
|
129
|
+
external = External.new(basepath:, path: filesystem_path, ref: cursor['$ref'])
|
122
130
|
filesystem_path = external.filesystem_path
|
123
131
|
path = external.path + path
|
124
132
|
cursor = external.data
|
@@ -127,7 +135,10 @@ class Reynard
|
|
127
135
|
cursor
|
128
136
|
end
|
129
137
|
# rubocop:enable Metrics/AbcSize
|
130
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
131
138
|
# rubocop:enable Metrics/MethodLength
|
139
|
+
|
140
|
+
def basepath
|
141
|
+
File.dirname(File.expand_path(@filename))
|
142
|
+
end
|
132
143
|
end
|
133
144
|
end
|
data/lib/reynard/version.rb
CHANGED
data/lib/reynard.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reynard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Manfred Stienstra
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: multi_json
|
@@ -78,9 +77,12 @@ files:
|
|
78
77
|
- lib/reynard/operation.rb
|
79
78
|
- lib/reynard/request_context.rb
|
80
79
|
- lib/reynard/schema.rb
|
80
|
+
- lib/reynard/schema/model_naming.rb
|
81
81
|
- lib/reynard/serialized_body.rb
|
82
82
|
- lib/reynard/server.rb
|
83
83
|
- lib/reynard/specification.rb
|
84
|
+
- lib/reynard/specification/finder.rb
|
85
|
+
- lib/reynard/specification/query.rb
|
84
86
|
- lib/reynard/template.rb
|
85
87
|
- lib/reynard/version.rb
|
86
88
|
homepage: https://github.com/Manfred/reynard
|
@@ -88,7 +90,6 @@ licenses:
|
|
88
90
|
- MIT
|
89
91
|
metadata:
|
90
92
|
rubygems_mfa_required: 'true'
|
91
|
-
post_install_message:
|
92
93
|
rdoc_options: []
|
93
94
|
require_paths:
|
94
95
|
- lib
|
@@ -96,15 +97,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
96
97
|
requirements:
|
97
98
|
- - ">"
|
98
99
|
- !ruby/object:Gem::Version
|
99
|
-
version: '3.
|
100
|
+
version: '3.1'
|
100
101
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
102
|
requirements:
|
102
103
|
- - ">="
|
103
104
|
- !ruby/object:Gem::Version
|
104
105
|
version: '0'
|
105
106
|
requirements: []
|
106
|
-
rubygems_version: 3.
|
107
|
-
signing_key:
|
107
|
+
rubygems_version: 3.6.7
|
108
108
|
specification_version: 4
|
109
109
|
summary: Minimal OpenAPI client.
|
110
110
|
test_files: []
|