landline 0.9.2
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/HACKING.md +30 -0
- data/LAYOUT.md +59 -0
- data/LICENSE.md +660 -0
- data/README.md +159 -0
- data/lib/landline/dsl/constructors_path.rb +107 -0
- data/lib/landline/dsl/constructors_probe.rb +28 -0
- data/lib/landline/dsl/methods_common.rb +28 -0
- data/lib/landline/dsl/methods_path.rb +75 -0
- data/lib/landline/dsl/methods_probe.rb +129 -0
- data/lib/landline/dsl/methods_template.rb +16 -0
- data/lib/landline/node.rb +87 -0
- data/lib/landline/path.rb +157 -0
- data/lib/landline/pattern_matching/glob.rb +168 -0
- data/lib/landline/pattern_matching/rematch.rb +49 -0
- data/lib/landline/pattern_matching/util.rb +15 -0
- data/lib/landline/pattern_matching.rb +75 -0
- data/lib/landline/probe/handler.rb +56 -0
- data/lib/landline/probe/http_method.rb +74 -0
- data/lib/landline/probe/serve_handler.rb +39 -0
- data/lib/landline/probe.rb +62 -0
- data/lib/landline/request.rb +135 -0
- data/lib/landline/response.rb +140 -0
- data/lib/landline/server.rb +49 -0
- data/lib/landline/template/erb.rb +27 -0
- data/lib/landline/template/erubi.rb +36 -0
- data/lib/landline/template.rb +95 -0
- data/lib/landline/util/cookie.rb +150 -0
- data/lib/landline/util/errors.rb +11 -0
- data/lib/landline/util/html.rb +119 -0
- data/lib/landline/util/lookup.rb +37 -0
- data/lib/landline/util/mime.rb +1276 -0
- data/lib/landline/util/multipart.rb +175 -0
- data/lib/landline/util/parsesorting.rb +37 -0
- data/lib/landline/util/parseutils.rb +111 -0
- data/lib/landline/util/query.rb +66 -0
- data/lib/landline.rb +20 -0
- metadata +85 -0
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require 'stringio'
|
5
|
+
require 'tempfile'
|
6
|
+
require_relative 'parseutils'
|
7
|
+
require_relative 'parsesorting'
|
8
|
+
require_relative 'html'
|
9
|
+
|
10
|
+
module Landline
|
11
|
+
module Util
|
12
|
+
# Valid element of form data with headers
|
13
|
+
# @!attribute headers [Hash] headers recevied from form data
|
14
|
+
# @!attribute name [String] name of the form part
|
15
|
+
# @!attribute data [String,nil] Data received in the field through form data
|
16
|
+
# @!attribute filename [String,nil] Original name of the sent file
|
17
|
+
# @!attribute filetype [String,nil] MIME-type of the file
|
18
|
+
# @!attribute tempfile [File,nil] Temporary file for storing sent file data.
|
19
|
+
FormPart = Struct.new(:data, :name, :filename,
|
20
|
+
:filetype, :tempfile, :headers) do
|
21
|
+
# Is this form part a file or plain data?
|
22
|
+
# @return [Boolean]
|
23
|
+
def file?
|
24
|
+
!tempfile.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
# Decode charset parameter
|
28
|
+
def decode(data)
|
29
|
+
data = Landline::Util.unescape_html(data)
|
30
|
+
return data unless self.headers['charset']
|
31
|
+
|
32
|
+
data.force_encoding(self.headers['charset']).encode("UTF-8")
|
33
|
+
end
|
34
|
+
|
35
|
+
# If FormPart is not a file, simplify to string.
|
36
|
+
# @return [FormPart, String]
|
37
|
+
def simplify
|
38
|
+
file? ? self : decode(self.data)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# A very naive implementation of a Multipart form parser.
|
43
|
+
class MultipartParser
|
44
|
+
include Landline::Util::ParserSorting
|
45
|
+
def initialize(io, boundary)
|
46
|
+
@input = io.is_a?(String) ? StringIO.new(io) : io
|
47
|
+
@boundary = boundary
|
48
|
+
@state = :idle
|
49
|
+
@data = []
|
50
|
+
end
|
51
|
+
|
52
|
+
# lord forgive me for what i'm about to do
|
53
|
+
# TODO: replace the god method with a state machine object
|
54
|
+
# rubocop:disable Metrics/*
|
55
|
+
|
56
|
+
# Parse multipart formdata
|
57
|
+
# @return [Array<FormPart, FormFile>]
|
58
|
+
def parse
|
59
|
+
return @data unless @data.empty?
|
60
|
+
|
61
|
+
while (line = @input.gets)
|
62
|
+
case @state
|
63
|
+
when :idle # waiting for valid boundary
|
64
|
+
if line == "--#{@boundary}\r\n"
|
65
|
+
# transition to :headers on valid boundary
|
66
|
+
@state = :headers
|
67
|
+
@data.append(FormPart.new(*([nil] * 5), {}))
|
68
|
+
end
|
69
|
+
when :headers # after valid boundary - checking for headers
|
70
|
+
if line == "\r\n"
|
71
|
+
# prepare form field and transition to :data or :file
|
72
|
+
@state = file?(@data[-1].headers) ? :file : :data
|
73
|
+
if @state == :data
|
74
|
+
setup_data_meta(@data[-1])
|
75
|
+
else
|
76
|
+
setup_file_meta(@data[-1])
|
77
|
+
end
|
78
|
+
next
|
79
|
+
end
|
80
|
+
push_header(line, @data[-1].headers)
|
81
|
+
when :data, :file # after headers - processing form data
|
82
|
+
if @data[-1].headers.empty?
|
83
|
+
# transition to :idle on empty headers
|
84
|
+
@state = :idle
|
85
|
+
next
|
86
|
+
end
|
87
|
+
if ["--#{@boundary}\r\n", "--#{@boundary}--\r\n"].include? line
|
88
|
+
# finalize and transition to either :headers or :idle
|
89
|
+
if @state == :file
|
90
|
+
@data[-1].tempfile.truncate(@data[-1].tempfile.size - 2)
|
91
|
+
@data[-1].tempfile.close
|
92
|
+
else
|
93
|
+
@data[-1].data.delete_suffix! "\r\n"
|
94
|
+
end
|
95
|
+
@state = line == "--#{@boundary}\r\n" ? :headers : :idle
|
96
|
+
@data.append(FormPart.new(*([nil] * 5), {}))
|
97
|
+
next
|
98
|
+
end
|
99
|
+
if @state == :data
|
100
|
+
@data[-1].data ||= ""
|
101
|
+
@data[-1].data << line
|
102
|
+
else
|
103
|
+
@data[-1].tempfile << line
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
@state = :idle
|
108
|
+
@data.pop
|
109
|
+
@data.freeze
|
110
|
+
end
|
111
|
+
# rubocop:enable Metrics/*
|
112
|
+
|
113
|
+
# Return a hash of current form.
|
114
|
+
# (equivalent to Query.parse but for multipart/form-data)
|
115
|
+
# @return [Hash]
|
116
|
+
def to_h
|
117
|
+
flatten(sort(gen_hash(parse)))
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def gen_hash(array)
|
123
|
+
hash = {}
|
124
|
+
array.each do |formpart|
|
125
|
+
key = formpart.name.to_s
|
126
|
+
if key.match?(/.*\[\d*\]\Z/)
|
127
|
+
new_key, index = key.match(/(.*)\[(\d*)\]\Z/).to_a[1..]
|
128
|
+
hash[new_key] = [] unless hash[new_key]
|
129
|
+
hash[new_key].append([index, formpart.simplify])
|
130
|
+
else
|
131
|
+
hash[key] = formpart.simplify
|
132
|
+
end
|
133
|
+
end
|
134
|
+
hash
|
135
|
+
end
|
136
|
+
|
137
|
+
# Setup file metadata
|
138
|
+
# @part part [FormPart]
|
139
|
+
def setup_file_meta(part)
|
140
|
+
part.name = part.headers.dig("content-disposition", 1, "name")
|
141
|
+
part.filename = part.headers.dig("content-disposition", 1, "filename")
|
142
|
+
part.filetype = part.headers["content-type"]
|
143
|
+
part.tempfile = Tempfile.new
|
144
|
+
end
|
145
|
+
|
146
|
+
# Setup plain metadata
|
147
|
+
# @part part [FormPart]
|
148
|
+
def setup_data_meta(part)
|
149
|
+
part.name = part.headers.dig("content-disposition", 1, "name")
|
150
|
+
end
|
151
|
+
|
152
|
+
# Analyze headers to check if current data part is a file.
|
153
|
+
# @param headers_hash [Hash]
|
154
|
+
# @return [Boolean]
|
155
|
+
def file?(headers_hash)
|
156
|
+
if headers_hash.dig("content-disposition", 1, "filename") and
|
157
|
+
headers_hash['content-type']
|
158
|
+
return true
|
159
|
+
end
|
160
|
+
|
161
|
+
false
|
162
|
+
end
|
163
|
+
|
164
|
+
# Parse a header and append it to headers_hash
|
165
|
+
# @param line [String]
|
166
|
+
# @param headers_hash [Hash]
|
167
|
+
def push_header(line, headers_hash)
|
168
|
+
return unless line.match(/^[\w!#$%&'*+-.^_`|~]+:.*\r\n$/)
|
169
|
+
|
170
|
+
k, v = line.match(/^([\w!#$%&'*+-.^_`|~]+):(.*)\r\n$/).to_a[1..]
|
171
|
+
headers_hash[k.downcase] = Landline::Util::ParserCommon.parse_value(v)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Landline
|
4
|
+
module Util
|
5
|
+
# Internal library for generating form hashes
|
6
|
+
module ParserSorting
|
7
|
+
private
|
8
|
+
|
9
|
+
# Sort key-value pair arrays
|
10
|
+
def sort(hash)
|
11
|
+
hash.filter { |_, v| v.is_a? Array }.each do |_, value|
|
12
|
+
value.sort_by! { |array| array[0].to_i }
|
13
|
+
end
|
14
|
+
hash
|
15
|
+
end
|
16
|
+
|
17
|
+
# Flatten key-value pair arrays
|
18
|
+
def flatten(hash)
|
19
|
+
hash.transform_values do |value|
|
20
|
+
if value.is_a? Array
|
21
|
+
new_array = []
|
22
|
+
value.each do |k, v|
|
23
|
+
if k.match?(/\d+/) and k.to_i < new_array.size
|
24
|
+
new_array[k.to_i] = v
|
25
|
+
else
|
26
|
+
new_array.append(v)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
new_array
|
30
|
+
else
|
31
|
+
value
|
32
|
+
end
|
33
|
+
end.to_h
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require_relative 'errors'
|
5
|
+
|
6
|
+
module Landline
|
7
|
+
module Util
|
8
|
+
# (not exactly precise) Regular expressions for some RFC definitions
|
9
|
+
module HeaderRegexp
|
10
|
+
# Matches the RFC2616 definiton of token
|
11
|
+
TOKEN = /[!-~&&[^()<>@,;:\\"\/\[\]?={}\t]]+/.freeze
|
12
|
+
# Matches the RFC2616 definition of quoted-string
|
13
|
+
QUOTED = /"[\x0-\x7E&&[^\x1-\x8\xb-\x1f]]*(?<!\\)"/.freeze
|
14
|
+
# Matches any CHAR except CTLs
|
15
|
+
PRINTCHAR = /[\x2-\x7E]/.freeze
|
16
|
+
# Matches 1 or more CHARs excluding CTLs
|
17
|
+
PRINTABLE = /#{PRINTCHAR}+/o.freeze
|
18
|
+
# Matches the RFC6265 definition of a cookie-octet
|
19
|
+
COOKIE_OCTET = /[\x21-\x7E&&[^",;\\]]*/.freeze
|
20
|
+
COOKIE_VALUE = /(?:#{QUOTED}|#{COOKIE_OCTET})/o.freeze
|
21
|
+
COOKIE_NAME = TOKEN
|
22
|
+
# Matches the RFC6265 definition of cookie-pair.
|
23
|
+
# Captures name (1) and value (2).
|
24
|
+
COOKIE_PAIR = /\A(#{COOKIE_NAME})=(#{COOKIE_VALUE})\Z/o.freeze
|
25
|
+
# Matches a very abstract definition of a quoted header paramter.
|
26
|
+
# Captures name (1) and value (2).
|
27
|
+
PARAM_QUOTED = /\A(#{TOKEN})=?(#{QUOTED}|#{PRINTCHAR}*)\Z/o.freeze
|
28
|
+
# Matches a very abstract definition of a header parameter.
|
29
|
+
# Captures name (1) and value (2).
|
30
|
+
PARAM = /\A(#{TOKEN})=?(#{PRINTCHAR}*)\Z/o.freeze
|
31
|
+
# Specifically matches cookie parameters
|
32
|
+
COOKIE_PARAM = /\A(#{TOKEN})=?(#{QUOTED}|#{COOKIE_OCTET})\Z/o.freeze
|
33
|
+
end
|
34
|
+
|
35
|
+
# Module for all things related to parsing HTTP and related syntax.
|
36
|
+
module ParserCommon
|
37
|
+
# #strftime parameter to return a correct RFC 1123 date.
|
38
|
+
RFC1123_DATE = "%a, %d %b %Y %H:%M:%S GMT"
|
39
|
+
|
40
|
+
# rubocop:disable Metrics/MethodLength
|
41
|
+
|
42
|
+
# Parse parametrized header values.
|
43
|
+
# This method will try the best attempt at decoding parameters.
|
44
|
+
# However, it does no decoding on the first argument.
|
45
|
+
# @param input [String]
|
46
|
+
# @param sep [String, Regexp]
|
47
|
+
# @param unquote [Boolean] interpret params as possibly quoted
|
48
|
+
# @param regexp [Regexp,nil] override param matching regexp
|
49
|
+
# @return [Array(String, Hash)]
|
50
|
+
def self.parse_value(input, sep: ";", unquote: false, regexp: nil)
|
51
|
+
parts = input.split(sep).map { |x| URI.decode_uri_component(x).strip }
|
52
|
+
base = parts.shift
|
53
|
+
opts = parts.map do |raw|
|
54
|
+
key, value = raw.match(if regexp
|
55
|
+
regexp
|
56
|
+
elsif unquote
|
57
|
+
HeaderRegexp::PARAM_QUOTED
|
58
|
+
else
|
59
|
+
HeaderRegexp::PARAM
|
60
|
+
end).to_a[1..]
|
61
|
+
value = case value
|
62
|
+
when "" then true
|
63
|
+
when /\A".*"\z/ then value.undump
|
64
|
+
else value
|
65
|
+
end
|
66
|
+
[key, value]
|
67
|
+
end.to_h
|
68
|
+
[base, opts]
|
69
|
+
end
|
70
|
+
|
71
|
+
# rubocop:enable Metrics/MethodLength
|
72
|
+
|
73
|
+
# Construct a parametrized header value.
|
74
|
+
# Does some input sanitization during construction
|
75
|
+
# @param input [String]
|
76
|
+
# @param opts [Hash]
|
77
|
+
# @return [String]
|
78
|
+
# @raise [Landline::ParsingError]
|
79
|
+
def self.make_value(input, opts, sep = ";")
|
80
|
+
output = input
|
81
|
+
unless input.match? HeaderRegexp::PRINTABLE
|
82
|
+
raise Landline::ParsingError, "input is not ascii printable"
|
83
|
+
end
|
84
|
+
|
85
|
+
opts.each do |key, value|
|
86
|
+
check_param(key, value)
|
87
|
+
newparam = if [String, Integer].include? value.class
|
88
|
+
"#{sep} #{key}=#{value}"
|
89
|
+
elsif value
|
90
|
+
"#{sep} #{key}"
|
91
|
+
end
|
92
|
+
output += newparam if newparam
|
93
|
+
end
|
94
|
+
output
|
95
|
+
end
|
96
|
+
|
97
|
+
# Checks if key and value are valid for constructing a parameter.
|
98
|
+
# Raises an error if that is not possible.
|
99
|
+
def self.check_param(key, value)
|
100
|
+
unless key.match? HeaderRegexp::TOKEN
|
101
|
+
raise Landline::ParsingError, "key #{key} is not an RFC2616 token"
|
102
|
+
end
|
103
|
+
|
104
|
+
if value.is_a?(String) and value.match? HeaderRegexp::PARAM_QUOTED
|
105
|
+
raise Landline::ParsingError, "quoted param value #{value} is invalid"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
private_class_method :check_param
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require_relative 'parsesorting'
|
5
|
+
|
6
|
+
module Landline
|
7
|
+
module Util
|
8
|
+
# Query string parser
|
9
|
+
class Query
|
10
|
+
include Landline::Util::ParserSorting
|
11
|
+
# @param query [String]
|
12
|
+
def initialize(query)
|
13
|
+
@query = query
|
14
|
+
end
|
15
|
+
|
16
|
+
# Shallow query parser (does not do PHP-like array keys)
|
17
|
+
# @return [Hash]
|
18
|
+
def parse_shallow
|
19
|
+
URI.decode_www_form(@query)
|
20
|
+
.sort_by { |array| array[0] }
|
21
|
+
.to_h
|
22
|
+
end
|
23
|
+
|
24
|
+
# Better(tm) query parser.
|
25
|
+
# Returns a hash with arrays.
|
26
|
+
# Key semantics:
|
27
|
+
#
|
28
|
+
# - `key=value` creates a key value pair
|
29
|
+
# - `key[]=value` appends `value` to an array named `key`
|
30
|
+
# - `key[index]=value` sets `value` at `index` of array named `key`
|
31
|
+
# @return [Hash]
|
32
|
+
def parse
|
33
|
+
construct_deep_hash(URI.decode_www_form(@query))
|
34
|
+
end
|
35
|
+
|
36
|
+
# Get key from query.
|
37
|
+
# @param key [String]
|
38
|
+
# @return [String,Array]
|
39
|
+
def [](key)
|
40
|
+
(@cache ||= parse)[key]
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Construct a hash with array support
|
46
|
+
def construct_deep_hash(array)
|
47
|
+
flatten(sort(group(array)))
|
48
|
+
end
|
49
|
+
|
50
|
+
# Assign values to keys in a new hash and group arrayable keys
|
51
|
+
def group(array)
|
52
|
+
hash = {}
|
53
|
+
array.each do |key, value|
|
54
|
+
if key.match?(/.*\[\d*\]\Z/)
|
55
|
+
new_key, index = key.match(/(.*)\[(\d*)\]\Z/).to_a[1..]
|
56
|
+
hash[new_key] = [] unless hash[new_key]
|
57
|
+
hash[new_key].append([index, value])
|
58
|
+
else
|
59
|
+
hash[key] = value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
hash
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/landline.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'landline/server'
|
4
|
+
require_relative 'landline/path'
|
5
|
+
require_relative 'landline/probe'
|
6
|
+
require_relative 'landline/request'
|
7
|
+
require_relative 'landline/response'
|
8
|
+
require_relative 'landline/template'
|
9
|
+
|
10
|
+
# Landline is a hideously simple ruby web framework
|
11
|
+
module Landline
|
12
|
+
# Landline version
|
13
|
+
VERSION = '0.9 "Moonsong" (beta/rewrite)'
|
14
|
+
|
15
|
+
# Landline branding and version
|
16
|
+
VLINE = "Landline/#{Landline::VERSION} (Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})\n"
|
17
|
+
|
18
|
+
# Landline copyright
|
19
|
+
COPYRIGHT = "Copyright 2023 Yessiest"
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: landline
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Yessiest
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-09-22 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |
|
14
|
+
Landline is a no-hard-dependencies HTTP routing DSL that was made entirely for fun.
|
15
|
+
It runs on any HTTP server that supports the Rack 3.0 protocol.
|
16
|
+
It is usable for many menial tasks, and as long as it continues to be fun, it will keep growing.
|
17
|
+
email: yessiest@text.512mb.org
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files:
|
21
|
+
- HACKING.md
|
22
|
+
- LAYOUT.md
|
23
|
+
- LICENSE.md
|
24
|
+
- README.md
|
25
|
+
files:
|
26
|
+
- HACKING.md
|
27
|
+
- LAYOUT.md
|
28
|
+
- LICENSE.md
|
29
|
+
- README.md
|
30
|
+
- lib/landline.rb
|
31
|
+
- lib/landline/dsl/constructors_path.rb
|
32
|
+
- lib/landline/dsl/constructors_probe.rb
|
33
|
+
- lib/landline/dsl/methods_common.rb
|
34
|
+
- lib/landline/dsl/methods_path.rb
|
35
|
+
- lib/landline/dsl/methods_probe.rb
|
36
|
+
- lib/landline/dsl/methods_template.rb
|
37
|
+
- lib/landline/node.rb
|
38
|
+
- lib/landline/path.rb
|
39
|
+
- lib/landline/pattern_matching.rb
|
40
|
+
- lib/landline/pattern_matching/glob.rb
|
41
|
+
- lib/landline/pattern_matching/rematch.rb
|
42
|
+
- lib/landline/pattern_matching/util.rb
|
43
|
+
- lib/landline/probe.rb
|
44
|
+
- lib/landline/probe/handler.rb
|
45
|
+
- lib/landline/probe/http_method.rb
|
46
|
+
- lib/landline/probe/serve_handler.rb
|
47
|
+
- lib/landline/request.rb
|
48
|
+
- lib/landline/response.rb
|
49
|
+
- lib/landline/server.rb
|
50
|
+
- lib/landline/template.rb
|
51
|
+
- lib/landline/template/erb.rb
|
52
|
+
- lib/landline/template/erubi.rb
|
53
|
+
- lib/landline/util/cookie.rb
|
54
|
+
- lib/landline/util/errors.rb
|
55
|
+
- lib/landline/util/html.rb
|
56
|
+
- lib/landline/util/lookup.rb
|
57
|
+
- lib/landline/util/mime.rb
|
58
|
+
- lib/landline/util/multipart.rb
|
59
|
+
- lib/landline/util/parsesorting.rb
|
60
|
+
- lib/landline/util/parseutils.rb
|
61
|
+
- lib/landline/util/query.rb
|
62
|
+
homepage: https://adastra7.net/git/Yessiest/landline
|
63
|
+
licenses:
|
64
|
+
- AGPL-3.0
|
65
|
+
metadata: {}
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options: []
|
68
|
+
require_paths:
|
69
|
+
- lib
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
requirements: []
|
81
|
+
rubygems_version: 3.3.25
|
82
|
+
signing_key:
|
83
|
+
specification_version: 4
|
84
|
+
summary: Elegant HTTP DSL
|
85
|
+
test_files: []
|