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 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
+ ![Screenshot of the Requestkit UI, listing POST requests to Stripe, GitHub, Twilio and Shopify](https://raw.githubusercontent.com/Rails-Designer/requestkit/HEAD/.github/screenshot.jpg)
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,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "requestkit"
4
+
5
+ Requestkit::CLI.start(ARGV)
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Requestkit
4
+ module Errors
5
+ class Error < StandardError; end
6
+ end
7
+ 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>
@@ -0,0 +1,3 @@
1
+ module Requestkit
2
+ VERSION = "0.5.0"
3
+ end
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: []