qna 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 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: []