rails-http-lab 0.1.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/LICENSE.txt +15 -0
- data/README.md +60 -0
- data/app/assets/javascripts/rails_http_lab/application.js +1318 -0
- data/app/assets/stylesheets/rails_http_lab/application.css +336 -0
- data/app/controllers/rails_http_lab/application_controller.rb +47 -0
- data/app/controllers/rails_http_lab/collections_controller.rb +20 -0
- data/app/controllers/rails_http_lab/environments_controller.rb +33 -0
- data/app/controllers/rails_http_lab/folders_controller.rb +47 -0
- data/app/controllers/rails_http_lab/requests_controller.rb +99 -0
- data/app/controllers/rails_http_lab/runs_controller.rb +34 -0
- data/app/controllers/rails_http_lab/ui_controller.rb +7 -0
- data/app/views/layouts/rails_http_lab.html.erb +14 -0
- data/app/views/rails_http_lab/ui/index.html.erb +103 -0
- data/config/routes.rb +24 -0
- data/lib/generators/rails_http_lab/install/install_generator.rb +41 -0
- data/lib/generators/rails_http_lab/install/templates/initializer.rb.tt +20 -0
- data/lib/rails-http-lab.rb +1 -0
- data/lib/rails_http_lab/bruno/block.rb +44 -0
- data/lib/rails_http_lab/bruno/document.rb +36 -0
- data/lib/rails_http_lab/bruno/parser.rb +207 -0
- data/lib/rails_http_lab/bruno/serializer.rb +68 -0
- data/lib/rails_http_lab/bruno.rb +12 -0
- data/lib/rails_http_lab/configuration.rb +51 -0
- data/lib/rails_http_lab/engine.rb +25 -0
- data/lib/rails_http_lab/execution/response.rb +20 -0
- data/lib/rails_http_lab/execution/runner.rb +187 -0
- data/lib/rails_http_lab/execution/variable_resolver.rb +30 -0
- data/lib/rails_http_lab/execution.rb +3 -0
- data/lib/rails_http_lab/storage/filesystem.rb +123 -0
- data/lib/rails_http_lab/storage/tree.rb +120 -0
- data/lib/rails_http_lab/storage.rb +2 -0
- data/lib/rails_http_lab/version.rb +3 -0
- data/lib/rails_http_lab.rb +14 -0
- metadata +92 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "json"
|
|
4
|
+
require "rails_http_lab/execution/variable_resolver"
|
|
5
|
+
require "rails_http_lab/execution/response"
|
|
6
|
+
|
|
7
|
+
module RailsHttpLab
|
|
8
|
+
module Execution
|
|
9
|
+
# Executes a Bruno::Document as an HTTP request, returning Response.
|
|
10
|
+
#
|
|
11
|
+
# Pipeline:
|
|
12
|
+
# 1. Resolve {{vars}} in url, headers, query, body, auth fields.
|
|
13
|
+
# 2. Build query string from params:query (merge with URL query).
|
|
14
|
+
# 3. Build Net::HTTP::<Verb>.
|
|
15
|
+
# 4. Apply auth (bearer/basic/apikey).
|
|
16
|
+
# 5. Set body per body:<type>.
|
|
17
|
+
# 6. Dispatch with timeout, capture timings + size.
|
|
18
|
+
class Runner
|
|
19
|
+
VERBS = {
|
|
20
|
+
"get" => Net::HTTP::Get,
|
|
21
|
+
"post" => Net::HTTP::Post,
|
|
22
|
+
"put" => Net::HTTP::Put,
|
|
23
|
+
"patch" => Net::HTTP::Patch,
|
|
24
|
+
"delete" => Net::HTTP::Delete,
|
|
25
|
+
"head" => Net::HTTP::Head,
|
|
26
|
+
"options" => Net::HTTP::Options
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
def initialize(document, resolver: VariableResolver.new, timeout: nil, max_body: nil)
|
|
30
|
+
@doc = document
|
|
31
|
+
@resolver = resolver
|
|
32
|
+
@timeout = timeout || RailsHttpLab.config.executor_timeout
|
|
33
|
+
@max_body = max_body || RailsHttpLab.config.executor_max_body
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def run
|
|
37
|
+
verb_block = @doc.verb_block
|
|
38
|
+
raise Error, "no HTTP verb block in document" unless verb_block
|
|
39
|
+
|
|
40
|
+
sent_request = nil
|
|
41
|
+
|
|
42
|
+
url = @resolver.resolve(verb_block["url"].to_s)
|
|
43
|
+
body_kind = verb_block["body"].to_s
|
|
44
|
+
auth_kind = verb_block["auth"].to_s
|
|
45
|
+
|
|
46
|
+
uri = URI.parse(url)
|
|
47
|
+
merge_query!(uri)
|
|
48
|
+
|
|
49
|
+
klass = VERBS.fetch(verb_block.name)
|
|
50
|
+
request = klass.new(uri.request_uri)
|
|
51
|
+
|
|
52
|
+
apply_headers!(request)
|
|
53
|
+
apply_auth!(request, auth_kind)
|
|
54
|
+
apply_body!(request, body_kind)
|
|
55
|
+
|
|
56
|
+
sent_request = summarize_request(request, uri)
|
|
57
|
+
dispatch(uri, request, sent_request)
|
|
58
|
+
rescue URI::InvalidURIError => e
|
|
59
|
+
Response.new(error: "Invalid URL: #{e.message}", request: sent_request)
|
|
60
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
61
|
+
Response.new(error: "Timeout: #{e.message}", request: sent_request)
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
Response.new(error: "#{e.class}: #{e.message}", request: sent_request)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def dispatch(uri, request, sent_request)
|
|
69
|
+
started = monotonic_now
|
|
70
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
71
|
+
http.use_ssl = uri.scheme == "https"
|
|
72
|
+
http.open_timeout = @timeout
|
|
73
|
+
http.read_timeout = @timeout
|
|
74
|
+
|
|
75
|
+
resp = http.request(request)
|
|
76
|
+
body = resp.body.to_s
|
|
77
|
+
body = body[0, @max_body] + "\n[...truncated]" if body.bytesize > @max_body
|
|
78
|
+
|
|
79
|
+
Response.new(
|
|
80
|
+
status: resp.code.to_i,
|
|
81
|
+
headers: resp.each_header.to_h,
|
|
82
|
+
body: body,
|
|
83
|
+
duration_ms: ((monotonic_now - started) * 1000).round,
|
|
84
|
+
size_bytes: resp.body.to_s.bytesize,
|
|
85
|
+
request: sent_request
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Captures the resolved request after all headers/auth/body have been
|
|
90
|
+
# applied so the UI can show an equivalent cURL command.
|
|
91
|
+
def summarize_request(request, uri)
|
|
92
|
+
headers = {}
|
|
93
|
+
request.each_header { |k, v| headers[k] = v }
|
|
94
|
+
{
|
|
95
|
+
method: request.method.to_s.upcase,
|
|
96
|
+
url: uri.to_s,
|
|
97
|
+
headers: headers,
|
|
98
|
+
body: request.body
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def monotonic_now
|
|
103
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def merge_query!(uri)
|
|
107
|
+
params_block = @doc.block("params:query") || @doc.block("query")
|
|
108
|
+
return unless params_block&.kv?
|
|
109
|
+
|
|
110
|
+
existing = uri.query.to_s
|
|
111
|
+
extra = params_block.pairs.reject { |k, _| k.start_with?("~") }.map do |k, v|
|
|
112
|
+
"#{URI.encode_www_form_component(k)}=#{URI.encode_www_form_component(@resolver.resolve(v.to_s))}"
|
|
113
|
+
end
|
|
114
|
+
merged = [existing, extra.join("&")].reject(&:empty?).join("&")
|
|
115
|
+
uri.query = merged unless merged.empty?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def apply_headers!(request)
|
|
119
|
+
block = @doc.block("headers")
|
|
120
|
+
return unless block&.kv?
|
|
121
|
+
block.pairs.each do |k, v|
|
|
122
|
+
next if k.start_with?("~")
|
|
123
|
+
request[k] = @resolver.resolve(v.to_s)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def apply_auth!(request, kind)
|
|
128
|
+
case kind
|
|
129
|
+
when "bearer"
|
|
130
|
+
token = @resolver.resolve(@doc.block("auth:bearer")&.[]("token").to_s)
|
|
131
|
+
request["Authorization"] = "Bearer #{token}" unless token.empty?
|
|
132
|
+
when "basic"
|
|
133
|
+
user = @resolver.resolve(@doc.block("auth:basic")&.[]("username").to_s)
|
|
134
|
+
pass = @resolver.resolve(@doc.block("auth:basic")&.[]("password").to_s)
|
|
135
|
+
request.basic_auth(user, pass)
|
|
136
|
+
when "apikey"
|
|
137
|
+
block = @doc.block("auth:apikey")
|
|
138
|
+
return unless block
|
|
139
|
+
key = @resolver.resolve(block["key"].to_s)
|
|
140
|
+
val = @resolver.resolve(block["value"].to_s)
|
|
141
|
+
placement = block["placement"].to_s
|
|
142
|
+
if placement == "queryparams"
|
|
143
|
+
uri = request.uri || URI.parse("")
|
|
144
|
+
# placement in query is handled at URL level; skip here
|
|
145
|
+
else
|
|
146
|
+
request[key] = val unless key.empty?
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def apply_body!(request, kind)
|
|
152
|
+
case kind
|
|
153
|
+
when "json"
|
|
154
|
+
raw = @doc.block("body:json")&.raw.to_s
|
|
155
|
+
request.body = @resolver.resolve(raw.strip)
|
|
156
|
+
request["Content-Type"] ||= "application/json"
|
|
157
|
+
when "text"
|
|
158
|
+
request.body = @resolver.resolve(@doc.block("body:text")&.raw.to_s)
|
|
159
|
+
request["Content-Type"] ||= "text/plain"
|
|
160
|
+
when "xml"
|
|
161
|
+
request.body = @resolver.resolve(@doc.block("body:xml")&.raw.to_s)
|
|
162
|
+
request["Content-Type"] ||= "application/xml"
|
|
163
|
+
when "formUrlEncoded", "form-urlencoded"
|
|
164
|
+
block = @doc.block("body:form-urlencoded")
|
|
165
|
+
if block&.kv?
|
|
166
|
+
pairs = block.pairs.reject { |k, _| k.start_with?("~") }
|
|
167
|
+
request.set_form_data(pairs.to_h.transform_values { |v| @resolver.resolve(v.to_s) })
|
|
168
|
+
end
|
|
169
|
+
when "multipartForm", "multipart-form"
|
|
170
|
+
# v1: text fields only.
|
|
171
|
+
block = @doc.block("body:multipart-form")
|
|
172
|
+
if block&.kv?
|
|
173
|
+
pairs = block.pairs.reject { |k, _| k.start_with?("~") }
|
|
174
|
+
request.set_form(pairs.map { |k, v| [k, @resolver.resolve(v.to_s)] }, "multipart/form-data")
|
|
175
|
+
end
|
|
176
|
+
when "graphql"
|
|
177
|
+
query = @doc.block("body:graphql")&.raw.to_s
|
|
178
|
+
variables = @doc.block("body:graphql:vars")&.raw.to_s
|
|
179
|
+
payload = { "query" => query }
|
|
180
|
+
payload["variables"] = JSON.parse(variables) rescue payload["variables"] = variables if !variables.empty?
|
|
181
|
+
request.body = JSON.generate(payload)
|
|
182
|
+
request["Content-Type"] ||= "application/json"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module RailsHttpLab
|
|
2
|
+
module Execution
|
|
3
|
+
# Replaces {{var}} with the value from a flat hash of variables.
|
|
4
|
+
# Unknown vars are left as-is so the user can see what's missing.
|
|
5
|
+
class VariableResolver
|
|
6
|
+
VAR_RE = /\{\{\s*([\w.-]+)\s*\}\}/
|
|
7
|
+
|
|
8
|
+
def initialize(vars = {})
|
|
9
|
+
@vars = stringify_keys(vars)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def resolve(str)
|
|
13
|
+
return str unless str.is_a?(String)
|
|
14
|
+
str.gsub(VAR_RE) { |m| @vars.fetch(Regexp.last_match(1), m) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.from_environment_document(doc)
|
|
18
|
+
return new({}) unless doc
|
|
19
|
+
pairs = doc.block("vars")&.pairs || []
|
|
20
|
+
new(pairs.to_h)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def stringify_keys(h)
|
|
26
|
+
h.each_with_object({}) { |(k, v), acc| acc[k.to_s] = v.to_s }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module RailsHttpLab
|
|
6
|
+
module Storage
|
|
7
|
+
# Filesystem-backed CRUD for Bruno collections under config.storage_path.
|
|
8
|
+
#
|
|
9
|
+
# All paths are relative to the storage root. The class refuses to operate
|
|
10
|
+
# on absolute paths, paths containing "..", or paths that escape the root
|
|
11
|
+
# after resolution.
|
|
12
|
+
class Filesystem
|
|
13
|
+
def initialize(root: RailsHttpLab.config.resolved_storage_path)
|
|
14
|
+
@root = Pathname.new(root.to_s)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :root
|
|
18
|
+
|
|
19
|
+
def root_exists?
|
|
20
|
+
@root.directory?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ensure_root!
|
|
24
|
+
FileUtils.mkdir_p(@root) unless @root.directory?
|
|
25
|
+
unless (@root + "bruno.json").file?
|
|
26
|
+
File.write(@root + "bruno.json", default_bruno_json)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def read_bru(relative_path)
|
|
31
|
+
abs = safe_path(relative_path)
|
|
32
|
+
raise NotFoundError, "no such file: #{relative_path}" unless abs.file?
|
|
33
|
+
Bruno.parse(abs.read)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def write_bru(relative_path, document)
|
|
37
|
+
abs = safe_path(relative_path)
|
|
38
|
+
FileUtils.mkdir_p(abs.dirname)
|
|
39
|
+
File.write(abs, Bruno.dump(document))
|
|
40
|
+
document
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def delete(relative_path)
|
|
44
|
+
abs = safe_path(relative_path)
|
|
45
|
+
return false unless abs.exist?
|
|
46
|
+
if abs.directory?
|
|
47
|
+
FileUtils.rm_rf(abs)
|
|
48
|
+
else
|
|
49
|
+
abs.delete
|
|
50
|
+
end
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Moves a file or directory from one relative path to another. Both paths
|
|
55
|
+
# are validated through safe_path. Refuses if destination already exists.
|
|
56
|
+
def rename(from, to)
|
|
57
|
+
abs_from = safe_path(from)
|
|
58
|
+
raise NotFoundError, "no such path: #{from}" unless abs_from.exist?
|
|
59
|
+
abs_to = safe_path(to)
|
|
60
|
+
raise Error, "destination exists: #{to}" if abs_to.exist?
|
|
61
|
+
FileUtils.mkdir_p(abs_to.dirname)
|
|
62
|
+
FileUtils.mv(abs_from.to_s, abs_to.to_s)
|
|
63
|
+
abs_to
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def create_folder(relative_path, display_name: nil)
|
|
67
|
+
abs = safe_path(relative_path)
|
|
68
|
+
FileUtils.mkdir_p(abs)
|
|
69
|
+
folder_meta = abs + "folder.bru"
|
|
70
|
+
unless folder_meta.file?
|
|
71
|
+
name = display_name || abs.basename.to_s
|
|
72
|
+
doc = Bruno::Document.new(blocks: [
|
|
73
|
+
Bruno::Block.new(name: "meta", mode: :kv, pairs: [
|
|
74
|
+
["name", name],
|
|
75
|
+
["seq", next_seq(abs.dirname).to_s]
|
|
76
|
+
])
|
|
77
|
+
])
|
|
78
|
+
File.write(folder_meta, Bruno.dump(doc))
|
|
79
|
+
end
|
|
80
|
+
relative_path
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def read_bruno_json
|
|
84
|
+
path = @root + "bruno.json"
|
|
85
|
+
return nil unless path.file?
|
|
86
|
+
JSON.parse(path.read)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def safe_path(relative_path)
|
|
92
|
+
rel = relative_path.to_s
|
|
93
|
+
raise OutsideStorageError, "empty path" if rel.empty?
|
|
94
|
+
raise OutsideStorageError, "absolute path forbidden: #{rel}" if rel.start_with?("/")
|
|
95
|
+
raise OutsideStorageError, "traversal forbidden: #{rel}" if rel.split("/").include?("..")
|
|
96
|
+
abs = (@root + rel).cleanpath
|
|
97
|
+
unless abs.to_s == @root.to_s || abs.to_s.start_with?(@root.to_s + "/")
|
|
98
|
+
raise OutsideStorageError, "outside storage root: #{rel}"
|
|
99
|
+
end
|
|
100
|
+
abs
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def default_bruno_json
|
|
104
|
+
JSON.pretty_generate(
|
|
105
|
+
"version" => "1",
|
|
106
|
+
"name" => "My APIs",
|
|
107
|
+
"type" => "collection",
|
|
108
|
+
"ignore" => ["node_modules", ".git"]
|
|
109
|
+
) + "\n"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def next_seq(dir)
|
|
113
|
+
existing = Dir.glob(File.join(dir, "*.bru")).map do |f|
|
|
114
|
+
doc = Bruno.parse(File.read(f))
|
|
115
|
+
(doc.block("meta")&.[]("seq") || "0").to_i
|
|
116
|
+
rescue StandardError
|
|
117
|
+
0
|
|
118
|
+
end
|
|
119
|
+
existing.empty? ? 1 : existing.max + 1
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
|
|
3
|
+
module RailsHttpLab
|
|
4
|
+
module Storage
|
|
5
|
+
# Builds a hierarchical view of the storage root for the sidebar.
|
|
6
|
+
# Returns:
|
|
7
|
+
# {
|
|
8
|
+
# name: "My APIs",
|
|
9
|
+
# type: "collection",
|
|
10
|
+
# children: [
|
|
11
|
+
# { type: "folder", name: "...", path: "...", children: [...] },
|
|
12
|
+
# { type: "request", name: "...", path: "...", method: "GET", seq: 1 }
|
|
13
|
+
# ],
|
|
14
|
+
# environments: [{ name: "Local", path: "environments/Local.bru" }]
|
|
15
|
+
# }
|
|
16
|
+
class Tree
|
|
17
|
+
def initialize(root: RailsHttpLab.config.resolved_storage_path)
|
|
18
|
+
@root = Pathname.new(root.to_s)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def build
|
|
22
|
+
return empty_tree unless @root.directory?
|
|
23
|
+
|
|
24
|
+
collection = read_collection_manifest
|
|
25
|
+
children = list_children(@root, relative_prefix: "")
|
|
26
|
+
envs = list_environments
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
name: collection["name"] || @root.basename.to_s,
|
|
30
|
+
type: "collection",
|
|
31
|
+
children: children,
|
|
32
|
+
environments: envs
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def empty_tree
|
|
39
|
+
{ name: "(missing)", type: "collection", children: [], environments: [] }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def read_collection_manifest
|
|
43
|
+
f = @root + "bruno.json"
|
|
44
|
+
return {} unless f.file?
|
|
45
|
+
JSON.parse(f.read)
|
|
46
|
+
rescue JSON::ParserError
|
|
47
|
+
{}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def list_children(dir, relative_prefix:)
|
|
51
|
+
return [] unless dir.directory?
|
|
52
|
+
|
|
53
|
+
entries = dir.children.sort_by { |c| c.basename.to_s.downcase }
|
|
54
|
+
out = []
|
|
55
|
+
|
|
56
|
+
entries.each do |child|
|
|
57
|
+
rel = relative_prefix.empty? ? child.basename.to_s : "#{relative_prefix}/#{child.basename}"
|
|
58
|
+
|
|
59
|
+
if child.directory?
|
|
60
|
+
next if child.basename.to_s == "environments" && relative_prefix.empty?
|
|
61
|
+
folder_meta = read_folder_meta(child)
|
|
62
|
+
out << {
|
|
63
|
+
type: "folder",
|
|
64
|
+
name: folder_meta[:name] || child.basename.to_s,
|
|
65
|
+
path: rel,
|
|
66
|
+
seq: folder_meta[:seq],
|
|
67
|
+
children: list_children(child, relative_prefix: rel)
|
|
68
|
+
}
|
|
69
|
+
elsif child.file? && child.extname == ".bru" && child.basename.to_s != "folder.bru"
|
|
70
|
+
meta = read_request_meta(child)
|
|
71
|
+
out << {
|
|
72
|
+
type: "request",
|
|
73
|
+
name: meta[:name] || child.basename(".bru").to_s,
|
|
74
|
+
path: rel,
|
|
75
|
+
method: meta[:method],
|
|
76
|
+
seq: meta[:seq]
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Top-level entries (collections) are always alphabetical.
|
|
82
|
+
# Nested entries respect Bruno's seq ordering, falling back to name.
|
|
83
|
+
if relative_prefix.empty?
|
|
84
|
+
out.sort_by { |c| c[:name].to_s.downcase }
|
|
85
|
+
else
|
|
86
|
+
out.sort_by { |c| [c[:seq] || 9999, c[:name].to_s.downcase] }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def list_environments
|
|
91
|
+
envs_dir = @root + "environments"
|
|
92
|
+
return [] unless envs_dir.directory?
|
|
93
|
+
envs_dir.children.select { |c| c.file? && c.extname == ".bru" }.map do |f|
|
|
94
|
+
{ name: f.basename(".bru").to_s, path: "environments/#{f.basename}" }
|
|
95
|
+
end.sort_by { |e| e[:name].downcase }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def read_folder_meta(dir)
|
|
99
|
+
f = dir + "folder.bru"
|
|
100
|
+
return {} unless f.file?
|
|
101
|
+
doc = Bruno.parse(f.read)
|
|
102
|
+
meta = doc.block("meta")
|
|
103
|
+
return {} unless meta
|
|
104
|
+
{ name: meta["name"], seq: meta["seq"]&.to_i }
|
|
105
|
+
rescue StandardError
|
|
106
|
+
{}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def read_request_meta(file)
|
|
110
|
+
doc = Bruno.parse(file.read)
|
|
111
|
+
meta = doc.block("meta")
|
|
112
|
+
method = doc.http_method
|
|
113
|
+
return { method: method } unless meta
|
|
114
|
+
{ name: meta["name"], seq: meta["seq"]&.to_i, method: method }
|
|
115
|
+
rescue StandardError
|
|
116
|
+
{ method: nil }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require "rails_http_lab/version"
|
|
2
|
+
require "rails_http_lab/configuration"
|
|
3
|
+
|
|
4
|
+
module RailsHttpLab
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
class ParseError < Error; end
|
|
7
|
+
class NotFoundError < Error; end
|
|
8
|
+
class OutsideStorageError < Error; end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require "rails_http_lab/bruno"
|
|
12
|
+
require "rails_http_lab/storage"
|
|
13
|
+
require "rails_http_lab/execution"
|
|
14
|
+
require "rails_http_lab/engine" if defined?(Rails)
|
metadata
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rails-http-lab
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jackson Pires
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.0'
|
|
26
|
+
description: Mounts a Bruno-like UI inside your Rails app for ad-hoc HTTP requests,
|
|
27
|
+
persisting collections as .bru files that are interchangeable with Bruno.
|
|
28
|
+
email:
|
|
29
|
+
- jackson@linkana.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- LICENSE.txt
|
|
35
|
+
- README.md
|
|
36
|
+
- app/assets/javascripts/rails_http_lab/application.js
|
|
37
|
+
- app/assets/stylesheets/rails_http_lab/application.css
|
|
38
|
+
- app/controllers/rails_http_lab/application_controller.rb
|
|
39
|
+
- app/controllers/rails_http_lab/collections_controller.rb
|
|
40
|
+
- app/controllers/rails_http_lab/environments_controller.rb
|
|
41
|
+
- app/controllers/rails_http_lab/folders_controller.rb
|
|
42
|
+
- app/controllers/rails_http_lab/requests_controller.rb
|
|
43
|
+
- app/controllers/rails_http_lab/runs_controller.rb
|
|
44
|
+
- app/controllers/rails_http_lab/ui_controller.rb
|
|
45
|
+
- app/views/layouts/rails_http_lab.html.erb
|
|
46
|
+
- app/views/rails_http_lab/ui/index.html.erb
|
|
47
|
+
- config/routes.rb
|
|
48
|
+
- lib/generators/rails_http_lab/install/install_generator.rb
|
|
49
|
+
- lib/generators/rails_http_lab/install/templates/initializer.rb.tt
|
|
50
|
+
- lib/rails-http-lab.rb
|
|
51
|
+
- lib/rails_http_lab.rb
|
|
52
|
+
- lib/rails_http_lab/bruno.rb
|
|
53
|
+
- lib/rails_http_lab/bruno/block.rb
|
|
54
|
+
- lib/rails_http_lab/bruno/document.rb
|
|
55
|
+
- lib/rails_http_lab/bruno/parser.rb
|
|
56
|
+
- lib/rails_http_lab/bruno/serializer.rb
|
|
57
|
+
- lib/rails_http_lab/configuration.rb
|
|
58
|
+
- lib/rails_http_lab/engine.rb
|
|
59
|
+
- lib/rails_http_lab/execution.rb
|
|
60
|
+
- lib/rails_http_lab/execution/response.rb
|
|
61
|
+
- lib/rails_http_lab/execution/runner.rb
|
|
62
|
+
- lib/rails_http_lab/execution/variable_resolver.rb
|
|
63
|
+
- lib/rails_http_lab/storage.rb
|
|
64
|
+
- lib/rails_http_lab/storage/filesystem.rb
|
|
65
|
+
- lib/rails_http_lab/storage/tree.rb
|
|
66
|
+
- lib/rails_http_lab/version.rb
|
|
67
|
+
homepage: https://github.com/jacksonpires/rails-http-lab
|
|
68
|
+
licenses:
|
|
69
|
+
- MIT
|
|
70
|
+
metadata:
|
|
71
|
+
source_code_uri: https://github.com/jacksonpires/rails-http-lab
|
|
72
|
+
bug_tracker_uri: https://github.com/jacksonpires/rails-http-lab/issues
|
|
73
|
+
rubygems_mfa_required: 'true'
|
|
74
|
+
allowed_push_host: https://rubygems.org
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.1'
|
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
requirements: []
|
|
89
|
+
rubygems_version: 4.0.3
|
|
90
|
+
specification_version: 4
|
|
91
|
+
summary: In-app HTTP request lab for Rails, with Bruno-compatible storage.
|
|
92
|
+
test_files: []
|