qna 0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: adb0435bdedde64951c33f9adb0c8e034a4e93ef91eed2066607574e58c285f2
4
+ data.tar.gz: 2d083475967af5c83aaee36cd5589fa175136043e8fbe92605f1c3e2bd0c3688
5
+ SHA512:
6
+ metadata.gz: beac1c493217d40e57269e63f0a6cb7735c1a9a64df59b2b6ed3b147e649d4527d546c6a42bb5b3570f960a28f66df27119d8eb75e5ae622c39cc31ae1fcadaa
7
+ data.tar.gz: cc2b6a4a1b75d1cdde1570ffb7276ab9a69951cf24c0059e503065c5022cc57476150f3d2e99876ff4b93cb3487ac301a8956bd1b2ec20c05bb5a6e151f79ead
data/.github/test.yml ADDED
@@ -0,0 +1,27 @@
1
+ name: Tests
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ strategy:
8
+ fail-fast: false
9
+ matrix:
10
+ os: [ubuntu-latest]
11
+ ruby: [2.6, 2.7, 3]
12
+
13
+ name: >-
14
+ ${{matrix.os}}, ${{matrix.ruby}}
15
+
16
+ runs-on: ${{matrix.os}}
17
+ steps:
18
+ - uses: actions/checkout@v1
19
+ - uses: actions/setup-ruby@v1
20
+ with:
21
+ ruby-version: ${{matrix.ruby}}
22
+ - name: Install dependencies
23
+ run: |
24
+ gem install bundler
25
+ bundle install
26
+ - name: Run tests
27
+ run: bundle exec rake test
data/.gitignore ADDED
@@ -0,0 +1,56 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ # Ignore Byebug command history file.
17
+ .byebug_history
18
+
19
+ ## Specific to RubyMotion:
20
+ .dat*
21
+ .repl_history
22
+ build/
23
+ *.bridgesupport
24
+ build-iPhoneOS/
25
+ build-iPhoneSimulator/
26
+
27
+ ## Specific to RubyMotion (use of CocoaPods):
28
+ #
29
+ # We recommend against adding the Pods directory to your .gitignore. However
30
+ # you should judge for yourself, the pros and cons are mentioned at:
31
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
32
+ #
33
+ # vendor/Pods/
34
+
35
+ ## Documentation cache and generated files:
36
+ /.yardoc/
37
+ /_yardoc/
38
+ /doc/
39
+ /rdoc/
40
+
41
+ ## Environment normalization:
42
+ /.bundle/
43
+ /vendor/bundle
44
+ /lib/bundler/man/
45
+
46
+ # for a library or gem, you might want to ignore these files since the code is
47
+ # intended to run in multiple environments; otherwise, check them in:
48
+ # Gemfile.lock
49
+ # .ruby-version
50
+ # .ruby-gemset
51
+
52
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
53
+ .rvmrc
54
+
55
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56
+ # .rubocop-https?--*
data/CHANGELOG.md ADDED
File without changes
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,32 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ qna (0.1)
5
+ escape_utils (~> 1.2.1)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ansi (1.5.0)
11
+ builder (3.2.4)
12
+ escape_utils (1.2.1)
13
+ minitest (5.11.3)
14
+ minitest-reporters (1.4.3)
15
+ ansi
16
+ builder
17
+ minitest (>= 5.0)
18
+ ruby-progressbar
19
+ rake (12.3.3)
20
+ ruby-progressbar (1.11.0)
21
+
22
+ PLATFORMS
23
+ ruby
24
+
25
+ DEPENDENCIES
26
+ minitest (~> 5.11.3)
27
+ minitest-reporters (~> 1.4.2)
28
+ qna!
29
+ rake (~> 12.3.3)
30
+
31
+ BUNDLED WITH
32
+ 2.1.4
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Sharon Rosner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # QNA
2
+
3
+ ## Cross-library feature rich HTTP request / response API
4
+
5
+ QNA provides a uniform API for dealing with HTTP requests and responses.
6
+
7
+ ## Features
8
+
9
+ - Works with different web server APIs, notably Tipi, Digital Fabric, and Rack
10
+ - Transport-agnostic
11
+ - High-performance routing API inspired by Roda
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/clean"
5
+
6
+ # frozen_string_literal: true
7
+
8
+ task :default => [:test]
9
+ task :test do
10
+ exec 'ruby test/run.rb'
11
+ end
data/lib/qna.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QNA
4
+ end
5
+
6
+ require_relative 'qna/request.rb'
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './request_info'
4
+ require_relative './routing'
5
+ require_relative './response'
6
+
7
+ module QNA
8
+ # HTTP request
9
+ class Request
10
+ include RequestInfoMethods
11
+ include RoutingMethods
12
+ include ResponseMethods
13
+
14
+ extend RequestInfoClassMethods
15
+
16
+ attr_reader :headers, :adapter
17
+ attr_accessor :__next__
18
+
19
+ def initialize(headers, adapter)
20
+ @headers = headers
21
+ @adapter = adapter
22
+ end
23
+
24
+ def buffer_body_chunk(chunk)
25
+ @buffered_body_chunks ||= []
26
+ @buffered_body_chunks << chunk
27
+ end
28
+
29
+ def next_chunk
30
+ if @buffered_body_chunks
31
+ chunk = @buffered_body_chunks.shift
32
+ @buffered_body_chunks = nil if @buffered_body_chunks.empty?
33
+ return chunk
34
+ end
35
+
36
+ @message_complete ? nil : @adapter.get_body_chunk
37
+ end
38
+
39
+ def each_chunk
40
+ if @buffered_body_chunks
41
+ while (chunk = @buffered_body_chunks.shift)
42
+ yield chunk
43
+ end
44
+ @buffered_body_chunks = nil
45
+ end
46
+ while !@message_complete && (chunk = @adapter.get_body_chunk)
47
+ yield chunk
48
+ end
49
+ end
50
+
51
+ def complete!(keep_alive = nil)
52
+ @message_complete = true
53
+ @keep_alive = keep_alive
54
+ end
55
+
56
+ def complete?
57
+ @message_complete
58
+ end
59
+
60
+ def consume
61
+ @adapter.consume_request
62
+ end
63
+
64
+ def keep_alive?
65
+ @keep_alive
66
+ end
67
+
68
+ def read
69
+ buf = @buffered_body_chunks ? @buffered_body_chunks.join : nil
70
+ while (chunk = @adapter.get_body_chunk)
71
+ (buf ||= +'') << chunk
72
+ end
73
+ @buffered_body_chunks = nil
74
+ buf
75
+ end
76
+ alias_method :body, :read
77
+
78
+ def respond(body, headers = {})
79
+ @adapter.respond(body, headers)
80
+ @headers_sent = true
81
+ end
82
+
83
+ def send_headers(headers = {}, empty_response = false)
84
+ return if @headers_sent
85
+
86
+ @headers_sent = true
87
+ @adapter.send_headers(headers, empty_response: empty_response)
88
+ end
89
+
90
+ def send_chunk(body, done: false)
91
+ send_headers({}) unless @headers_sent
92
+
93
+ @adapter.send_chunk(body, done: done)
94
+ end
95
+ alias_method :<<, :send_chunk
96
+
97
+ def finish
98
+ send_headers({}) unless @headers_sent
99
+
100
+ @adapter.finish
101
+ end
102
+
103
+ def headers_sent?
104
+ @headers_sent
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'escape_utils'
5
+
6
+ module QNA
7
+ module RequestInfoMethods
8
+ def host
9
+ @headers['host']
10
+ end
11
+
12
+ def connection
13
+ @headers['connection']
14
+ end
15
+
16
+ def upgrade_protocol
17
+ connection == 'upgrade' && @headers['upgrade']&.downcase
18
+ end
19
+
20
+ def protocol
21
+ @protocol ||= @adapter.protocol
22
+ end
23
+
24
+ def method
25
+ @method ||= @headers[':method'].downcase
26
+ end
27
+
28
+ def scheme
29
+ @scheme ||= @headers[':scheme']
30
+ end
31
+
32
+ def uri
33
+ @uri ||= URI.parse(@headers[':path'] || '')
34
+ end
35
+
36
+ def path
37
+ @path ||= uri.path
38
+ end
39
+
40
+ def query_string
41
+ @query_string ||= uri.query
42
+ end
43
+
44
+ def query
45
+ return @query if @query
46
+
47
+ @query = (q = uri.query) ? split_query_string(q) : {}
48
+ end
49
+
50
+ def split_query_string(query)
51
+ query.split('&').each_with_object({}) do |kv, h|
52
+ k, v = kv.split('=')
53
+ h[k.to_sym] = URI.decode_www_form_component(v)
54
+ end
55
+ end
56
+
57
+ def request_id
58
+ @headers['x-request-id']
59
+ end
60
+
61
+ def forwarded_for
62
+ @headers['x-forwarded-for']
63
+ end
64
+ end
65
+
66
+ module RequestInfoClassMethods
67
+ def parse_form_data(body, headers)
68
+ case (content_type = headers['content-type'])
69
+ when /multipart\/form\-data; boundary=([^\s]+)/
70
+ boundary = "--#{Regexp.last_match(1)}"
71
+ parse_multipart_form_data(body, boundary)
72
+ when 'application/x-www-form-urlencoded'
73
+ parse_urlencoded_form_data(body)
74
+ else
75
+ raise "Unsupported form data content type: #{content_type}"
76
+ end
77
+ end
78
+
79
+ def parse_multipart_form_data(body, boundary)
80
+ parts = body.split(boundary)
81
+ parts.each_with_object({}) do |p, h|
82
+ next if p.empty? || p == "--\r\n"
83
+
84
+ # remove post-boundary \r\n
85
+ p.slice!(0, 2)
86
+ parse_multipart_form_data_part(p, h)
87
+ end
88
+ end
89
+
90
+ def parse_multipart_form_data_part(part, hash)
91
+ body, headers = parse_multipart_form_data_part_headers(part)
92
+ disposition = headers['content-disposition'] || ''
93
+
94
+ name = (disposition =~ /name="([^"]+)"/) ? Regexp.last_match(1) : nil
95
+ filename = (disposition =~ /filename="([^"]+)"/) ? Regexp.last_match(1) : nil
96
+
97
+ if filename
98
+ hash[name] = { filename: filename, content_type: headers['content-type'], data: body }
99
+ else
100
+ hash[name] = body
101
+ end
102
+ end
103
+
104
+ def parse_multipart_form_data_part_headers(part)
105
+ headers = {}
106
+ while true
107
+ idx = part.index("\r\n")
108
+ break unless idx
109
+
110
+ header = part[0, idx]
111
+ part.slice!(0, idx + 2)
112
+ break if header.empty?
113
+
114
+ next unless header =~ /^([^\:]+)\:\s?(.+)$/
115
+
116
+ headers[Regexp.last_match(1).downcase] = Regexp.last_match(2)
117
+ end
118
+ # remove trailing \r\n
119
+ part.slice!(part.size - 2, 2)
120
+ [part, headers]
121
+ end
122
+
123
+ PARAMETER_RE = /^(.+)=(.*)$/.freeze
124
+ MAX_PARAMETER_NAME_SIZE = 256
125
+ MAX_PARAMETER_VALUE_SIZE = 2**20 # 1MB
126
+
127
+ def parse_urlencoded_form_data(body)
128
+ body.force_encoding(UTF_8) unless body.encoding == Encoding::UTF_8
129
+ body.split('&').each_with_object({}) do |i, m|
130
+ raise 'Invalid parameter format' unless i =~ PARAMETER_RE
131
+
132
+ k = Regexp.last_match(1)
133
+ raise 'Invalid parameter size' if k.size > MAX_PARAMETER_NAME_SIZE
134
+
135
+ v = Regexp.last_match(2)
136
+ raise 'Invalid parameter size' if v.size > MAX_PARAMETER_VALUE_SIZE
137
+
138
+ m[EscapeUtils.unescape_uri(k)] = EscapeUtils.unescape_uri(v)
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QNA
4
+ module ResponseMethods
5
+ def redirect(url)
6
+ respond(nil, ':status' => 302, 'Location' => url)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QNA
4
+ module RoutingMethods
5
+ def route(&block)
6
+ res = catch(:stop) { yield self }
7
+ return if res == :found
8
+
9
+ respond(nil, ':status' => 404)
10
+ end
11
+
12
+ def route_found(&block)
13
+ catch(:stop, &block)
14
+ throw :stop, :found
15
+ end
16
+
17
+ @@regexp_cache = {}
18
+
19
+ def routing_path
20
+ @__routing_path__
21
+ end
22
+
23
+ def on(route = nil, &block)
24
+ @__routing_path__ ||= path
25
+
26
+ if route
27
+ regexp = (@@regexp_cache[route] ||= /^\/#{route}(\/.*)?/)
28
+ return unless @__routing_path__ =~ regexp
29
+
30
+ @__routing_path__ = Regexp.last_match(1) || '/'
31
+ end
32
+
33
+ route_found(&block)
34
+ end
35
+
36
+ def is(route = '/', &block)
37
+ return unless @__routing_path__ == route
38
+
39
+ route_found(&block)
40
+ end
41
+
42
+ def on_root(&block)
43
+ return unless @__routing_path__ == '/'
44
+
45
+ route_found(&block)
46
+ end
47
+
48
+ def on_get(route = nil, &block)
49
+ return unless method == 'get'
50
+
51
+ on(route, &block)
52
+ end
53
+
54
+ def on_post(route = nil, &block)
55
+ return unless method == 'post'
56
+
57
+ on(route, &block)
58
+ end
59
+
60
+ def on_options(route = nil, &block)
61
+ return unless method == 'options'
62
+
63
+ on(route, &block)
64
+ end
65
+
66
+ def on_upgrade(protocol, &block)
67
+ return unless upgrade_protocol == protocol
68
+
69
+ route_found(&block)
70
+ end
71
+
72
+ def on_query_param(key)
73
+ value = query[key]
74
+ return unless value
75
+
76
+ route_found { yield value }
77
+ end
78
+
79
+ def on_accept(accept, &block)
80
+ if accept.is_a?(Regexp)
81
+ return unless headers['accept'] =~ accept
82
+ else
83
+ return unless headers['accept'] == accept
84
+ end
85
+
86
+ route_found(&block)
87
+ end
88
+
89
+ def stop_routing
90
+ yield if block_given?
91
+ throw :stop, :found
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QNA
4
+ VERSION = '0.1'
5
+ end
data/qna.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ require_relative './lib/qna/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'qna'
5
+ s.version = QNA::VERSION
6
+ s.licenses = ['MIT']
7
+ s.summary = 'QNA - cross library HTTP request / response API'
8
+ s.author = 'Sharon Rosner'
9
+ s.email = 'sharon@noteflakes.com'
10
+ s.files = `git ls-files`.split
11
+ s.homepage = 'http://github.com/digital-fabric/qna'
12
+ s.metadata = {
13
+ "source_code_uri" => "https://github.com/digital-fabric/qna"
14
+ }
15
+ s.rdoc_options = ["--title", "QNA", "--main", "README.md"]
16
+ s.extra_rdoc_files = ["README.md"]
17
+ s.require_paths = ["lib"]
18
+ s.required_ruby_version = '>= 2.6'
19
+
20
+ s.add_runtime_dependency 'escape_utils', '~>1.2.1'
21
+
22
+ s.add_development_dependency 'rake', '~>12.3.3'
23
+ s.add_development_dependency 'minitest', '~>5.11.3'
24
+ s.add_development_dependency 'minitest-reporters', '~>1.4.2'
25
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'qna'
5
+
6
+ require 'fileutils'
7
+
8
+ require_relative './coverage' if ENV['COVERAGE']
9
+
10
+ require 'minitest/autorun'
11
+ require 'minitest/reporters'
12
+
13
+ module QNA
14
+ def self.mock(headers)
15
+ Request.new(headers, nil)
16
+ end
17
+ end
18
+
19
+ Minitest::Reporters.use! [
20
+ Minitest::Reporters::SpecReporter.new
21
+ ]
data/test/run.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob("#{__dir__}/test_*.rb").each do |path|
4
+ require(path)
5
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class RequestInfoTest < MiniTest::Test
6
+ def test_uri
7
+ r = QNA.mock(':path' => '/test/path')
8
+ assert_equal '/test/path', r.path
9
+ assert_equal({}, r.query)
10
+
11
+ r = QNA.mock(':path' => '/test/path?a=1&b=2&c=3%2f4')
12
+ assert_equal '/test/path', r.path
13
+ assert_equal({ a: '1', b: '2', c: '3/4' }, r.query)
14
+ end
15
+
16
+ def test_host
17
+ r = QNA.mock(':path' => '/')
18
+ assert_nil r.host
19
+
20
+ r = QNA.mock('host' => 'my.example.com')
21
+ assert_equal 'my.example.com', r.host
22
+ end
23
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: qna
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Sharon Rosner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-02-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: escape_utils
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.2.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.2.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 12.3.3
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 12.3.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 5.11.3
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 5.11.3
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest-reporters
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.4.2
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.4.2
69
+ description:
70
+ email: sharon@noteflakes.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files:
74
+ - README.md
75
+ files:
76
+ - ".github/test.yml"
77
+ - ".gitignore"
78
+ - CHANGELOG.md
79
+ - Gemfile
80
+ - Gemfile.lock
81
+ - LICENSE
82
+ - README.md
83
+ - Rakefile
84
+ - lib/qna.rb
85
+ - lib/qna/request.rb
86
+ - lib/qna/request_info.rb
87
+ - lib/qna/response.rb
88
+ - lib/qna/routing.rb
89
+ - lib/qna/version.rb
90
+ - qna.gemspec
91
+ - test/helper.rb
92
+ - test/run.rb
93
+ - test/test_request.rb
94
+ homepage: http://github.com/digital-fabric/qna
95
+ licenses:
96
+ - MIT
97
+ metadata:
98
+ source_code_uri: https://github.com/digital-fabric/qna
99
+ post_install_message:
100
+ rdoc_options:
101
+ - "--title"
102
+ - QNA
103
+ - "--main"
104
+ - README.md
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '2.6'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 3.1.4
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: QNA - cross library HTTP request / response API
122
+ test_files: []