rails-http-lab 0.1.0 → 0.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 455a98547ed2be13947848e16d5225d1107b5a4cbce170ebf6b5d8bcfcdf071a
4
- data.tar.gz: b38a9225af1559ad6ead838f4fede715ac4ed399fd2d9fd6c79e32787d400714
3
+ metadata.gz: '09a056aea1f71d9c958b238c6eb6bda36ac6cb982315bc19b540652926c05726'
4
+ data.tar.gz: a469930e6fc214f3ca3310581ea20210fa5bba233d3aefba9c6095bbfa55b1e9
5
5
  SHA512:
6
- metadata.gz: 895dc4334974bb9bb08e07a8fdbcb3f4e8f7b50fa7b7ed846930fdd63cf49d3a9e3ec28b94167bc7cc6bf0963ba649b000eb89f51c2fd4daf41a3fec9dbf90a2
7
- data.tar.gz: 84f1c94df54f49893c87e0790de96acc2d574eddf9d664222e08d6ea4866a0994f831099c630fd76e2358059093d143fb23edd83aabc50f567c5988c599cf7bb
6
+ metadata.gz: b1f002fe432bd2ba8ea8757b26c83d1614bd27f665155493596bbcfe90feeb28fb4eb6ef9ad454d71480df37daab8ea50a3aff4308473da3cd730803af6d8aab
7
+ data.tar.gz: 1771183d8b275004bb49e393d1d9b631df83e76ed6f5962d7ca51071d20f311acfd1910c9963013b8c5a8919587f5b57d6b91d202d4e15c11cd857e70cb03956
data/README.md CHANGED
@@ -4,7 +4,7 @@ In-app HTTP request lab for Rails. Mounts a UI at `/rails/http-lab` and persists
4
4
 
5
5
  No external app, no separate workspace — your requests live next to the code that serves them, version-controlled in `docs/http-lab/`.
6
6
 
7
- ![Rails HTTP Lab](https://raw.githubusercontent.com/jacksonpires/rails-http-lab/main/docs/rails-http-lab.png)
7
+ ![Rails HTTP Lab demo: browsing a collection, sending requests with an environment, editing params, JSON body with bearer auth, cURL export and environments](https://raw.githubusercontent.com/jacksonpires/rails-http-lab/main/docs/rails-http-lab.gif)
8
8
 
9
9
  ## Why
10
10
 
@@ -30,6 +30,21 @@ bin/rails g rails_http_lab:install
30
30
 
31
31
  Visit `http://localhost:3000/rails/http-lab`.
32
32
 
33
+ ## Compatibility
34
+
35
+ Requires Ruby `>= 3.1` and Rails `>= 7.0`. Every combination below runs in
36
+ [CI](.github/workflows/ci.yml):
37
+
38
+ | Ruby \ Rails | 7.0 | 7.1 | 7.2 | 8.0 | 8.1 |
39
+ |--------------|:---:|:---:|:---:|:---:|:---:|
40
+ | **4.0** | ✅ | ✅ | ✅ | ✅ | ✅ |
41
+ | **3.4** | ✅ | ✅ | ✅ | ✅ | ✅ |
42
+ | **3.3** | ✅ | ✅ | ✅ | ✅ | ✅ |
43
+ | **3.2** | ✅ | ✅ | ✅ | ✅ | ✅ |
44
+ | **3.1** | ✅ | ✅ | ✅ | — | — |
45
+
46
+ — Rails 8.x itself requires Ruby `>= 3.2`.
47
+
33
48
  ## Configuration
34
49
 
35
50
  `config/initializers/rails_http_lab.rb`:
@@ -55,6 +70,20 @@ risk if exposed to untrusted users. Defaults:
55
70
  - Bruno `script` and `tests` blocks are **persisted but not executed** to avoid
56
71
  arbitrary code execution. Run them in Bruno itself.
57
72
 
73
+ ## Development
74
+
75
+ ```bash
76
+ bundle install
77
+ bundle exec rspec
78
+ ```
79
+
80
+ To run the suite against a specific Rails series (as CI does), set
81
+ `RAILS_VERSION` before installing:
82
+
83
+ ```bash
84
+ RAILS_VERSION=7.2 bundle update rails && RAILS_VERSION=7.2 bundle exec rspec
85
+ ```
86
+
58
87
  ## License
59
88
 
60
89
  MIT
@@ -86,13 +86,25 @@
86
86
  function normalizedSource(doc) {
87
87
  if (!doc) return "";
88
88
  const blocks = (doc.blocks || []).filter((b) => {
89
- if (b.mode === "raw") return (b.raw || "").trim() !== "";
89
+ if (b.mode === "raw" || b.mode === "list") return (b.raw || "").trim() !== "";
90
90
  const pairs = (b.pairs || []).filter(([k, v]) =>
91
91
  (k || "").trim() !== "" || (v || "").trim() !== "");
92
92
  return pairs.length > 0;
93
93
  });
94
94
  return serializeDoc({ blocks });
95
95
  }
96
+ // Viewing a tab lazily injects an empty block (e.g. `params:query {}`).
97
+ // Drop those before writing/sending so they don't pollute the .bru file —
98
+ // but keep empty blocks that were already on disk, for byte-stable round-trip.
99
+ function isEmptyBlock(b) {
100
+ if (b.mode === "raw" || b.mode === "list") return (b.raw || "").trim() === "";
101
+ return !(b.pairs || []).some(([k, v]) => (k || "").trim() !== "" || (v || "").trim() !== "");
102
+ }
103
+ function persistableDoc(session) {
104
+ const onDisk = new Set((session.savedDoc?.blocks || []).map((b) => b.name));
105
+ return { blocks: (session.doc.blocks || []).filter((b) => !isEmptyBlock(b) || onDisk.has(b.name)) };
106
+ }
107
+
96
108
  function isDirty(session) {
97
109
  if (!session) return false;
98
110
  return normalizedSource(session.doc) !== normalizedSource(session.savedDoc);
@@ -201,10 +213,11 @@
201
213
  // ---------- .bru source serializer (client-side) ----------
202
214
  function serializeDoc(doc) {
203
215
  return (doc.blocks || []).map((b) => {
204
- if (b.mode === "raw") {
216
+ if (b.mode === "raw" || b.mode === "list") {
217
+ const [open, close] = b.mode === "list" ? ["[", "]"] : ["{", "}"];
205
218
  const body = b.raw || "";
206
219
  const tail = body.endsWith("\n") || body === "" ? "" : "\n";
207
- return `${b.name} {\n${body}${tail}}\n`;
220
+ return `${b.name} ${open}\n${body}${tail}${close}\n`;
208
221
  }
209
222
  const lines = (b.pairs || []).map(([k, v]) => ` ${k}: ${v}`).join("\n");
210
223
  return `${b.name} {\n${lines}${lines ? "\n" : ""}}\n`;
@@ -300,6 +313,7 @@
300
313
  const s = state.sessions.get(hydratedKey);
301
314
  if (!s) return;
302
315
  state.currentKey = hydratedKey;
316
+ ensureExpanded(parentDir(hydratedKey)); // don't restore into a collapsed folder
303
317
  showEditorForSession(s);
304
318
  }
305
319
 
@@ -678,6 +692,7 @@
678
692
 
679
693
  function renderKvTab(panel, blockName, placeholder) {
680
694
  const block = ensureKvBlock(blockName);
695
+ const changed = blockName === "params:query" ? syncUrlFromParams : () => {};
681
696
  const table = ce("table", { class: "rhl-kv-table" });
682
697
  const thead = ce("thead", {}, ce("tr", {}, ce("th", {}, "Name"), ce("th", {}, "Value"), ce("th", {})));
683
698
  const tbody = ce("tbody");
@@ -688,15 +703,15 @@
688
703
  const tr = ce("tr");
689
704
  tr.appendChild(ce("td", {}, ce("input", {
690
705
  value: pair[0] || "", placeholder,
691
- oninput: (e) => { block.pairs[i][0] = e.target.value; }
706
+ oninput: (e) => { block.pairs[i][0] = e.target.value; changed(); }
692
707
  })));
693
708
  tr.appendChild(ce("td", {}, ce("input", {
694
709
  value: pair[1] || "", placeholder: "value",
695
- oninput: (e) => { block.pairs[i][1] = e.target.value; }
710
+ oninput: (e) => { block.pairs[i][1] = e.target.value; changed(); }
696
711
  })));
697
712
  tr.appendChild(ce("td", { class: "rhl-kv-table__del" }, ce("button", {
698
713
  class: "rhl-iconbtn",
699
- onclick: () => { block.pairs.splice(i, 1); onDocEdited(); renderPanel(); }
714
+ onclick: () => { block.pairs.splice(i, 1); changed(); onDocEdited(); renderPanel(); }
700
715
  }, "×")));
701
716
  tbody.appendChild(tr);
702
717
  });
@@ -826,6 +841,40 @@
826
841
  }
827
842
  }
828
843
 
844
+ // ---------- URL <-> params:query sync ----------
845
+ // Bruno keeps the URL's query string and the params:query block mirrored
846
+ // (and only sends the URL), so edits on either side must update the other.
847
+ // Disabled params (`~key`) live only in the block, never in the URL.
848
+ function splitUrl(url) {
849
+ const i = url.indexOf("?");
850
+ return i < 0 ? [url, ""] : [url.slice(0, i), url.slice(i + 1)];
851
+ }
852
+ function syncUrlFromParams() {
853
+ if (!currentDoc()) return;
854
+ const block = findBlock(currentDoc(), "params:query");
855
+ const enabled = (block?.pairs || []).filter(([k]) => k && !k.startsWith("~"));
856
+ const [base] = splitUrl($("#rhl-url").value);
857
+ const qs = enabled.map(([k, v]) => `${k}=${v || ""}`).join("&");
858
+ const url = qs ? `${base}?${qs}` : base;
859
+ $("#rhl-url").value = url;
860
+ setVerbPair("url", url);
861
+ }
862
+ function syncParamsFromUrl() {
863
+ const doc = currentDoc();
864
+ if (!doc) return;
865
+ const [, qs] = splitUrl($("#rhl-url").value);
866
+ const fromUrl = qs.split("&").filter(Boolean).map((part) => {
867
+ const i = part.indexOf("=");
868
+ return i < 0 ? [part, ""] : [part.slice(0, i), part.slice(i + 1)];
869
+ });
870
+ const existing = findBlock(doc, "params:query");
871
+ if (!existing && fromUrl.length === 0) return;
872
+ const block = existing || ensureKvBlock("params:query");
873
+ const disabled = block.pairs.filter(([k]) => (k || "").startsWith("~"));
874
+ block.pairs = [...fromUrl, ...disabled];
875
+ if (activeSession()?.activeTab === "params") renderPanel();
876
+ }
877
+
829
878
  function formRow(label, block, field) {
830
879
  const pair = block.pairs.find(([k]) => k === field);
831
880
  const input = ce("input", {
@@ -885,7 +934,7 @@
885
934
  state.envName = envName;
886
935
  try {
887
936
  const resp = await api("POST", "/run", {
888
- source: serializeDoc(session.doc),
937
+ source: serializeDoc(persistableDoc(session)),
889
938
  environment: envName
890
939
  });
891
940
  session.response = resp;
@@ -910,14 +959,14 @@
910
959
  if (!path.endsWith(".bru")) path += ".bru";
911
960
  }
912
961
  try {
913
- await api("PUT", `/requests/${encodePath(path)}`, { source: serializeDoc(session.doc) });
962
+ await api("PUT", `/requests/${encodePath(path)}`, { source: serializeDoc(persistableDoc(session)) });
914
963
  if (path !== session.path) {
915
964
  state.sessions.delete(session.path);
916
965
  session.path = path;
917
966
  state.sessions.set(path, session);
918
967
  state.currentKey = path;
919
968
  }
920
- session.savedDoc = cloneDoc(session.doc); // now clean
969
+ session.savedDoc = cloneDoc(persistableDoc(session)); // now clean (matches disk)
921
970
  session._wasDirty = false;
922
971
  ensureExpanded(parentDir(path));
923
972
  await loadTree();
@@ -1300,7 +1349,11 @@
1300
1349
 
1301
1350
  // Keep verb/url synced into the active doc as the user types.
1302
1351
  $("#rhl-verb").addEventListener("change", () => { if (currentDoc()) setVerbName($("#rhl-verb").value.toLowerCase()); });
1303
- $("#rhl-url").addEventListener("input", () => { if (currentDoc()) setVerbPair("url", $("#rhl-url").value); });
1352
+ $("#rhl-url").addEventListener("input", () => {
1353
+ if (!currentDoc()) return;
1354
+ setVerbPair("url", $("#rhl-url").value);
1355
+ syncParamsFromUrl();
1356
+ });
1304
1357
  }
1305
1358
 
1306
1359
  document.addEventListener("DOMContentLoaded", async () => {
@@ -253,6 +253,7 @@ html, body.rhl-body {
253
253
 
254
254
  .rhl-response__meta { display: flex; gap: 8px; font-size: 12px; align-items: center; }
255
255
  .rhl-response__meta span { padding: 2px 8px; background: var(--rhl-bg-alt); border-radius: 3px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
256
+ .rhl-response__meta span:empty { display: none; }
256
257
  .rhl-pill { font-weight: 700; }
257
258
  .rhl-pill--ok { background: #DCEEDC !important; color: var(--rhl-success); }
258
259
  .rhl-pill--warn { background: #FFE9C7 !important; color: var(--rhl-warn); }
@@ -39,8 +39,8 @@ module RailsHttpLab
39
39
 
40
40
  def render_unprocessable(error)
41
41
  respond_to do |format|
42
- format.json { render json: { error: error.message }, status: :unprocessable_entity }
43
- format.any { head :unprocessable_entity }
42
+ format.json { render json: { error: error.message }, status: 422 }
43
+ format.any { head 422 }
44
44
  end
45
45
  end
46
46
  end
@@ -17,15 +17,29 @@ module RailsHttpLab
17
17
  v = v.to_unsafe_h if v.respond_to?(:to_unsafe_h)
18
18
  [v["key"].to_s, v["value"].to_s]
19
19
  }
20
- doc = RailsHttpLab::Bruno::Document.new(blocks: [
21
- RailsHttpLab::Bruno::Block.new(name: "vars", mode: :kv, pairs: pairs)
22
- ])
23
- storage.write_bru("environments/#{params[:name]}.bru", doc)
20
+ rel = "environments/#{params[:name]}.bru"
21
+ doc = existing_document(rel) || RailsHttpLab::Bruno::Document.new
22
+ vars_block = doc.block("vars")
23
+ if vars_block
24
+ vars_block.pairs = pairs
25
+ else
26
+ # Bruno writes `vars` first, before e.g. `vars:secret [ ... ]`.
27
+ doc.blocks.unshift(RailsHttpLab::Bruno::Block.new(name: "vars", mode: :kv, pairs: pairs))
28
+ end
29
+ # Only `vars` is editable here; any other block (notably the
30
+ # `vars:secret` list Bruno desktop maintains) is written back untouched.
31
+ storage.write_bru(rel, doc)
24
32
  render json: { ok: true }
25
33
  end
26
34
 
27
35
  private
28
36
 
37
+ def existing_document(rel)
38
+ storage.read_bru(rel)
39
+ rescue RailsHttpLab::NotFoundError
40
+ nil
41
+ end
42
+
29
43
  def storage
30
44
  @storage ||= RailsHttpLab::Storage::Filesystem.new
31
45
  end
@@ -73,6 +73,8 @@ module RailsHttpLab
73
73
  mode = b["mode"]&.to_sym || :kv
74
74
  if mode == :raw
75
75
  docs << "#{b['name']} {\n#{b['raw']}\n}\n"
76
+ elsif mode == :list
77
+ docs << "#{b['name']} [\n#{b['raw']}\n]\n"
76
78
  else
77
79
  lines = Array(b["pairs"]).map { |k, v| " #{k}: #{v}" }.join("\n")
78
80
  docs << "#{b['name']} {\n#{lines}#{lines.empty? ? '' : "\n"}}\n"
@@ -89,6 +91,8 @@ module RailsHttpLab
89
91
  blocks: doc.blocks.map { |b|
90
92
  if b.kv?
91
93
  { name: b.name, mode: "kv", pairs: b.pairs }
94
+ elsif b.list?
95
+ { name: b.name, mode: "list", raw: b.raw }
92
96
  else
93
97
  { name: b.name, mode: "raw", raw: b.raw }
94
98
  end
@@ -1,7 +1,11 @@
1
1
  module RailsHttpLab
2
2
  module Bruno
3
3
  class Block
4
- MODES = %i[kv raw].freeze
4
+ # :kv — `name {` + `key: value` lines
5
+ # :raw — `name {` + opaque body, verbatim
6
+ # :list — `name [` + one item per line (e.g. Bruno's `vars:secret [`),
7
+ # body kept verbatim in `raw` for byte-stable round-trip
8
+ MODES = %i[kv raw list].freeze
5
9
 
6
10
  attr_accessor :name, :mode, :pairs, :raw
7
11
 
@@ -15,6 +19,13 @@ module RailsHttpLab
15
19
 
16
20
  def kv?; mode == :kv; end
17
21
  def raw?; mode == :raw; end
22
+ def list?; mode == :list; end
23
+
24
+ # Item names of a :list block. Bruno separates items with ",\n".
25
+ def items
26
+ return [] unless list?
27
+ raw.to_s.split("\n").map { |l| l.strip.delete_suffix(",").strip }.reject(&:empty?)
28
+ end
18
29
 
19
30
  def [](key)
20
31
  return nil unless kv?
@@ -35,6 +46,8 @@ module RailsHttpLab
35
46
  def to_h
36
47
  if kv?
37
48
  pairs.to_h
49
+ elsif list?
50
+ { items: items }
38
51
  else
39
52
  { raw: raw }
40
53
  end
@@ -5,9 +5,10 @@ module RailsHttpLab
5
5
  module Bruno
6
6
  # Parses Bruno .bru files into a Document of ordered Blocks.
7
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,
8
+ # Three content modes:
9
+ # - :kv — every line is " key: value" (2-space indent), one pair per line.
10
+ # - :list — `name [ ... ]` (e.g. `vars:secret`), body preserved verbatim.
11
+ # - :raw — opaque body, preserved verbatim. Used for body:json, body:text,
11
12
  # body:xml, body:graphql, body:graphql:vars, body:sparql,
12
13
  # script:pre-request, script:post-response, tests, docs.
13
14
  #
@@ -29,6 +30,8 @@ module RailsHttpLab
29
30
 
30
31
  # name = letters/digits/_/- with optional colon-delimited variants
31
32
  BLOCK_OPEN_RE = /\A([a-zA-Z][\w-]*(?::[\w-]+)*)\s*\{\s*\z/
33
+ # List blocks, e.g. the `vars:secret [ ... ]` Bruno writes in environments.
34
+ LIST_OPEN_RE = /\A([a-zA-Z][\w-]*(?::[\w-]+)*)\s*\[\s*\z/
32
35
 
33
36
  def self.parse(source)
34
37
  new(source).parse
@@ -67,6 +70,9 @@ module RailsHttpLab
67
70
  else
68
71
  blocks << parse_kv_block(name)
69
72
  end
73
+ elsif (m = line.match(LIST_OPEN_RE))
74
+ @i += 1
75
+ blocks << parse_list_block(m[1])
70
76
  else
71
77
  # If we got here we hit something we don't understand. To stay forgiving,
72
78
  # skip it; alternatively raise. We raise so callers know their file is off.
@@ -159,6 +165,20 @@ module RailsHttpLab
159
165
  in_triple || depth > 0
160
166
  end
161
167
 
168
+ # Reads until a line that is exactly "]" (Bruno's convention). Item lines
169
+ # are kept verbatim so the block round-trips byte-for-byte.
170
+ def parse_list_block(name)
171
+ content_lines = []
172
+ while @i < @lines.length
173
+ line = @lines[@i]
174
+ @i += 1
175
+ return Block.new(name: name, mode: :list, raw: content_lines.join("\n")) if line.strip == "]"
176
+ content_lines << line
177
+ end
178
+
179
+ raise ParseError, "unterminated list block #{name.inspect} (missing closing ']')"
180
+ end
181
+
162
182
  # Counts braces to find matching '}'. Body content can include arbitrary braces
163
183
  # (JSON objects, JS blocks, etc.) so we can't rely on indentation alone.
164
184
  def parse_raw_block(name)
@@ -42,7 +42,8 @@ module RailsHttpLab
42
42
  def render_block(block)
43
43
  case block.mode
44
44
  when :kv then render_kv(block)
45
- when :raw then render_raw(block)
45
+ when :raw then render_raw(block)
46
+ when :list then render_raw(block, open: "[", close: "]")
46
47
  end
47
48
  end
48
49
 
@@ -55,12 +56,12 @@ module RailsHttpLab
55
56
  lines
56
57
  end
57
58
 
58
- def render_raw(block)
59
+ def render_raw(block, open: "{", close: "}")
59
60
  body = block.raw.to_s
60
- out = +"#{block.name} {\n"
61
+ out = +"#{block.name} #{open}\n"
61
62
  out << body
62
63
  out << "\n" unless body.empty? || body.end_with?("\n")
63
- out << "}\n"
64
+ out << "#{close}\n"
64
65
  out
65
66
  end
66
67
  end
@@ -43,8 +43,15 @@ module RailsHttpLab
43
43
  body_kind = verb_block["body"].to_s
44
44
  auth_kind = verb_block["auth"].to_s
45
45
 
46
+ unresolved = url.scan(/\{\{\s*([^{}]+?)\s*\}\}/).flatten.uniq
47
+ unless unresolved.empty?
48
+ names = unresolved.map { |n| "{{#{n}}}" }.join(", ")
49
+ raise URI::InvalidURIError,
50
+ "unresolved variable #{names} — select an environment that defines it"
51
+ end
52
+
46
53
  uri = URI.parse(url)
47
- merge_query!(uri)
54
+ merge_query!(uri, auth_kind)
48
55
 
49
56
  klass = VERBS.fetch(verb_block.name)
50
57
  request = klass.new(uri.request_uri)
@@ -103,16 +110,42 @@ module RailsHttpLab
103
110
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
104
111
  end
105
112
 
106
- def merge_query!(uri)
113
+ # Bruno keeps the URL's query string and the `params:query` block in
114
+ # sync, so the same params usually appear in both. Keys present in the
115
+ # block (or added by apikey auth) replace the URL's occurrences of those
116
+ # keys instead of being appended twice; other URL params are kept as-is.
117
+ def merge_query!(uri, auth_kind)
118
+ extra = []
119
+
107
120
  params_block = @doc.block("params:query") || @doc.block("query")
108
- return unless params_block&.kv?
121
+ if params_block&.kv?
122
+ params_block.pairs.each do |k, v|
123
+ next if k.start_with?("~")
124
+ extra << [k, @resolver.resolve(v.to_s)]
125
+ end
126
+ end
127
+
128
+ if auth_kind == "apikey" && (auth = @doc.block("auth:apikey")) && auth["placement"].to_s == "queryparams"
129
+ key = @resolver.resolve(auth["key"].to_s)
130
+ extra << [key, @resolver.resolve(auth["value"].to_s)] unless key.empty?
131
+ end
132
+
133
+ return if extra.empty?
109
134
 
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))}"
135
+ overridden = extra.map(&:first)
136
+ kept = uri.query.to_s.split("&").reject do |part|
137
+ part.empty? || overridden.include?(query_key(part))
138
+ end
139
+ added = extra.map do |k, v|
140
+ "#{URI.encode_www_form_component(k)}=#{URI.encode_www_form_component(v)}"
113
141
  end
114
- merged = [existing, extra.join("&")].reject(&:empty?).join("&")
115
- uri.query = merged unless merged.empty?
142
+ uri.query = (kept + added).join("&")
143
+ end
144
+
145
+ def query_key(part)
146
+ URI.decode_www_form_component(part.split("=", 2).first)
147
+ rescue ArgumentError
148
+ part.split("=", 2).first
116
149
  end
117
150
 
118
151
  def apply_headers!(request)
@@ -138,13 +171,8 @@ module RailsHttpLab
138
171
  return unless block
139
172
  key = @resolver.resolve(block["key"].to_s)
140
173
  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
174
+ # placement "queryparams" is applied to the URL in merge_query!
175
+ request[key] = val unless key.empty? || block["placement"].to_s == "queryparams"
148
176
  end
149
177
  end
150
178
 
@@ -1,3 +1,3 @@
1
1
  module RailsHttpLab
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-http-lab
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jackson Pires