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,103 @@
|
|
|
1
|
+
<div class="rhl-app" id="rhl-app">
|
|
2
|
+
<header class="rhl-topbar">
|
|
3
|
+
<div class="rhl-topbar__brand">Rails HTTP Lab</div>
|
|
4
|
+
<div class="rhl-topbar__env">
|
|
5
|
+
<button class="rhl-btn rhl-btn--ghost" id="rhl-clear-cache" title="Clear locally cached drafts and responses (does not touch saved .bru files)">Clear cache</button>
|
|
6
|
+
<label for="rhl-env-select">Environment</label>
|
|
7
|
+
<select id="rhl-env-select">
|
|
8
|
+
<option value="">(none)</option>
|
|
9
|
+
</select>
|
|
10
|
+
<button class="rhl-btn rhl-btn--ghost" id="rhl-env-edit" title="Edit environments">Edit</button>
|
|
11
|
+
</div>
|
|
12
|
+
</header>
|
|
13
|
+
|
|
14
|
+
<main class="rhl-main">
|
|
15
|
+
<aside class="rhl-sidebar">
|
|
16
|
+
<div class="rhl-sidebar__header">
|
|
17
|
+
<span>Collections</span>
|
|
18
|
+
<button class="rhl-iconbtn" id="rhl-new-collection" title="New collection">+</button>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="rhl-sidebar__tree" id="rhl-tree"><div class="rhl-tree-empty">Loading…</div></div>
|
|
21
|
+
</aside>
|
|
22
|
+
|
|
23
|
+
<section class="rhl-pane">
|
|
24
|
+
<div class="rhl-pane__empty" id="rhl-empty">
|
|
25
|
+
<p>Select a request from the sidebar. Use <strong>+</strong> to create a new collection, then add folders and requests via the <strong>⋯</strong> menu on each folder. Rename and delete are available from the <strong>⋯</strong> menu on folders and requests.</p>
|
|
26
|
+
<p class="rhl-tip">
|
|
27
|
+
<strong>Tip — environment variables:</strong>
|
|
28
|
+
write <code>{{var_name}}</code> in any field (URL, headers, query, body, auth) to interpolate from the active environment.
|
|
29
|
+
For example: <code>{{baseUrl}}/v0/users</code>, <code>Authorization: Bearer {{token}}</code>.
|
|
30
|
+
Pick an environment in the top-right and manage them with the <strong>Edit</strong> button.
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="rhl-pane__editor" id="rhl-editor" hidden>
|
|
34
|
+
<div class="rhl-request-bar">
|
|
35
|
+
<select class="rhl-verb" id="rhl-verb">
|
|
36
|
+
<option>GET</option><option>POST</option><option>PUT</option>
|
|
37
|
+
<option>PATCH</option><option>DELETE</option><option>HEAD</option><option>OPTIONS</option>
|
|
38
|
+
</select>
|
|
39
|
+
<input type="text" class="rhl-url" id="rhl-url" placeholder="https://…">
|
|
40
|
+
<button class="rhl-btn rhl-btn--send" id="rhl-send">Send</button>
|
|
41
|
+
<button class="rhl-btn" id="rhl-save">Save</button>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<nav class="rhl-tabs" id="rhl-tabs">
|
|
45
|
+
<button class="rhl-tab is-active" data-tab="params">Params</button>
|
|
46
|
+
<button class="rhl-tab" data-tab="headers">Headers</button>
|
|
47
|
+
<button class="rhl-tab" data-tab="body">Body</button>
|
|
48
|
+
<button class="rhl-tab" data-tab="auth">Auth</button>
|
|
49
|
+
<button class="rhl-tab" data-tab="vars">Vars</button>
|
|
50
|
+
<button class="rhl-tab" data-tab="script">Script</button>
|
|
51
|
+
<button class="rhl-tab" data-tab="tests">Tests</button>
|
|
52
|
+
<button class="rhl-tab" data-tab="docs">Docs</button>
|
|
53
|
+
</nav>
|
|
54
|
+
|
|
55
|
+
<div class="rhl-split">
|
|
56
|
+
<div class="rhl-tab-panel" id="rhl-panel"></div>
|
|
57
|
+
<div class="rhl-splitter" id="rhl-splitter" title="Drag to resize"></div>
|
|
58
|
+
<div class="rhl-response" id="rhl-response">
|
|
59
|
+
<div class="rhl-response__header">
|
|
60
|
+
<nav class="rhl-response__tabs" id="rhl-response-tabs">
|
|
61
|
+
<button class="rhl-response-tab is-active" data-rtab="body">Response</button>
|
|
62
|
+
<button class="rhl-response-tab" data-rtab="pretty">Pretty</button>
|
|
63
|
+
<button class="rhl-response-tab" data-rtab="headers">Headers<span class="rhl-response-tab__count" id="rhl-response-headers-count" hidden></span></button>
|
|
64
|
+
<button class="rhl-response-tab" data-rtab="curl">cURL</button>
|
|
65
|
+
</nav>
|
|
66
|
+
<div class="rhl-response__meta">
|
|
67
|
+
<span id="rhl-response-status">Ready</span>
|
|
68
|
+
<span id="rhl-response-time"></span>
|
|
69
|
+
<span id="rhl-response-size"></span>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
<pre class="rhl-response__body" id="rhl-response-body">Click Send to execute the request.</pre>
|
|
73
|
+
<pre class="rhl-response__body" id="rhl-response-pretty" hidden></pre>
|
|
74
|
+
<div class="rhl-response__headers" id="rhl-response-headers" hidden></div>
|
|
75
|
+
<pre class="rhl-response__body" id="rhl-response-curl" hidden></pre>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</section>
|
|
80
|
+
</main>
|
|
81
|
+
|
|
82
|
+
<div class="rhl-modal" id="rhl-env-modal" hidden>
|
|
83
|
+
<div class="rhl-modal__backdrop" data-close="1"></div>
|
|
84
|
+
<div class="rhl-modal__dialog">
|
|
85
|
+
<div class="rhl-modal__header">
|
|
86
|
+
<h2>Environments</h2>
|
|
87
|
+
<button class="rhl-iconbtn" id="rhl-env-modal-close" title="Close">×</button>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="rhl-modal__body">
|
|
90
|
+
<div class="rhl-envs-list" id="rhl-envs-list"></div>
|
|
91
|
+
<div class="rhl-envs-editor" id="rhl-envs-editor">
|
|
92
|
+
<em>Select an environment on the left.</em>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="rhl-modal__footer">
|
|
96
|
+
Reference these variables anywhere in a request with <code>{{name}}</code>.
|
|
97
|
+
Example: a variable <code>baseUrl = http://localhost:3000</code> used in the URL as
|
|
98
|
+
<code>{{baseUrl}}/v0/users</code> resolves to <code>http://localhost:3000/v0/users</code>
|
|
99
|
+
when the environment is active.
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
RailsHttpLab::Engine.routes.draw do
|
|
2
|
+
root to: "ui#index"
|
|
3
|
+
|
|
4
|
+
scope "api", defaults: { format: :json } do
|
|
5
|
+
get "tree", to: "collections#tree"
|
|
6
|
+
post "collections", to: "collections#create"
|
|
7
|
+
|
|
8
|
+
post "folders", to: "folders#create"
|
|
9
|
+
post "folders/rename", to: "folders#rename"
|
|
10
|
+
delete "folders/*path", to: "folders#destroy", constraints: { path: /.*/ }
|
|
11
|
+
|
|
12
|
+
post "requests/rename", to: "requests#rename"
|
|
13
|
+
get "requests/*path", to: "requests#show", constraints: { path: /.*/ }
|
|
14
|
+
put "requests/*path", to: "requests#update", constraints: { path: /.*/ }
|
|
15
|
+
delete "requests/*path", to: "requests#destroy", constraints: { path: /.*/ }
|
|
16
|
+
post "requests", to: "requests#create"
|
|
17
|
+
|
|
18
|
+
get "environments", to: "environments#index"
|
|
19
|
+
get "environments/:name", to: "environments#show"
|
|
20
|
+
put "environments/:name", to: "environments#update"
|
|
21
|
+
|
|
22
|
+
post "run", to: "runs#create"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "rails/generators/base"
|
|
3
|
+
|
|
4
|
+
module RailsHttpLab
|
|
5
|
+
module Generators
|
|
6
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
|
8
|
+
|
|
9
|
+
desc "Installs rails-http-lab: initializer, mount in routes, and docs/http-lab/ collection root."
|
|
10
|
+
|
|
11
|
+
def copy_initializer
|
|
12
|
+
template "initializer.rb.tt", "config/initializers/rails_http_lab.rb"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def mount_engine
|
|
16
|
+
route_line = 'mount RailsHttpLab::Engine => RailsHttpLab.config.mount_path'
|
|
17
|
+
routes_file = "config/routes.rb"
|
|
18
|
+
if File.exist?(routes_file) && File.read(routes_file).include?(route_line)
|
|
19
|
+
say_status :exist, "mount already present in #{routes_file}", :blue
|
|
20
|
+
else
|
|
21
|
+
route route_line
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_storage_root
|
|
26
|
+
# Just the empty root. `bruno.json` and any subfolders
|
|
27
|
+
# (environments/, collections, ...) are created lazily on first use
|
|
28
|
+
# via Storage::Filesystem#ensure_root!.
|
|
29
|
+
empty_directory "docs/http-lab"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def post_install_message
|
|
33
|
+
say ""
|
|
34
|
+
say "rails-http-lab installed.", :green
|
|
35
|
+
say "Mounted at /rails/http-lab (configurable in config/initializers/rails_http_lab.rb)"
|
|
36
|
+
say "Collections live in docs/http-lab/ (Bruno-compatible .bru files)"
|
|
37
|
+
say ""
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "rails_http_lab"
|
|
2
|
+
|
|
3
|
+
RailsHttpLab.configure do |c|
|
|
4
|
+
# Where the UI mounts. Default: /rails/http-lab
|
|
5
|
+
# c.mount_path = "/rails/http-lab"
|
|
6
|
+
|
|
7
|
+
# Where collections are stored. Default: Rails.root/docs/http-lab
|
|
8
|
+
# c.storage_path = Rails.root.join("docs", "http-lab")
|
|
9
|
+
|
|
10
|
+
# Rails environments where the engine is reachable. Default: [:development]
|
|
11
|
+
# Production requires an authenticator (see below) or the engine will refuse to boot.
|
|
12
|
+
# c.enabled_envs = %i[development]
|
|
13
|
+
|
|
14
|
+
# Optional gate. Receives the request object; return truthy to allow.
|
|
15
|
+
# c.authenticator = ->(request) { request.session[:admin] }
|
|
16
|
+
|
|
17
|
+
# Executor limits.
|
|
18
|
+
# c.executor_timeout = 30 # seconds
|
|
19
|
+
# c.executor_max_body = 10 * 1024 * 1024
|
|
20
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "rails_http_lab"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module RailsHttpLab
|
|
2
|
+
module Bruno
|
|
3
|
+
class Block
|
|
4
|
+
MODES = %i[kv raw].freeze
|
|
5
|
+
|
|
6
|
+
attr_accessor :name, :mode, :pairs, :raw
|
|
7
|
+
|
|
8
|
+
def initialize(name:, mode:, pairs: nil, raw: nil)
|
|
9
|
+
raise ArgumentError, "invalid mode: #{mode}" unless MODES.include?(mode)
|
|
10
|
+
@name = name
|
|
11
|
+
@mode = mode
|
|
12
|
+
@pairs = pairs || []
|
|
13
|
+
@raw = raw
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def kv?; mode == :kv; end
|
|
17
|
+
def raw?; mode == :raw; end
|
|
18
|
+
|
|
19
|
+
def [](key)
|
|
20
|
+
return nil unless kv?
|
|
21
|
+
found = pairs.find { |k, _| k == key }
|
|
22
|
+
found && found[1]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def []=(key, value)
|
|
26
|
+
return unless kv?
|
|
27
|
+
existing = pairs.find { |k, _| k == key }
|
|
28
|
+
if existing
|
|
29
|
+
existing[1] = value
|
|
30
|
+
else
|
|
31
|
+
pairs << [key, value]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_h
|
|
36
|
+
if kv?
|
|
37
|
+
pairs.to_h
|
|
38
|
+
else
|
|
39
|
+
{ raw: raw }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module RailsHttpLab
|
|
2
|
+
module Bruno
|
|
3
|
+
class Document
|
|
4
|
+
attr_accessor :blocks
|
|
5
|
+
attr_accessor :leading_blank_lines, :trailing_newline
|
|
6
|
+
|
|
7
|
+
def initialize(blocks: [], leading_blank_lines: 0, trailing_newline: true)
|
|
8
|
+
@blocks = blocks
|
|
9
|
+
@leading_blank_lines = leading_blank_lines
|
|
10
|
+
@trailing_newline = trailing_newline
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def block(name)
|
|
14
|
+
blocks.find { |b| b.name == name }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def blocks_named(name)
|
|
18
|
+
blocks.select { |b| b.name == name }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
VERBS = %w[get post put patch delete head options].freeze
|
|
22
|
+
|
|
23
|
+
def verb_block
|
|
24
|
+
blocks.find { |b| VERBS.include?(b.name) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def http_method
|
|
28
|
+
verb_block&.name&.upcase
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def url
|
|
32
|
+
verb_block&.[]("url")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
require "rails_http_lab/bruno/block"
|
|
2
|
+
require "rails_http_lab/bruno/document"
|
|
3
|
+
|
|
4
|
+
module RailsHttpLab
|
|
5
|
+
module Bruno
|
|
6
|
+
# Parses Bruno .bru files into a Document of ordered Blocks.
|
|
7
|
+
#
|
|
8
|
+
# Two content modes:
|
|
9
|
+
# - :kv — every line is " key: value" (2-space indent), one pair per line.
|
|
10
|
+
# - :raw — opaque body, preserved verbatim. Used for body:json, body:text,
|
|
11
|
+
# body:xml, body:graphql, body:graphql:vars, body:sparql,
|
|
12
|
+
# script:pre-request, script:post-response, tests, docs.
|
|
13
|
+
#
|
|
14
|
+
# Round-trip property: Serializer.dump(Parser.parse(s)) == s for files written
|
|
15
|
+
# by Bruno itself (see spec/bruno/round_trip_spec.rb).
|
|
16
|
+
class Parser
|
|
17
|
+
RAW_BLOCK_NAMES = %w[
|
|
18
|
+
body:json
|
|
19
|
+
body:text
|
|
20
|
+
body:xml
|
|
21
|
+
body:sparql
|
|
22
|
+
body:graphql
|
|
23
|
+
body:graphql:vars
|
|
24
|
+
script:pre-request
|
|
25
|
+
script:post-response
|
|
26
|
+
tests
|
|
27
|
+
docs
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
# name = letters/digits/_/- with optional colon-delimited variants
|
|
31
|
+
BLOCK_OPEN_RE = /\A([a-zA-Z][\w-]*(?::[\w-]+)*)\s*\{\s*\z/
|
|
32
|
+
|
|
33
|
+
def self.parse(source)
|
|
34
|
+
new(source).parse
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(source)
|
|
38
|
+
@source = source
|
|
39
|
+
# Split keeping line content; the last element may be "" if source ends with \n.
|
|
40
|
+
@lines = source.split("\n", -1)
|
|
41
|
+
@i = 0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse
|
|
45
|
+
blocks = []
|
|
46
|
+
leading_blanks = 0
|
|
47
|
+
|
|
48
|
+
# Count leading blank lines.
|
|
49
|
+
while @i < @lines.length && blank?(@lines[@i])
|
|
50
|
+
leading_blanks += 1
|
|
51
|
+
@i += 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
while @i < @lines.length
|
|
55
|
+
line = @lines[@i]
|
|
56
|
+
|
|
57
|
+
if blank?(line)
|
|
58
|
+
@i += 1
|
|
59
|
+
next
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if (m = line.match(BLOCK_OPEN_RE))
|
|
63
|
+
name = m[1]
|
|
64
|
+
@i += 1
|
|
65
|
+
if RAW_BLOCK_NAMES.include?(name)
|
|
66
|
+
blocks << parse_raw_block(name)
|
|
67
|
+
else
|
|
68
|
+
blocks << parse_kv_block(name)
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
# If we got here we hit something we don't understand. To stay forgiving,
|
|
72
|
+
# skip it; alternatively raise. We raise so callers know their file is off.
|
|
73
|
+
raise ParseError, "unexpected line outside of any block (line #{@i + 1}): #{line.inspect}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Trailing newline preserved iff source ends with \n (then @lines's last entry is "").
|
|
78
|
+
trailing_newline = @source.end_with?("\n")
|
|
79
|
+
|
|
80
|
+
Document.new(
|
|
81
|
+
blocks: blocks,
|
|
82
|
+
leading_blank_lines: leading_blanks,
|
|
83
|
+
trailing_newline: trailing_newline
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def blank?(line)
|
|
90
|
+
line.nil? || line.strip.empty?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def parse_kv_block(name)
|
|
94
|
+
pairs = []
|
|
95
|
+
while @i < @lines.length
|
|
96
|
+
line = @lines[@i]
|
|
97
|
+
if line.strip == "}"
|
|
98
|
+
@i += 1
|
|
99
|
+
return Block.new(name: name, mode: :kv, pairs: pairs)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
if blank?(line)
|
|
103
|
+
@i += 1
|
|
104
|
+
next
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Parse " key: value" — first ":" splits. Value may span multiple
|
|
108
|
+
# lines via unbalanced braces (URLs with embedded JSON) or via
|
|
109
|
+
# triple-quoted '''...''' strings (Bruno multi-line literal).
|
|
110
|
+
stripped = line.sub(/\A {0,4}/, "") # tolerate 0-4 leading spaces
|
|
111
|
+
colon_idx = stripped.index(":")
|
|
112
|
+
if colon_idx.nil?
|
|
113
|
+
raise ParseError, "expected 'key: value' in block #{name} at line #{@i + 1}: #{line.inspect}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
key = stripped[0...colon_idx]
|
|
117
|
+
value = stripped[(colon_idx + 1)..]
|
|
118
|
+
value = value.sub(/\A /, "") if value
|
|
119
|
+
@i += 1
|
|
120
|
+
|
|
121
|
+
value = consume_multiline_value(value, block_name: name)
|
|
122
|
+
pairs << [key, value]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
raise ParseError, "unterminated block #{name.inspect} (missing closing '}')"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Returns the (possibly multi-line) value. Continues reading lines while
|
|
129
|
+
# the running brace depth of the value is > 0 OR a '''...''' string is open.
|
|
130
|
+
def consume_multiline_value(value, block_name:)
|
|
131
|
+
while value_needs_continuation?(value)
|
|
132
|
+
if @i >= @lines.length
|
|
133
|
+
raise ParseError, "unterminated multi-line value in block #{block_name.inspect}"
|
|
134
|
+
end
|
|
135
|
+
value = "#{value}\n#{@lines[@i]}"
|
|
136
|
+
@i += 1
|
|
137
|
+
end
|
|
138
|
+
value
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def value_needs_continuation?(value)
|
|
142
|
+
in_triple = false
|
|
143
|
+
depth = 0
|
|
144
|
+
i = 0
|
|
145
|
+
len = value.length
|
|
146
|
+
while i < len
|
|
147
|
+
if value[i, 3] == "'''"
|
|
148
|
+
in_triple = !in_triple
|
|
149
|
+
i += 3
|
|
150
|
+
next
|
|
151
|
+
end
|
|
152
|
+
unless in_triple
|
|
153
|
+
c = value[i]
|
|
154
|
+
depth += 1 if c == "{"
|
|
155
|
+
depth -= 1 if c == "}"
|
|
156
|
+
end
|
|
157
|
+
i += 1
|
|
158
|
+
end
|
|
159
|
+
in_triple || depth > 0
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Counts braces to find matching '}'. Body content can include arbitrary braces
|
|
163
|
+
# (JSON objects, JS blocks, etc.) so we can't rely on indentation alone.
|
|
164
|
+
def parse_raw_block(name)
|
|
165
|
+
content_lines = []
|
|
166
|
+
depth = 1 # we already consumed the opening '{'
|
|
167
|
+
|
|
168
|
+
while @i < @lines.length
|
|
169
|
+
line = @lines[@i]
|
|
170
|
+
|
|
171
|
+
# Compute depth change from this line.
|
|
172
|
+
opens = line.count("{")
|
|
173
|
+
closes = line.count("}")
|
|
174
|
+
|
|
175
|
+
# If this line would close the block (depth would reach 0 here),
|
|
176
|
+
# we need to figure out *which* '}' closes it. If the line is exactly "}"
|
|
177
|
+
# (with optional leading whitespace) AND no '{' on the same line AND
|
|
178
|
+
# depth would go to 0, that's the closing brace and isn't part of content.
|
|
179
|
+
if depth + opens - closes <= 0
|
|
180
|
+
# Edge case: closing brace is not alone on its line (mixed content).
|
|
181
|
+
# We try to honor the "alone on a line" convention Bruno uses.
|
|
182
|
+
if line.strip == "}" && opens == 0
|
|
183
|
+
@i += 1
|
|
184
|
+
return Block.new(name: name, mode: :raw, raw: content_lines.join("\n"))
|
|
185
|
+
else
|
|
186
|
+
# Mixed line: we still treat the final '}' as terminator and keep the
|
|
187
|
+
# rest as content. This branch is defensive; real Bruno files don't hit it.
|
|
188
|
+
# Strip the trailing '}' character from the line; everything before it
|
|
189
|
+
# is content. This is best-effort.
|
|
190
|
+
last_brace = line.rindex("}")
|
|
191
|
+
prefix = line[0...last_brace]
|
|
192
|
+
content_lines << prefix unless prefix.empty?
|
|
193
|
+
@i += 1
|
|
194
|
+
return Block.new(name: name, mode: :raw, raw: content_lines.join("\n"))
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
content_lines << line
|
|
199
|
+
depth += opens - closes
|
|
200
|
+
@i += 1
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
raise ParseError, "unterminated raw block #{name.inspect}"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module RailsHttpLab
|
|
2
|
+
module Bruno
|
|
3
|
+
# Round-trip-stable serializer for Bruno documents.
|
|
4
|
+
#
|
|
5
|
+
# Format per block:
|
|
6
|
+
#
|
|
7
|
+
# <name> {
|
|
8
|
+
# key: value
|
|
9
|
+
# }
|
|
10
|
+
#
|
|
11
|
+
# Raw blocks emit their `raw` content verbatim between '{' and '}'.
|
|
12
|
+
# Blocks are separated by a single blank line, matching Bruno's output.
|
|
13
|
+
class Serializer
|
|
14
|
+
def self.dump(document)
|
|
15
|
+
new(document).dump
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(document)
|
|
19
|
+
@document = document
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def dump
|
|
23
|
+
out = +""
|
|
24
|
+
out << ("\n" * @document.leading_blank_lines)
|
|
25
|
+
|
|
26
|
+
@document.blocks.each_with_index do |block, idx|
|
|
27
|
+
out << "\n" if idx > 0
|
|
28
|
+
out << render_block(block)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if @document.trailing_newline && !out.end_with?("\n")
|
|
32
|
+
out << "\n"
|
|
33
|
+
elsif !@document.trailing_newline && out.end_with?("\n")
|
|
34
|
+
out.chomp!
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
out
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def render_block(block)
|
|
43
|
+
case block.mode
|
|
44
|
+
when :kv then render_kv(block)
|
|
45
|
+
when :raw then render_raw(block)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def render_kv(block)
|
|
50
|
+
lines = +"#{block.name} {\n"
|
|
51
|
+
block.pairs.each do |k, v|
|
|
52
|
+
lines << " #{k}: #{v}\n"
|
|
53
|
+
end
|
|
54
|
+
lines << "}\n"
|
|
55
|
+
lines
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def render_raw(block)
|
|
59
|
+
body = block.raw.to_s
|
|
60
|
+
out = +"#{block.name} {\n"
|
|
61
|
+
out << body
|
|
62
|
+
out << "\n" unless body.empty? || body.end_with?("\n")
|
|
63
|
+
out << "}\n"
|
|
64
|
+
out
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require "rails_http_lab/bruno/block"
|
|
2
|
+
require "rails_http_lab/bruno/document"
|
|
3
|
+
require "rails_http_lab/bruno/parser"
|
|
4
|
+
require "rails_http_lab/bruno/serializer"
|
|
5
|
+
|
|
6
|
+
module RailsHttpLab
|
|
7
|
+
module Bruno
|
|
8
|
+
def self.parse(src); Parser.parse(src); end
|
|
9
|
+
def self.dump(doc); Serializer.dump(doc); end
|
|
10
|
+
def self.parse_file(path); parse(File.read(path)); end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module RailsHttpLab
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :mount_path,
|
|
4
|
+
:storage_path,
|
|
5
|
+
:enabled_envs,
|
|
6
|
+
:authenticator,
|
|
7
|
+
:executor_timeout,
|
|
8
|
+
:executor_max_body
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@mount_path = "/rails/http-lab"
|
|
12
|
+
@storage_path = nil
|
|
13
|
+
@enabled_envs = %i[development]
|
|
14
|
+
@authenticator = nil
|
|
15
|
+
@executor_timeout = 30
|
|
16
|
+
@executor_max_body = 10 * 1024 * 1024
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def resolved_storage_path
|
|
20
|
+
@storage_path || default_storage_path
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def default_storage_path
|
|
26
|
+
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
27
|
+
Rails.root.join("docs", "http-lab")
|
|
28
|
+
else
|
|
29
|
+
File.expand_path("docs/http-lab", Dir.pwd)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
def configuration
|
|
36
|
+
@configuration ||= Configuration.new
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def configure
|
|
40
|
+
yield(configuration)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def config
|
|
44
|
+
configuration
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def reset_configuration!
|
|
48
|
+
@configuration = Configuration.new
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require "rails/engine"
|
|
2
|
+
|
|
3
|
+
module RailsHttpLab
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace RailsHttpLab
|
|
6
|
+
|
|
7
|
+
initializer "rails_http_lab.assets" do |app|
|
|
8
|
+
if app.config.respond_to?(:assets) && app.config.assets.respond_to?(:precompile)
|
|
9
|
+
app.config.assets.precompile += %w[rails_http_lab/application.css rails_http_lab/application.js]
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
config.after_initialize do
|
|
14
|
+
cfg = RailsHttpLab.config
|
|
15
|
+
if cfg.enabled_envs.include?(:production) &&
|
|
16
|
+
defined?(Rails) && Rails.env.production? &&
|
|
17
|
+
cfg.authenticator.nil?
|
|
18
|
+
raise <<~MSG
|
|
19
|
+
[rails-http-lab] Refusing to boot: production is in enabled_envs but no authenticator is configured.
|
|
20
|
+
Set RailsHttpLab.config.authenticator to a callable ->(request) { ... } or remove :production from enabled_envs.
|
|
21
|
+
MSG
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module RailsHttpLab
|
|
2
|
+
module Execution
|
|
3
|
+
Response = Struct.new(
|
|
4
|
+
:status, :headers, :body, :duration_ms, :size_bytes, :error, :request,
|
|
5
|
+
keyword_init: true
|
|
6
|
+
) do
|
|
7
|
+
def to_h
|
|
8
|
+
{
|
|
9
|
+
status: status,
|
|
10
|
+
headers: headers,
|
|
11
|
+
body: body,
|
|
12
|
+
duration_ms: duration_ms,
|
|
13
|
+
size_bytes: size_bytes,
|
|
14
|
+
error: error,
|
|
15
|
+
request: request
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|