requestkit 0.5.0
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/README.md +123 -0
- data/exe/requestkit +5 -0
- data/lib/requestkit/cli.rb +78 -0
- data/lib/requestkit/config.rb +38 -0
- data/lib/requestkit/errors.rb +7 -0
- data/lib/requestkit/server/render.rb +46 -0
- data/lib/requestkit/server/request.rb +51 -0
- data/lib/requestkit/server.rb +114 -0
- data/lib/requestkit/storage.rb +55 -0
- data/lib/requestkit/templates/index.css +2 -0
- data/lib/requestkit/templates/index.html.erb +146 -0
- data/lib/requestkit/version.rb +3 -0
- data/lib/requestkit.rb +11 -0
- metadata +85 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1b3ef7e7cd97233efe4a2d92df61192c3a622588c8925fb2b1639f95ba396ec6
|
|
4
|
+
data.tar.gz: 8479411497b672380fb53e0a9f2d81f0960d2b6f0cc8984332ef97f2f9f47811
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9a27d2dcffca1175edfc1402e0f9c253812fb1d2965a56d3adaf1abbcf7f7d5a36540434ce0a68dd72c5fe15353b7b54f589f1f6437e4585707ecdeafca48089
|
|
7
|
+
data.tar.gz: bf364c6b6ffb7a062ed3f73c3e00bf2113724eb3da705a02cc674d3296d5e884ee345030f51fcb084fb89da47da4d67d894d7be72387550dc4f673d9b684b900
|
data/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Requestkit
|
|
2
|
+
|
|
3
|
+
Local HTTP request toolkit for development. Test Stripe webhooks, GitHub hooks or any HTTP endpoint locally. Your data stays private, works offline and runs fast without network latency.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
**Sponsored By [Rails Designer](https://railsdesigner.com/)**
|
|
10
|
+
|
|
11
|
+
<a href="https://railsdesigner.com/" target="_blank">
|
|
12
|
+
<picture>
|
|
13
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Rails-Designer/requestkit/HEAD/.github/logo-dark.svg">
|
|
14
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Rails-Designer/requestkit/HEAD/.github/logo-light.svg">
|
|
15
|
+
<img alt="Rails Designer" src="https://raw.githubusercontent.com/Rails-Designer/requestkit/HEAD/.github/logo-light.svg" width="240" style="max-width: 100%;">
|
|
16
|
+
</picture>
|
|
17
|
+
</a>
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
If you have a Ruby environment available, you can install Requestkit globally:
|
|
23
|
+
```bash
|
|
24
|
+
gem install requestkit
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
Start the server:
|
|
31
|
+
```bash
|
|
32
|
+
requestkit
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This starts Requestkit on `http://localhost:4000`. Send any HTTP request to test:
|
|
36
|
+
```bash
|
|
37
|
+
curl -X POST http://localhost:4000/stripe/webhook \
|
|
38
|
+
-H "Content-Type: application/json" \
|
|
39
|
+
-d '{"event": "payment.succeeded", "amount": 2500}'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Open `http://localhost:4000` in your browser to see all captured requests with headers and body.
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
### Custom Port
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
requestkit --port 8080
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
### Persistent Storage
|
|
53
|
+
|
|
54
|
+
By default, requests are stored in memory and cleared when you stop the server. Use file storage to persist across restarts:
|
|
55
|
+
```bash
|
|
56
|
+
requestkit --storage file
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Requests are saved to `~/.config/requestkit/requestkit.db`.
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
### Custom Database Path
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
requestkit --storage file --database-path ./my-project.db
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
|
|
71
|
+
Create a configuration file to set defaults:
|
|
72
|
+
|
|
73
|
+
**User-wide settings** (`~/.config/requestkit/config.yml`):
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
port: 5000
|
|
77
|
+
storage: file
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Project-specific settings** (`./.requestkit.yml`):
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
storage: memory
|
|
84
|
+
default_namespace: my-rails-app
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Configuration precedence: CLI flags > project config > user config > defaults
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
### Available Options
|
|
91
|
+
|
|
92
|
+
| Option | Description | Default |
|
|
93
|
+
|--------|-------------|---------|
|
|
94
|
+
| `port` | Server port | `4000` |
|
|
95
|
+
| `storage` | Storage type: `memory` or `file` | `memory` |
|
|
96
|
+
| `database_path` | Database file location | `~/.config/requestkit/requestkit.db` |
|
|
97
|
+
| `default_namespace` | Default namespace for root requests | `default` |
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
## Namespaces
|
|
101
|
+
|
|
102
|
+
Requestkit automatically organizes requests by namespace using the first path segment:
|
|
103
|
+
```bash
|
|
104
|
+
# Namespace: stripe
|
|
105
|
+
curl http://localhost:4000/stripe/payment-webhook
|
|
106
|
+
|
|
107
|
+
# Namespace: github
|
|
108
|
+
curl http://localhost:4000/github/push-event
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Filter by namespace in the web UI. Requests to `/` use the `default_namespace` from your config.
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
## Help
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
requestkit help
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
Perron is released under the [MIT License](https://opensource.org/licenses/MIT).
|
data/exe/requestkit
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Requestkit
|
|
4
|
+
class CLI
|
|
5
|
+
class << self
|
|
6
|
+
def start(arguments)
|
|
7
|
+
command = arguments[0]
|
|
8
|
+
|
|
9
|
+
case command
|
|
10
|
+
when "server", nil
|
|
11
|
+
start_server with: arguments
|
|
12
|
+
when "help", "--help", "-h"
|
|
13
|
+
output_help
|
|
14
|
+
else
|
|
15
|
+
puts "Unknown command: #{command}"
|
|
16
|
+
puts "Run `requestkit help` for usage information"
|
|
17
|
+
|
|
18
|
+
exit 1
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def start_server(with:)
|
|
25
|
+
config = Config.new
|
|
26
|
+
merge! config, with: with
|
|
27
|
+
|
|
28
|
+
trap("INT") do
|
|
29
|
+
puts "\n📦 Packing up the toolkit…"
|
|
30
|
+
|
|
31
|
+
exit 0
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Server.new(config).start
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def output_help
|
|
38
|
+
puts <<~HELP
|
|
39
|
+
Requestkit - Local HTTP request toolkit
|
|
40
|
+
|
|
41
|
+
Usage:
|
|
42
|
+
requestkit [server] [options]
|
|
43
|
+
requestkit help
|
|
44
|
+
|
|
45
|
+
Options:
|
|
46
|
+
-p, --port PORT Port to run on (default: 4000)
|
|
47
|
+
-s, --storage TYPE Storage type: memory or file (default: memory)
|
|
48
|
+
-d, --database-path PATH Database file path (default: ~/.config/requestkit/requestkit.db)
|
|
49
|
+
-h, --help Show this help
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
requestkit
|
|
53
|
+
requestkit server --port 8080
|
|
54
|
+
requestkit server --storage file
|
|
55
|
+
requestkit server --storage file --database-path ./my-project.db
|
|
56
|
+
HELP
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def merge!(config, with:)
|
|
60
|
+
arguments = with
|
|
61
|
+
|
|
62
|
+
config.port = extract(arguments, "--port", "-p").to_i if has?(arguments, "--port", "-p")
|
|
63
|
+
config.storage = extract(arguments, "--storage", "-s") if has?(arguments, "--storage", "-s")
|
|
64
|
+
config.database_path = extract(arguments, "--database-path", "-d") if has?(arguments, "--database-path", "-d")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def has?(arguments, *flags)
|
|
68
|
+
flags.any? { arguments.include? it }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def extract(arguments, *flags)
|
|
72
|
+
index = flags.map { arguments.index it }.compact.first
|
|
73
|
+
|
|
74
|
+
arguments[index + 1]
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Requestkit
|
|
6
|
+
class Config
|
|
7
|
+
attr_accessor :port, :storage, :database_path, :default_namespace
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@port = 4000
|
|
11
|
+
@storage = "memory"
|
|
12
|
+
@database_path = File.join(Dir.home, ".config", "requestkit", "requestkit.db")
|
|
13
|
+
@default_namespace = "default"
|
|
14
|
+
|
|
15
|
+
load!
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def load!
|
|
21
|
+
merge! user_config if File.exist? user_config
|
|
22
|
+
merge! local_config if File.exist? local_config
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def merge!(path)
|
|
26
|
+
data = YAML.load_file(path)
|
|
27
|
+
|
|
28
|
+
@port = data["port"] if data["port"]
|
|
29
|
+
@storage = data["storage"] if data["storage"]
|
|
30
|
+
@database_path = data["database_path"] if data["database_path"]
|
|
31
|
+
@default_namespace = data["default_namespace"] if data["default_namespace"]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def user_config = File.join(Dir.home, ".config", "requestkit", "config.yml")
|
|
35
|
+
|
|
36
|
+
def local_config = File.join(Dir.pwd, ".requestkit.yml")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
|
|
5
|
+
module Requestkit
|
|
6
|
+
class Server
|
|
7
|
+
class Render
|
|
8
|
+
class << self
|
|
9
|
+
def html(request, database, config)
|
|
10
|
+
params = query_params(from: request.path)
|
|
11
|
+
selected_namespace = params["namespace"] unless params["namespace"] && params["namespace"].empty?
|
|
12
|
+
|
|
13
|
+
context = {
|
|
14
|
+
requests: selected_namespace ? database.by_namespace(selected_namespace) : database.all,
|
|
15
|
+
namespaces: database.namespaces,
|
|
16
|
+
selected_namespace: selected_namespace
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
template = ERB.new(File.read(File.join(__dir__, "..", "templates", "index.html.erb")))
|
|
20
|
+
html = template.result_with_hash(context)
|
|
21
|
+
|
|
22
|
+
Protocol::HTTP::Response[200, {"content-type" => "text/html"}, [html]]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def css
|
|
26
|
+
css_path = File.join(__dir__, "..", "templates", "index.css")
|
|
27
|
+
css_content = File.read(css_path)
|
|
28
|
+
|
|
29
|
+
Protocol::HTTP::Response[200, {"content-type" => "text/css"}, [css_content]]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def query_params(from:)
|
|
35
|
+
return {} unless from.include?("?")
|
|
36
|
+
|
|
37
|
+
query_string = from.split("?", 2).last
|
|
38
|
+
query_string.split("&").each_with_object({}) do |pair, hash|
|
|
39
|
+
key, value = pair.split("=", 2)
|
|
40
|
+
hash[key] = value
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async/http/client"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module Requestkit
|
|
7
|
+
class Server
|
|
8
|
+
class Request
|
|
9
|
+
class << self
|
|
10
|
+
def send(database:, namespace:)
|
|
11
|
+
request_data = {
|
|
12
|
+
headers: {"Content-Type" => "application/json", "User-Agent" => "Requestkit/#{Requestkit::VERSION}"},
|
|
13
|
+
body: '{"test": "data"}'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
endpoint = ssl_endpoint_for("https://httpbin.org/post")
|
|
17
|
+
client = Async::HTTP::Client.new(endpoint)
|
|
18
|
+
|
|
19
|
+
response = client.post("/post", request_data[:headers], request_data[:body])
|
|
20
|
+
response_data = {
|
|
21
|
+
headers: response.headers.to_h,
|
|
22
|
+
body: response.body&.read || ""
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
client.close
|
|
26
|
+
|
|
27
|
+
database.store(
|
|
28
|
+
namespace: namespace,
|
|
29
|
+
method: "POST",
|
|
30
|
+
path: "https://httpbin.org/post",
|
|
31
|
+
request: request_data.to_json,
|
|
32
|
+
response: response_data.to_json,
|
|
33
|
+
status: response.status,
|
|
34
|
+
direction: "outbound",
|
|
35
|
+
timestamp: Time.now.iso8601
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def ssl_endpoint_for(url)
|
|
42
|
+
endpoint = Async::HTTP::Endpoint.parse(url)
|
|
43
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
|
44
|
+
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
45
|
+
|
|
46
|
+
endpoint.with(ssl_context: ssl_context)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
require "async/http/endpoint"
|
|
5
|
+
require "async/http/server"
|
|
6
|
+
require "protocol/http/response"
|
|
7
|
+
|
|
8
|
+
require "requestkit/server/render"
|
|
9
|
+
require "requestkit/server/request"
|
|
10
|
+
|
|
11
|
+
Console.logger.level = :error
|
|
12
|
+
|
|
13
|
+
module Requestkit
|
|
14
|
+
class Server
|
|
15
|
+
def initialize(config)
|
|
16
|
+
@config = config
|
|
17
|
+
@port = config.port
|
|
18
|
+
@db = Storage.new(config)
|
|
19
|
+
@clients = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def start
|
|
23
|
+
puts "📦 Requestkit starting on http://localhost:#{@port}"
|
|
24
|
+
puts "Press Ctrl+C to stop"
|
|
25
|
+
|
|
26
|
+
endpoint = Async::HTTP::Endpoint.parse("http://localhost:#{@port}")
|
|
27
|
+
|
|
28
|
+
Async do
|
|
29
|
+
Async::HTTP::Server.new(method(:route), endpoint).run
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def route(request)
|
|
36
|
+
path = request.path.split("?").first
|
|
37
|
+
method = request.method
|
|
38
|
+
|
|
39
|
+
return Protocol::HTTP::Response[204, {}, []] if path == "/favicon.ico"
|
|
40
|
+
return stream! if path == "/events" && method == "GET"
|
|
41
|
+
|
|
42
|
+
case [path, method]
|
|
43
|
+
when ["/", "GET"]
|
|
44
|
+
Render.html(request, @db, @config)
|
|
45
|
+
when ["/send", "POST"]
|
|
46
|
+
Request.send(database: @db, namespace: "test")
|
|
47
|
+
|
|
48
|
+
notify!
|
|
49
|
+
|
|
50
|
+
Protocol::HTTP::Response[200, {"content-type" => "text/plain"}, ["Request sent!"]]
|
|
51
|
+
when ["/index.css", "GET"]
|
|
52
|
+
Render.css
|
|
53
|
+
when ["/clear", "POST"]
|
|
54
|
+
@db.clear
|
|
55
|
+
|
|
56
|
+
Protocol::HTTP::Response[303, {"location" => "/"}, []]
|
|
57
|
+
else
|
|
58
|
+
capture(request)
|
|
59
|
+
|
|
60
|
+
Protocol::HTTP::Response[200, {"content-type" => "text/plain"}, ["OK"]]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def capture(request)
|
|
65
|
+
request_data = {
|
|
66
|
+
headers: request.headers.to_h,
|
|
67
|
+
body: request.body&.read || ""
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
namespace = extract_namespace(from: request.path)
|
|
71
|
+
|
|
72
|
+
@db.store(
|
|
73
|
+
namespace: namespace,
|
|
74
|
+
method: request.method,
|
|
75
|
+
path: request.path,
|
|
76
|
+
request: request_data.to_json,
|
|
77
|
+
direction: "inbound",
|
|
78
|
+
timestamp: Time.now.iso8601
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
notify!
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def stream!
|
|
85
|
+
body = Protocol::HTTP::Body::Writable.new
|
|
86
|
+
@clients << body
|
|
87
|
+
|
|
88
|
+
Protocol::HTTP::Response[
|
|
89
|
+
200,
|
|
90
|
+
{"content-type" => "text/event-stream", "cache-control" => "no-cache", "connection" => "keep-alive"},
|
|
91
|
+
body
|
|
92
|
+
]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def extract_namespace(from:)
|
|
96
|
+
path = from.split("?").first
|
|
97
|
+
segments = path.split("/").reject(&:empty?)
|
|
98
|
+
|
|
99
|
+
segments.first || @config.default_namespace
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def notify!
|
|
103
|
+
latest = @db.all.first
|
|
104
|
+
return unless latest
|
|
105
|
+
|
|
106
|
+
data = JSON.generate(latest)
|
|
107
|
+
@clients.each do |client|
|
|
108
|
+
client.write("data: #{data}\n\n")
|
|
109
|
+
rescue Errno::EPIPE, IOError
|
|
110
|
+
@clients.delete(client)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "sqlite3"
|
|
5
|
+
|
|
6
|
+
module Requestkit
|
|
7
|
+
class Storage
|
|
8
|
+
def initialize(config)
|
|
9
|
+
database_path = (config.storage == "file") ? config.database_path : ":memory:"
|
|
10
|
+
|
|
11
|
+
if config.storage == "file"
|
|
12
|
+
FileUtils.mkdir_p(File.dirname(config.database_path))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
@db = SQLite3::Database.new(database_path)
|
|
16
|
+
@db.results_as_hash = true
|
|
17
|
+
|
|
18
|
+
setup!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def store(namespace:, method:, path:, request:, timestamp:, direction: "inbound", response: nil, status: nil, parent_id: nil)
|
|
22
|
+
@db.execute(
|
|
23
|
+
"INSERT INTO requests (namespace, direction, method, path, request, response, status, parent_id, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
24
|
+
[namespace, direction, method, path, request, response, status, parent_id, timestamp]
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def all = @db.execute("SELECT * FROM requests ORDER BY id DESC")
|
|
29
|
+
|
|
30
|
+
def namespaces = @db.execute("SELECT DISTINCT namespace FROM requests ORDER BY namespace").map { |row| row["namespace"] }
|
|
31
|
+
|
|
32
|
+
def by_namespace(namespace) = @db.execute("SELECT * FROM requests WHERE namespace = ? ORDER BY id DESC", [namespace])
|
|
33
|
+
|
|
34
|
+
def clear = @db.execute("DELETE FROM requests")
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def setup!
|
|
39
|
+
@db.execute <<~SQL
|
|
40
|
+
CREATE TABLE IF NOT EXISTS requests (
|
|
41
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
42
|
+
namespace TEXT NOT NULL,
|
|
43
|
+
direction TEXT NOT NULL DEFAULT 'inbound',
|
|
44
|
+
method TEXT NOT NULL,
|
|
45
|
+
path TEXT NOT NULL,
|
|
46
|
+
request TEXT NOT NULL,
|
|
47
|
+
response TEXT,
|
|
48
|
+
status INTEGER,
|
|
49
|
+
parent_id INTEGER,
|
|
50
|
+
timestamp TEXT NOT NULL
|
|
51
|
+
)
|
|
52
|
+
SQL
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! tailwindcss v4.1.16 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-200:oklch(88.5% .062 18.334);--color-red-700:oklch(50.5% .213 27.518);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-800:oklch(27.8% .033 256.848);--color-white:#fff;--spacing:.25rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--radius-sm:.25rem;--radius-md:.375rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.mx-auto{margin-inline:auto}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.flex{display:flex}.hidden{display:none}.inline-flex{display:inline-flex}.w-full{width:100%}.max-w-5xl{max-width:var(--container-5xl)}.border-collapse{border-collapse:collapse}.cursor-pointer{cursor:pointer}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-end{justify-content:flex-end}.gap-x-1\.5{column-gap:calc(var(--spacing)*1.5)}.gap-x-2{column-gap:calc(var(--spacing)*2)}.overflow-x-auto{overflow-x:auto}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-t-md{border-top-left-radius:var(--radius-md);border-top-right-radius:var(--radius-md)}.rounded-b-md{border-bottom-right-radius:var(--radius-md);border-bottom-left-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-gray-200{border-color:var(--color-gray-200)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200\/60{background-color:#e5e7eb99}@supports (color:color-mix(in lab, red, red)){.bg-gray-200\/60{background-color:color-mix(in oklab,var(--color-gray-200)60%,transparent)}}.bg-white{background-color:var(--color-white)}.p-3{padding:calc(var(--spacing)*3)}.px-2{padding-inline:calc(var(--spacing)*2)}.py-1{padding-block:calc(var(--spacing)*1)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-800{color:var(--color-gray-800)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}:is(.\*\:px-4>*){padding-inline:calc(var(--spacing)*4)}:is(.\*\:py-1>*){padding-block:calc(var(--spacing)*1)}:is(.\*\:py-2>*){padding-block:calc(var(--spacing)*2)}:is(.\*\:text-left>*){text-align:left}:is(.\*\:text-sm>*){font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}:is(.\*\:text-xs>*){font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}:is(.\*\:font-semibold>*){--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}:is(.\*\:text-gray-500>*){color:var(--color-gray-500)}@media (hover:hover){.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.hover\:bg-red-200:hover{background-color:var(--color-red-200)}.hover\:text-red-700:hover{color:var(--color-red-700)}}.has-\[\+tr\[open\]\]\:bg-gray-50:has(+tr[open]){background-color:var(--color-gray-50)}.\[\&_svg\]\:size-3\.5 svg{width:calc(var(--spacing)*3.5);height:calc(var(--spacing)*3.5)}.\[\[open\]\]\:table-row[open]{display:table-row}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Requestkit</title>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<link href="/index.css" rel="stylesheet" />
|
|
7
|
+
<script defer src="https://cdn.jsdelivr.net/npm/attractivejs@latest"></script>
|
|
8
|
+
</head>
|
|
9
|
+
|
|
10
|
+
<body class="bg-gray-100">
|
|
11
|
+
<div class="max-w-5xl mx-auto px-2 py-4 bg-white rounded-b-md">
|
|
12
|
+
<div class="flex items-center justify-between">
|
|
13
|
+
<% if namespaces.any? %>
|
|
14
|
+
<form method="get" action="/" id="namespace" class="inline-flex">
|
|
15
|
+
<select name="namespace" data-action="form#submit" data-target="#namespace" class="inline-flex items-center gap-x-1.5 px-2 py-1 text-xs font-normal text-gray-600 bg-gray-100 rounded-md transition hover:bg-gray-200">
|
|
16
|
+
<option value="">All namespaces</option>
|
|
17
|
+
|
|
18
|
+
<% namespaces.each do |namespace| %>
|
|
19
|
+
<option value="<%= namespace %>" <%= 'selected' if selected_namespace == namespace %>><%= namespace %></option>
|
|
20
|
+
<% end %>
|
|
21
|
+
</select>
|
|
22
|
+
</form>
|
|
23
|
+
<% end %>
|
|
24
|
+
|
|
25
|
+
<div class="flex items-center justify-end gap-x-2 [&_svg]:size-3.5">
|
|
26
|
+
<a href="/" class="inline-flex items-center gap-x-1.5 px-2 py-1 text-xs font-normal text-gray-600 bg-gray-100 rounded-md transition hover:bg-gray-200">
|
|
27
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16H211.4L184.81,71.64l-.25-.24a80,80,0,1,0-1.67,114.78,8,8,0,0,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60L224,85.8V56a8,8,0,1,1,16,0Z"/></svg>
|
|
28
|
+
|
|
29
|
+
Refresh
|
|
30
|
+
</a>
|
|
31
|
+
|
|
32
|
+
<% unless requests.empty? %>
|
|
33
|
+
<form method="post" action="/clear">
|
|
34
|
+
<button type="submit" data-action="confirm" data-confirm-message="Clear all requests?" class="inline-flex items-center gap-x-1.5 px-2 py-1 text-xs font-normal text-gray-600 bg-gray-100 rounded-md cursor-pointer transition hover:bg-red-200 hover:text-red-700">
|
|
35
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"/></svg>
|
|
36
|
+
|
|
37
|
+
Clear
|
|
38
|
+
</button>
|
|
39
|
+
</form>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<% if requests.empty? %>
|
|
45
|
+
<p class="mt-3 text-sm text-gray-500">
|
|
46
|
+
No webhooks received yet. Send a request to any endpoint.
|
|
47
|
+
</p>
|
|
48
|
+
<% else %>
|
|
49
|
+
<table class="w-full mt-4 border-collapse">
|
|
50
|
+
<thead>
|
|
51
|
+
<tr class="rounded-t-md *:px-4 *:py-1 *:text-left *:text-xs *:font-semibold *:text-gray-500">
|
|
52
|
+
<th>#</th>
|
|
53
|
+
|
|
54
|
+
<th>Method</th>
|
|
55
|
+
|
|
56
|
+
<th>Path</th>
|
|
57
|
+
|
|
58
|
+
<th>Time</th>
|
|
59
|
+
</tr>
|
|
60
|
+
</thead>
|
|
61
|
+
|
|
62
|
+
<tbody>
|
|
63
|
+
<% requests.each do |request| %>
|
|
64
|
+
<% request_data = JSON.parse(request["request"]) %>
|
|
65
|
+
<tr data-action="toggleAttribute#open" data-target="#details-<%= request["id"] %>" class="*:px-4 *:py-2 *:text-sm has-[+tr[open]]:bg-gray-50 hover:bg-gray-50">
|
|
66
|
+
<td><%= request["id"] %></td>
|
|
67
|
+
|
|
68
|
+
<td class="text-xs font-medium"><%= request["method"] %></td>
|
|
69
|
+
|
|
70
|
+
<td>
|
|
71
|
+
<code class="text-xs bg-gray-200/60 px-2 py-1 rounded-sm">
|
|
72
|
+
<%= request["path"] %>
|
|
73
|
+
</code>
|
|
74
|
+
</td>
|
|
75
|
+
|
|
76
|
+
<td class="text-xs"><%= request["timestamp"] %></td>
|
|
77
|
+
</tr>
|
|
78
|
+
|
|
79
|
+
<tr id="details-<%= request["id"] %>" class="hidden w-full bg-gray-50 [[open]]:table-row">
|
|
80
|
+
<td colspan="5" class="px-2 py-3">
|
|
81
|
+
<section>
|
|
82
|
+
<h4 class="font-medium text-sm text-gray-800">
|
|
83
|
+
Headers
|
|
84
|
+
</h4>
|
|
85
|
+
<pre class="text-xs bg-white p-3 rounded-sm border border-gray-200 overflow-x-auto"><%= JSON.pretty_generate(request_data["headers"]) %></pre>
|
|
86
|
+
|
|
87
|
+
<h4 class="mt-4 font-medium text-sm text-gray-800">
|
|
88
|
+
Body
|
|
89
|
+
</h4>
|
|
90
|
+
<pre class="text-xs bg-white p-3 rounded-sm border border-gray-200 overflow-x-auto"><%= request_data["body"].empty? ? "(empty)" : request_data["body"] %></pre>
|
|
91
|
+
</section>
|
|
92
|
+
</td>
|
|
93
|
+
</tr>
|
|
94
|
+
<% end %>
|
|
95
|
+
</tbody>
|
|
96
|
+
</table>
|
|
97
|
+
<% end %>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<script>
|
|
101
|
+
new EventSource("/events").onmessage = (eventData) => {
|
|
102
|
+
const request = JSON.parse(eventData.data);
|
|
103
|
+
const tableBody = document.querySelector("tbody");
|
|
104
|
+
|
|
105
|
+
if (!tableBody) {
|
|
106
|
+
location.reload();
|
|
107
|
+
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const template = document.createElement("template");
|
|
112
|
+
template.innerHTML = `
|
|
113
|
+
<tr data-action="toggleAttribute#open" data-target="#details-${request.id}" class="*:px-4 *:py-2 *:text-sm has-[+tr[open]]:bg-gray-50 hover:bg-gray-50">
|
|
114
|
+
<td>${request.id}</td>
|
|
115
|
+
|
|
116
|
+
<td class="text-xs font-medium">${request.method}</td>
|
|
117
|
+
|
|
118
|
+
<td>
|
|
119
|
+
<code class="text-xs bg-gray-200/60 px-2 py-1 rounded-sm">
|
|
120
|
+
${request.path}
|
|
121
|
+
</code>
|
|
122
|
+
</td>
|
|
123
|
+
|
|
124
|
+
<td class="text-xs">${request.timestamp}</td>
|
|
125
|
+
</tr>
|
|
126
|
+
|
|
127
|
+
<tr id="details-${request.id}" class="hidden w-full bg-gray-50 [[open]]:table-row">
|
|
128
|
+
<td colspan="5" class="px-2 py-3">
|
|
129
|
+
<section>
|
|
130
|
+
<h4 class="font-medium text-sm text-gray-800">Headers</h4>
|
|
131
|
+
|
|
132
|
+
<pre class="text-xs bg-white p-3 rounded-sm border border-gray-200 overflow-x-auto">${JSON.stringify(requestData.headers, null, 2)}</pre>
|
|
133
|
+
|
|
134
|
+
<h4 class="mt-4 font-medium text-sm text-gray-800">Body</h4>
|
|
135
|
+
|
|
136
|
+
<pre class="text-xs bg-white p-3 rounded-sm border border-gray-200 overflow-x-auto">${requestData.body || "(empty)"}</pre>
|
|
137
|
+
</section>
|
|
138
|
+
</td>
|
|
139
|
+
</tr>
|
|
140
|
+
`.trim();
|
|
141
|
+
|
|
142
|
+
tableBody.insertBefore(template.content, tableBody.firstChild);
|
|
143
|
+
};
|
|
144
|
+
</script>
|
|
145
|
+
</body>
|
|
146
|
+
</html>
|
data/lib/requestkit.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "requestkit/version"
|
|
4
|
+
require_relative "requestkit/config"
|
|
5
|
+
require_relative "requestkit/cli"
|
|
6
|
+
require_relative "requestkit/errors"
|
|
7
|
+
require_relative "requestkit/server"
|
|
8
|
+
require_relative "requestkit/storage"
|
|
9
|
+
|
|
10
|
+
module Requestkit
|
|
11
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: requestkit
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.5.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Rails Designer Developers
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: async-http
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.90'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.90'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: sqlite3
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.7'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.7'
|
|
40
|
+
description: Capture webhooks and send HTTP requests locally. Think webhook.site meets
|
|
41
|
+
Postman, but living on your machine where it belongs.
|
|
42
|
+
email:
|
|
43
|
+
- devs@railsdesigner.com
|
|
44
|
+
executables:
|
|
45
|
+
- requestkit
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- README.md
|
|
50
|
+
- exe/requestkit
|
|
51
|
+
- lib/requestkit.rb
|
|
52
|
+
- lib/requestkit/cli.rb
|
|
53
|
+
- lib/requestkit/config.rb
|
|
54
|
+
- lib/requestkit/errors.rb
|
|
55
|
+
- lib/requestkit/server.rb
|
|
56
|
+
- lib/requestkit/server/render.rb
|
|
57
|
+
- lib/requestkit/server/request.rb
|
|
58
|
+
- lib/requestkit/storage.rb
|
|
59
|
+
- lib/requestkit/templates/index.css
|
|
60
|
+
- lib/requestkit/templates/index.html.erb
|
|
61
|
+
- lib/requestkit/version.rb
|
|
62
|
+
homepage: https://requestkit.railsdesigner.com/
|
|
63
|
+
licenses:
|
|
64
|
+
- MIT
|
|
65
|
+
metadata:
|
|
66
|
+
homepage_uri: https://requestkit.railsdesigner.com/
|
|
67
|
+
source_code_uri: https://github.com/Rails-Designer/requestkit
|
|
68
|
+
rdoc_options: []
|
|
69
|
+
require_paths:
|
|
70
|
+
- lib
|
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: 3.4.0
|
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
requirements: []
|
|
82
|
+
rubygems_version: 3.6.9
|
|
83
|
+
specification_version: 4
|
|
84
|
+
summary: Local HTTP request toolkit
|
|
85
|
+
test_files: []
|