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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +15 -0
  3. data/README.md +60 -0
  4. data/app/assets/javascripts/rails_http_lab/application.js +1318 -0
  5. data/app/assets/stylesheets/rails_http_lab/application.css +336 -0
  6. data/app/controllers/rails_http_lab/application_controller.rb +47 -0
  7. data/app/controllers/rails_http_lab/collections_controller.rb +20 -0
  8. data/app/controllers/rails_http_lab/environments_controller.rb +33 -0
  9. data/app/controllers/rails_http_lab/folders_controller.rb +47 -0
  10. data/app/controllers/rails_http_lab/requests_controller.rb +99 -0
  11. data/app/controllers/rails_http_lab/runs_controller.rb +34 -0
  12. data/app/controllers/rails_http_lab/ui_controller.rb +7 -0
  13. data/app/views/layouts/rails_http_lab.html.erb +14 -0
  14. data/app/views/rails_http_lab/ui/index.html.erb +103 -0
  15. data/config/routes.rb +24 -0
  16. data/lib/generators/rails_http_lab/install/install_generator.rb +41 -0
  17. data/lib/generators/rails_http_lab/install/templates/initializer.rb.tt +20 -0
  18. data/lib/rails-http-lab.rb +1 -0
  19. data/lib/rails_http_lab/bruno/block.rb +44 -0
  20. data/lib/rails_http_lab/bruno/document.rb +36 -0
  21. data/lib/rails_http_lab/bruno/parser.rb +207 -0
  22. data/lib/rails_http_lab/bruno/serializer.rb +68 -0
  23. data/lib/rails_http_lab/bruno.rb +12 -0
  24. data/lib/rails_http_lab/configuration.rb +51 -0
  25. data/lib/rails_http_lab/engine.rb +25 -0
  26. data/lib/rails_http_lab/execution/response.rb +20 -0
  27. data/lib/rails_http_lab/execution/runner.rb +187 -0
  28. data/lib/rails_http_lab/execution/variable_resolver.rb +30 -0
  29. data/lib/rails_http_lab/execution.rb +3 -0
  30. data/lib/rails_http_lab/storage/filesystem.rb +123 -0
  31. data/lib/rails_http_lab/storage/tree.rb +120 -0
  32. data/lib/rails_http_lab/storage.rb +2 -0
  33. data/lib/rails_http_lab/version.rb +3 -0
  34. data/lib/rails_http_lab.rb +14 -0
  35. 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