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,336 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--rhl-red: #CC0000;
|
|
3
|
+
--rhl-red-dark: #A30000;
|
|
4
|
+
--rhl-red-soft: #F4D6D6;
|
|
5
|
+
--rhl-cream: #FAF7F0;
|
|
6
|
+
--rhl-ink: #1A1A1A;
|
|
7
|
+
--rhl-muted: #6B6B6B;
|
|
8
|
+
--rhl-border: #E5E1D8;
|
|
9
|
+
--rhl-bg-alt: #F1ECE0;
|
|
10
|
+
--rhl-success: #2E7D32;
|
|
11
|
+
--rhl-warn: #E89B00;
|
|
12
|
+
--rhl-verb-get: #2E7D32;
|
|
13
|
+
--rhl-verb-post: #6A1B9A;
|
|
14
|
+
--rhl-verb-put: #1565C0;
|
|
15
|
+
--rhl-verb-patch: #00796B;
|
|
16
|
+
--rhl-verb-del: #C62828;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
* { box-sizing: border-box; }
|
|
20
|
+
[hidden] { display: none !important; }
|
|
21
|
+
|
|
22
|
+
html, body.rhl-body {
|
|
23
|
+
margin: 0; padding: 0; height: 100%;
|
|
24
|
+
background: var(--rhl-cream);
|
|
25
|
+
color: var(--rhl-ink);
|
|
26
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
27
|
+
font-size: 14px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.rhl-app { display: flex; flex-direction: column; height: 100vh; }
|
|
31
|
+
|
|
32
|
+
.rhl-topbar {
|
|
33
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
34
|
+
padding: 10px 16px;
|
|
35
|
+
background: var(--rhl-red);
|
|
36
|
+
color: white;
|
|
37
|
+
border-bottom: 2px solid var(--rhl-red-dark);
|
|
38
|
+
}
|
|
39
|
+
.rhl-topbar__brand { font-weight: 700; letter-spacing: 0.3px; }
|
|
40
|
+
.rhl-topbar__env { display: flex; align-items: center; gap: 8px; }
|
|
41
|
+
.rhl-topbar__env label { font-size: 12px; opacity: 0.9; }
|
|
42
|
+
.rhl-topbar__env select {
|
|
43
|
+
background: white; color: var(--rhl-ink);
|
|
44
|
+
border: none; padding: 4px 8px; border-radius: 4px;
|
|
45
|
+
}
|
|
46
|
+
.rhl-topbar__env .rhl-btn--ghost {
|
|
47
|
+
background: rgba(255,255,255,0.15); color: white; border-color: transparent;
|
|
48
|
+
}
|
|
49
|
+
.rhl-topbar__env .rhl-btn--ghost:hover { background: rgba(255,255,255,0.3); }
|
|
50
|
+
|
|
51
|
+
.rhl-main { display: flex; flex: 1; min-height: 0; }
|
|
52
|
+
|
|
53
|
+
.rhl-sidebar {
|
|
54
|
+
width: 280px; border-right: 1px solid var(--rhl-border);
|
|
55
|
+
background: var(--rhl-bg-alt); display: flex; flex-direction: column;
|
|
56
|
+
}
|
|
57
|
+
.rhl-sidebar__header {
|
|
58
|
+
padding: 12px 14px; font-weight: 600; border-bottom: 1px solid var(--rhl-border);
|
|
59
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
60
|
+
}
|
|
61
|
+
.rhl-sidebar__tree { padding: 8px 0; overflow-y: auto; flex: 1; font-size: 13px; }
|
|
62
|
+
.rhl-tree-empty { padding: 12px 14px; color: var(--rhl-muted); font-size: 12px; }
|
|
63
|
+
.rhl-tree-node { padding: 4px 14px; cursor: pointer; user-select: none; display: flex; gap: 8px; align-items: center; }
|
|
64
|
+
.rhl-tree-node:hover { background: var(--rhl-red-soft); }
|
|
65
|
+
.rhl-tree-node.is-active { background: var(--rhl-red-soft); }
|
|
66
|
+
.rhl-tree-node.is-folder { font-weight: 600; }
|
|
67
|
+
.rhl-tree-caret {
|
|
68
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
69
|
+
width: 16px; flex: 0 0 16px; font-size: 18px; line-height: 1;
|
|
70
|
+
color: var(--rhl-muted);
|
|
71
|
+
}
|
|
72
|
+
.rhl-tree-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; }
|
|
73
|
+
.rhl-dirty-dot { color: var(--rhl-warn); margin-left: 6px; font-size: 9px; vertical-align: middle; }
|
|
74
|
+
.rhl-tree-action {
|
|
75
|
+
background: transparent; border: none; cursor: pointer; color: var(--rhl-muted);
|
|
76
|
+
padding: 0 6px; font-size: 16px; line-height: 1; border-radius: 4px;
|
|
77
|
+
opacity: 0; flex: 0 0 auto;
|
|
78
|
+
}
|
|
79
|
+
.rhl-tree-node:hover .rhl-tree-action { opacity: 1; }
|
|
80
|
+
.rhl-tree-action:hover { background: white; color: var(--rhl-red); }
|
|
81
|
+
|
|
82
|
+
.rhl-menu {
|
|
83
|
+
position: fixed; z-index: 1100; background: white;
|
|
84
|
+
border: 1px solid var(--rhl-border); border-radius: 6px;
|
|
85
|
+
box-shadow: 0 6px 18px rgba(0,0,0,0.14); padding: 4px; min-width: 160px;
|
|
86
|
+
}
|
|
87
|
+
.rhl-menu__item {
|
|
88
|
+
display: block; width: 100%; text-align: left; background: transparent; border: none;
|
|
89
|
+
padding: 6px 10px; cursor: pointer; border-radius: 4px; color: var(--rhl-ink);
|
|
90
|
+
font: inherit;
|
|
91
|
+
}
|
|
92
|
+
.rhl-menu__item:hover { background: var(--rhl-red-soft); color: var(--rhl-red); }
|
|
93
|
+
.rhl-menu__item--danger { color: var(--rhl-red); }
|
|
94
|
+
.rhl-menu__item--danger:hover { background: var(--rhl-red); color: white; }
|
|
95
|
+
.rhl-verb-badge {
|
|
96
|
+
font-size: 10px; font-weight: 700; padding: 1px 4px; border-radius: 3px;
|
|
97
|
+
color: white; min-width: 40px; text-align: center;
|
|
98
|
+
}
|
|
99
|
+
.rhl-verb-badge.GET { background: var(--rhl-verb-get); }
|
|
100
|
+
.rhl-verb-badge.POST { background: var(--rhl-verb-post); }
|
|
101
|
+
.rhl-verb-badge.PUT { background: var(--rhl-verb-put); }
|
|
102
|
+
.rhl-verb-badge.PATCH { background: var(--rhl-verb-patch); }
|
|
103
|
+
.rhl-verb-badge.DELETE { background: var(--rhl-verb-del); }
|
|
104
|
+
.rhl-verb-badge.HEAD,
|
|
105
|
+
.rhl-verb-badge.OPTIONS { background: var(--rhl-muted); }
|
|
106
|
+
|
|
107
|
+
.rhl-pane { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
108
|
+
.rhl-pane__empty {
|
|
109
|
+
margin: auto; color: var(--rhl-muted); font-size: 15px;
|
|
110
|
+
max-width: 560px; padding: 24px; text-align: center; line-height: 1.5;
|
|
111
|
+
}
|
|
112
|
+
.rhl-pane__empty p { margin: 0 0 14px; }
|
|
113
|
+
.rhl-pane__empty p:last-child { margin-bottom: 0; }
|
|
114
|
+
.rhl-tip {
|
|
115
|
+
background: var(--rhl-bg-alt); border: 1px solid var(--rhl-border);
|
|
116
|
+
border-left: 3px solid var(--rhl-red); border-radius: 4px;
|
|
117
|
+
padding: 12px 14px; text-align: left; font-size: 13px; color: var(--rhl-ink);
|
|
118
|
+
}
|
|
119
|
+
.rhl-tip code {
|
|
120
|
+
background: white; padding: 1px 6px; border-radius: 3px;
|
|
121
|
+
border: 1px solid var(--rhl-border); font-size: 12px;
|
|
122
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
123
|
+
}
|
|
124
|
+
.rhl-pane__editor { display: flex; flex-direction: column; flex: 1; min-height: 0; padding: 16px; gap: 12px; }
|
|
125
|
+
|
|
126
|
+
.rhl-request-bar { display: flex; gap: 8px; align-items: stretch; }
|
|
127
|
+
.rhl-verb {
|
|
128
|
+
font-weight: 700; padding: 0 10px; border: 1px solid var(--rhl-border);
|
|
129
|
+
background: white; border-radius: 4px;
|
|
130
|
+
}
|
|
131
|
+
.rhl-url {
|
|
132
|
+
flex: 1; padding: 8px 10px; border: 1px solid var(--rhl-border);
|
|
133
|
+
border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
134
|
+
background: white;
|
|
135
|
+
}
|
|
136
|
+
.rhl-btn {
|
|
137
|
+
padding: 8px 14px; border: 1px solid var(--rhl-border);
|
|
138
|
+
background: white; cursor: pointer; border-radius: 4px; font-weight: 600;
|
|
139
|
+
}
|
|
140
|
+
.rhl-btn:hover { background: var(--rhl-bg-alt); }
|
|
141
|
+
.rhl-btn--send {
|
|
142
|
+
background: var(--rhl-red); color: white; border-color: var(--rhl-red-dark);
|
|
143
|
+
}
|
|
144
|
+
.rhl-btn--send:hover { background: var(--rhl-red-dark); }
|
|
145
|
+
.rhl-btn--ghost { background: transparent; border-color: transparent; color: var(--rhl-red); }
|
|
146
|
+
.rhl-btn--ghost:hover { background: var(--rhl-red-soft); }
|
|
147
|
+
#rhl-save.is-dirty::after { content: "●"; color: var(--rhl-warn); margin-left: 6px; font-size: 9px; vertical-align: middle; }
|
|
148
|
+
|
|
149
|
+
.rhl-iconbtn {
|
|
150
|
+
background: transparent; border: none; cursor: pointer; font-size: 16px;
|
|
151
|
+
color: var(--rhl-ink); padding: 2px 8px; border-radius: 4px;
|
|
152
|
+
}
|
|
153
|
+
.rhl-iconbtn:hover { background: var(--rhl-red-soft); }
|
|
154
|
+
|
|
155
|
+
.rhl-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--rhl-border); flex-shrink: 0; }
|
|
156
|
+
.rhl-tab {
|
|
157
|
+
background: transparent; border: none; padding: 10px 14px; cursor: pointer;
|
|
158
|
+
color: var(--rhl-muted); font-weight: 600; border-bottom: 2px solid transparent;
|
|
159
|
+
}
|
|
160
|
+
.rhl-tab.is-active { color: var(--rhl-red); border-bottom-color: var(--rhl-red); }
|
|
161
|
+
|
|
162
|
+
/* The split between request panel (top) and response (bottom), with a splitter. */
|
|
163
|
+
.rhl-split {
|
|
164
|
+
display: flex; flex-direction: column; flex: 1; min-height: 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.rhl-tab-panel {
|
|
168
|
+
flex: 1 1 0; min-height: 100px; padding: 12px 0;
|
|
169
|
+
overflow: auto;
|
|
170
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
171
|
+
font-size: 13px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.rhl-tab-head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px dashed var(--rhl-border); }
|
|
175
|
+
.rhl-tab-head__label { font-size: 12px; color: var(--rhl-muted); font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
|
176
|
+
.rhl-select { padding: 4px 8px; border: 1px solid var(--rhl-border); border-radius: 4px; background: white; }
|
|
177
|
+
|
|
178
|
+
.rhl-hint { padding: 10px 12px; color: var(--rhl-muted); background: var(--rhl-bg-alt); border-radius: 4px; }
|
|
179
|
+
.rhl-error { padding: 10px 12px; color: var(--rhl-red); background: var(--rhl-red-soft); border-radius: 4px; }
|
|
180
|
+
|
|
181
|
+
.rhl-kv-table { width: 100%; border-collapse: collapse; margin-bottom: 8px; }
|
|
182
|
+
.rhl-kv-table th, .rhl-kv-table td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--rhl-border); }
|
|
183
|
+
.rhl-kv-table th { font-size: 11px; color: var(--rhl-muted); text-transform: uppercase; letter-spacing: 0.4px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
|
184
|
+
.rhl-kv-table input { width: 100%; border: none; background: transparent; padding: 4px; font: inherit; }
|
|
185
|
+
.rhl-kv-table input:focus { outline: 1px solid var(--rhl-red); background: white; }
|
|
186
|
+
.rhl-kv-table__del { width: 36px; text-align: right; }
|
|
187
|
+
|
|
188
|
+
.rhl-textarea {
|
|
189
|
+
width: 100%; min-height: 220px; padding: 10px; border: 1px solid var(--rhl-border);
|
|
190
|
+
border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
191
|
+
font-size: 13px; background: white; color: var(--rhl-ink); display: block; resize: vertical;
|
|
192
|
+
}
|
|
193
|
+
.rhl-textarea--body { min-height: 280px; }
|
|
194
|
+
.rhl-textarea--readonly {
|
|
195
|
+
color: var(--rhl-muted);
|
|
196
|
+
background: var(--rhl-bg-alt);
|
|
197
|
+
cursor: not-allowed;
|
|
198
|
+
opacity: 0.75;
|
|
199
|
+
}
|
|
200
|
+
.rhl-textarea--readonly:focus { outline: 1px dashed var(--rhl-muted); }
|
|
201
|
+
.rhl-readonly-banner {
|
|
202
|
+
background: var(--rhl-bg-alt); border-left: 3px solid var(--rhl-muted);
|
|
203
|
+
color: var(--rhl-muted); padding: 8px 12px; border-radius: 4px;
|
|
204
|
+
margin-bottom: 10px; font-size: 12px; line-height: 1.4;
|
|
205
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.rhl-form-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
|
209
|
+
.rhl-form-row label { width: 200px; color: var(--rhl-muted); font-size: 12px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
|
210
|
+
.rhl-form-row input {
|
|
211
|
+
flex: 1; padding: 6px 8px; border: 1px solid var(--rhl-border); border-radius: 4px;
|
|
212
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; background: white;
|
|
213
|
+
}
|
|
214
|
+
.rhl-input-wide {
|
|
215
|
+
flex: 1; padding: 6px 8px; border: 1px solid var(--rhl-border); border-radius: 4px; background: white;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* Splitter handle */
|
|
219
|
+
.rhl-splitter {
|
|
220
|
+
height: 6px; cursor: row-resize; background: transparent; flex: 0 0 6px;
|
|
221
|
+
border-top: 1px solid var(--rhl-border); border-bottom: 1px solid var(--rhl-border);
|
|
222
|
+
position: relative;
|
|
223
|
+
}
|
|
224
|
+
.rhl-splitter:hover { background: var(--rhl-red-soft); }
|
|
225
|
+
.rhl-splitter::after {
|
|
226
|
+
content: ""; position: absolute; left: 50%; top: 2px; transform: translateX(-50%);
|
|
227
|
+
width: 40px; height: 0; border-top: 2px dotted var(--rhl-muted);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.rhl-response {
|
|
231
|
+
flex: 1 1 0;
|
|
232
|
+
display: flex; flex-direction: column; gap: 8px; min-height: 120px; padding-top: 8px;
|
|
233
|
+
}
|
|
234
|
+
.rhl-response__header {
|
|
235
|
+
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
236
|
+
border-bottom: 1px solid var(--rhl-border);
|
|
237
|
+
}
|
|
238
|
+
.rhl-response__tabs { display: flex; gap: 0; }
|
|
239
|
+
.rhl-response-tab {
|
|
240
|
+
background: transparent; border: none; padding: 6px 12px; cursor: pointer;
|
|
241
|
+
color: var(--rhl-muted); font-weight: 600; border-bottom: 2px solid transparent;
|
|
242
|
+
font-size: 13px; display: inline-flex; align-items: center; gap: 6px;
|
|
243
|
+
}
|
|
244
|
+
.rhl-response-tab.is-active { color: var(--rhl-red); border-bottom-color: var(--rhl-red); }
|
|
245
|
+
.rhl-response-tab__count {
|
|
246
|
+
background: var(--rhl-bg-alt); color: var(--rhl-muted);
|
|
247
|
+
font-size: 10px; font-weight: 700; padding: 1px 6px; border-radius: 8px;
|
|
248
|
+
min-width: 18px; text-align: center;
|
|
249
|
+
}
|
|
250
|
+
.rhl-response-tab.is-active .rhl-response-tab__count {
|
|
251
|
+
background: var(--rhl-red-soft); color: var(--rhl-red);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.rhl-response__meta { display: flex; gap: 8px; font-size: 12px; align-items: center; }
|
|
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-pill { font-weight: 700; }
|
|
257
|
+
.rhl-pill--ok { background: #DCEEDC !important; color: var(--rhl-success); }
|
|
258
|
+
.rhl-pill--warn { background: #FFE9C7 !important; color: var(--rhl-warn); }
|
|
259
|
+
.rhl-pill--err { background: #F4D6D6 !important; color: var(--rhl-red); }
|
|
260
|
+
.rhl-pill--info { background: var(--rhl-bg-alt) !important; }
|
|
261
|
+
|
|
262
|
+
.rhl-response__body {
|
|
263
|
+
margin: 0; padding: 10px; background: #1A1A1A; color: #F1ECE0;
|
|
264
|
+
border-radius: 4px; overflow: auto; flex: 1;
|
|
265
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
|
|
266
|
+
white-space: pre-wrap; word-break: break-word;
|
|
267
|
+
}
|
|
268
|
+
.rhl-response__body--err { color: #FFB4B4; }
|
|
269
|
+
|
|
270
|
+
/* JSON syntax highlighting tokens (used inside .rhl-response__body) */
|
|
271
|
+
.rhl-tok--key { color: #82AAFF; }
|
|
272
|
+
.rhl-tok--str { color: #C3E88D; }
|
|
273
|
+
.rhl-tok--num { color: #F78C6C; }
|
|
274
|
+
.rhl-tok--bool { color: #C792EA; }
|
|
275
|
+
.rhl-tok--null { color: #FF6B6B; font-style: italic; }
|
|
276
|
+
|
|
277
|
+
.rhl-response__headers {
|
|
278
|
+
flex: 1; overflow: auto; background: white;
|
|
279
|
+
border: 1px solid var(--rhl-border); border-radius: 4px;
|
|
280
|
+
}
|
|
281
|
+
.rhl-headers-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
282
|
+
.rhl-headers-table th, .rhl-headers-table td {
|
|
283
|
+
text-align: left; padding: 6px 10px; border-bottom: 1px solid var(--rhl-border);
|
|
284
|
+
vertical-align: top;
|
|
285
|
+
}
|
|
286
|
+
.rhl-headers-table th {
|
|
287
|
+
font-size: 11px; color: var(--rhl-muted); text-transform: uppercase; letter-spacing: 0.4px;
|
|
288
|
+
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
|
289
|
+
background: var(--rhl-bg-alt); position: sticky; top: 0;
|
|
290
|
+
}
|
|
291
|
+
.rhl-headers-table__name {
|
|
292
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
293
|
+
color: var(--rhl-ink); white-space: nowrap; width: 1%;
|
|
294
|
+
}
|
|
295
|
+
.rhl-headers-table__value {
|
|
296
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
297
|
+
color: var(--rhl-ink); word-break: break-all;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* Modal */
|
|
301
|
+
.rhl-modal { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; }
|
|
302
|
+
.rhl-modal__backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.4); }
|
|
303
|
+
.rhl-modal__dialog {
|
|
304
|
+
position: relative; background: var(--rhl-cream); border: 1px solid var(--rhl-border);
|
|
305
|
+
border-radius: 8px; width: min(900px, 92vw); height: min(600px, 80vh); display: flex; flex-direction: column;
|
|
306
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
|
|
307
|
+
}
|
|
308
|
+
.rhl-modal__header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--rhl-border); }
|
|
309
|
+
.rhl-modal__header h2 { margin: 0; font-size: 16px; }
|
|
310
|
+
.rhl-modal__body { display: flex; flex: 1; min-height: 0; }
|
|
311
|
+
.rhl-envs-list { width: 220px; border-right: 1px solid var(--rhl-border); padding: 8px 0; overflow-y: auto; }
|
|
312
|
+
.rhl-envs-list__item { padding: 8px 14px; cursor: pointer; }
|
|
313
|
+
.rhl-envs-list__item:hover { background: var(--rhl-red-soft); }
|
|
314
|
+
.rhl-envs-editor { flex: 1; padding: 14px 18px; overflow-y: auto; }
|
|
315
|
+
.rhl-envs-editor h3 { margin: 0 0 12px; }
|
|
316
|
+
.rhl-envs-editor__footer { margin-top: 12px; display: flex; gap: 8px; }
|
|
317
|
+
|
|
318
|
+
.rhl-modal__footer {
|
|
319
|
+
padding: 10px 16px;
|
|
320
|
+
border-top: 1px solid var(--rhl-border);
|
|
321
|
+
background: var(--rhl-bg-alt);
|
|
322
|
+
color: var(--rhl-muted);
|
|
323
|
+
font-size: 12px;
|
|
324
|
+
line-height: 1.5;
|
|
325
|
+
border-bottom-left-radius: 8px;
|
|
326
|
+
border-bottom-right-radius: 8px;
|
|
327
|
+
}
|
|
328
|
+
.rhl-modal__footer code {
|
|
329
|
+
background: white;
|
|
330
|
+
border: 1px solid var(--rhl-border);
|
|
331
|
+
border-radius: 3px;
|
|
332
|
+
padding: 1px 5px;
|
|
333
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
334
|
+
font-size: 11px;
|
|
335
|
+
color: var(--rhl-ink);
|
|
336
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module RailsHttpLab
|
|
2
|
+
class ApplicationController < ActionController::Base
|
|
3
|
+
protect_from_forgery with: :exception, prepend: true
|
|
4
|
+
layout "rails_http_lab"
|
|
5
|
+
|
|
6
|
+
before_action :guard_environment!
|
|
7
|
+
|
|
8
|
+
rescue_from RailsHttpLab::NotFoundError, with: :render_not_found
|
|
9
|
+
rescue_from RailsHttpLab::OutsideStorageError, with: :render_forbidden
|
|
10
|
+
rescue_from RailsHttpLab::ParseError, with: :render_unprocessable
|
|
11
|
+
rescue_from RailsHttpLab::Error, with: :render_unprocessable
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def guard_environment!
|
|
16
|
+
cfg = RailsHttpLab.config
|
|
17
|
+
env_sym = Rails.env.to_sym
|
|
18
|
+
unless cfg.enabled_envs.include?(env_sym)
|
|
19
|
+
head :not_found and return
|
|
20
|
+
end
|
|
21
|
+
if cfg.authenticator && !cfg.authenticator.call(request)
|
|
22
|
+
head :forbidden and return
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render_not_found(error)
|
|
27
|
+
respond_to do |format|
|
|
28
|
+
format.json { render json: { error: error.message }, status: :not_found }
|
|
29
|
+
format.any { head :not_found }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def render_forbidden(error)
|
|
34
|
+
respond_to do |format|
|
|
35
|
+
format.json { render json: { error: error.message }, status: :forbidden }
|
|
36
|
+
format.any { head :forbidden }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def render_unprocessable(error)
|
|
41
|
+
respond_to do |format|
|
|
42
|
+
format.json { render json: { error: error.message }, status: :unprocessable_entity }
|
|
43
|
+
format.any { head :unprocessable_entity }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module RailsHttpLab
|
|
2
|
+
class CollectionsController < ApplicationController
|
|
3
|
+
skip_forgery_protection only: [:create]
|
|
4
|
+
|
|
5
|
+
def tree
|
|
6
|
+
RailsHttpLab::Storage::Filesystem.new.ensure_root!
|
|
7
|
+
render json: RailsHttpLab::Storage::Tree.new.build
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def create
|
|
11
|
+
name = params.require(:name).to_s
|
|
12
|
+
fs = RailsHttpLab::Storage::Filesystem.new
|
|
13
|
+
fs.ensure_root!
|
|
14
|
+
manifest = fs.read_bruno_json || {}
|
|
15
|
+
manifest["name"] = name if manifest["name"].to_s.empty?
|
|
16
|
+
File.write(File.join(fs.root.to_s, "bruno.json"), JSON.pretty_generate(manifest) + "\n")
|
|
17
|
+
render json: { ok: true, name: manifest["name"] }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module RailsHttpLab
|
|
2
|
+
class EnvironmentsController < ApplicationController
|
|
3
|
+
skip_forgery_protection only: [:update]
|
|
4
|
+
|
|
5
|
+
def index
|
|
6
|
+
render json: { environments: RailsHttpLab::Storage::Tree.new.build[:environments] }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def show
|
|
10
|
+
doc = storage.read_bru("environments/#{params[:name]}.bru")
|
|
11
|
+
vars = doc.block("vars")&.pairs || []
|
|
12
|
+
render json: { name: params[:name], vars: vars }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def update
|
|
16
|
+
pairs = Array(params[:vars]).map { |v|
|
|
17
|
+
v = v.to_unsafe_h if v.respond_to?(:to_unsafe_h)
|
|
18
|
+
[v["key"].to_s, v["value"].to_s]
|
|
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)
|
|
24
|
+
render json: { ok: true }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def storage
|
|
30
|
+
@storage ||= RailsHttpLab::Storage::Filesystem.new
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module RailsHttpLab
|
|
2
|
+
class FoldersController < ApplicationController
|
|
3
|
+
skip_forgery_protection only: [:create, :rename, :destroy]
|
|
4
|
+
|
|
5
|
+
def create
|
|
6
|
+
path = params.require(:path).to_s
|
|
7
|
+
RailsHttpLab::Storage::Filesystem.new.create_folder(path, display_name: params[:name])
|
|
8
|
+
render json: { ok: true, path: path }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def rename
|
|
12
|
+
from = params.require(:path).to_s
|
|
13
|
+
new_name = params.require(:name).to_s.strip
|
|
14
|
+
raise RailsHttpLab::Error, "name cannot be empty" if new_name.empty?
|
|
15
|
+
raise RailsHttpLab::Error, "name cannot contain '/' or '\\'" if new_name.match?(%r{[/\\]})
|
|
16
|
+
|
|
17
|
+
parent = File.dirname(from)
|
|
18
|
+
parent = "" if parent == "."
|
|
19
|
+
to = parent.empty? ? new_name : "#{parent}/#{new_name}"
|
|
20
|
+
|
|
21
|
+
fs = RailsHttpLab::Storage::Filesystem.new
|
|
22
|
+
fs.rename(from, to)
|
|
23
|
+
update_folder_meta_name(fs, to, new_name)
|
|
24
|
+
|
|
25
|
+
render json: { ok: true, path: to }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def destroy
|
|
29
|
+
path = params.require(:path).to_s
|
|
30
|
+
RailsHttpLab::Storage::Filesystem.new.delete(path)
|
|
31
|
+
head :no_content
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def update_folder_meta_name(fs, folder_path, new_name)
|
|
37
|
+
meta_path = "#{folder_path}/folder.bru"
|
|
38
|
+
doc = fs.read_bru(meta_path)
|
|
39
|
+
meta = doc.block("meta")
|
|
40
|
+
return unless meta&.kv?
|
|
41
|
+
meta["name"] = new_name
|
|
42
|
+
fs.write_bru(meta_path, doc)
|
|
43
|
+
rescue RailsHttpLab::NotFoundError
|
|
44
|
+
# folder had no folder.bru — nothing to update
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module RailsHttpLab
|
|
2
|
+
class RequestsController < ApplicationController
|
|
3
|
+
skip_forgery_protection only: [:create, :update, :destroy, :rename]
|
|
4
|
+
|
|
5
|
+
def show
|
|
6
|
+
doc = storage.read_bru(path_param)
|
|
7
|
+
render json: serialize(doc, path_param)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def update
|
|
11
|
+
doc = doc_from_params
|
|
12
|
+
storage.write_bru(path_param, doc)
|
|
13
|
+
render json: serialize(doc, path_param)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create
|
|
17
|
+
rel = params.require(:path).to_s
|
|
18
|
+
doc = doc_from_params
|
|
19
|
+
storage.write_bru(rel, doc)
|
|
20
|
+
render json: serialize(doc, rel)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def destroy
|
|
24
|
+
storage.delete(path_param)
|
|
25
|
+
head :no_content
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def rename
|
|
29
|
+
from = params.require(:path).to_s
|
|
30
|
+
new_name = params.require(:name).to_s.strip
|
|
31
|
+
raise RailsHttpLab::Error, "name cannot be empty" if new_name.empty?
|
|
32
|
+
raise RailsHttpLab::Error, "name cannot contain '/' or '\\'" if new_name.match?(%r{[/\\]})
|
|
33
|
+
|
|
34
|
+
base = new_name.sub(/\.bru\z/i, "")
|
|
35
|
+
parent = File.dirname(from)
|
|
36
|
+
parent = "" if parent == "."
|
|
37
|
+
to = parent.empty? ? "#{base}.bru" : "#{parent}/#{base}.bru"
|
|
38
|
+
|
|
39
|
+
storage.rename(from, to)
|
|
40
|
+
doc = storage.read_bru(to)
|
|
41
|
+
meta = doc.block("meta")
|
|
42
|
+
if meta&.kv?
|
|
43
|
+
meta["name"] = base
|
|
44
|
+
storage.write_bru(to, doc)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
render json: serialize(doc, to)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def storage
|
|
53
|
+
@storage ||= RailsHttpLab::Storage::Filesystem.new
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def path_param
|
|
57
|
+
params[:path].to_s
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def doc_from_params
|
|
61
|
+
raw = params[:source].to_s
|
|
62
|
+
return RailsHttpLab::Bruno.parse(raw) unless raw.empty?
|
|
63
|
+
|
|
64
|
+
RailsHttpLab::Bruno.parse(blocks_to_source(params[:blocks]))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Accepts an array of { name, mode, pairs|raw } and rebuilds a Document.
|
|
68
|
+
def blocks_to_source(blocks)
|
|
69
|
+
blocks = blocks.respond_to?(:to_unsafe_h) ? blocks.to_unsafe_h.values : blocks
|
|
70
|
+
docs = []
|
|
71
|
+
Array(blocks).each do |b|
|
|
72
|
+
b = b.to_unsafe_h if b.respond_to?(:to_unsafe_h)
|
|
73
|
+
mode = b["mode"]&.to_sym || :kv
|
|
74
|
+
if mode == :raw
|
|
75
|
+
docs << "#{b['name']} {\n#{b['raw']}\n}\n"
|
|
76
|
+
else
|
|
77
|
+
lines = Array(b["pairs"]).map { |k, v| " #{k}: #{v}" }.join("\n")
|
|
78
|
+
docs << "#{b['name']} {\n#{lines}#{lines.empty? ? '' : "\n"}}\n"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
docs.join("\n")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def serialize(doc, rel)
|
|
85
|
+
{
|
|
86
|
+
path: rel,
|
|
87
|
+
method: doc.http_method,
|
|
88
|
+
url: doc.url,
|
|
89
|
+
blocks: doc.blocks.map { |b|
|
|
90
|
+
if b.kv?
|
|
91
|
+
{ name: b.name, mode: "kv", pairs: b.pairs }
|
|
92
|
+
else
|
|
93
|
+
{ name: b.name, mode: "raw", raw: b.raw }
|
|
94
|
+
end
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module RailsHttpLab
|
|
2
|
+
class RunsController < ApplicationController
|
|
3
|
+
skip_forgery_protection only: [:create]
|
|
4
|
+
|
|
5
|
+
def create
|
|
6
|
+
doc = build_document
|
|
7
|
+
resolver = build_resolver
|
|
8
|
+
response = RailsHttpLab::Execution::Runner.new(doc, resolver: resolver).run
|
|
9
|
+
render json: response.to_h
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def build_document
|
|
15
|
+
if params[:path].present?
|
|
16
|
+
RailsHttpLab::Storage::Filesystem.new.read_bru(params[:path])
|
|
17
|
+
elsif params[:source].present?
|
|
18
|
+
RailsHttpLab::Bruno.parse(params[:source].to_s)
|
|
19
|
+
else
|
|
20
|
+
raise RailsHttpLab::Error, "missing :path or :source"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def build_resolver
|
|
25
|
+
env_name = params[:environment].to_s
|
|
26
|
+
return RailsHttpLab::Execution::VariableResolver.new if env_name.empty?
|
|
27
|
+
|
|
28
|
+
env_doc = RailsHttpLab::Storage::Filesystem.new.read_bru("environments/#{env_name}.bru")
|
|
29
|
+
RailsHttpLab::Execution::VariableResolver.from_environment_document(env_doc)
|
|
30
|
+
rescue RailsHttpLab::NotFoundError
|
|
31
|
+
RailsHttpLab::Execution::VariableResolver.new
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Rails HTTP Lab</title>
|
|
5
|
+
<meta name="rhl-base" content="<%= request.script_name %>">
|
|
6
|
+
<%= csrf_meta_tags %>
|
|
7
|
+
<%= csp_meta_tag if respond_to?(:csp_meta_tag) %>
|
|
8
|
+
<%= stylesheet_link_tag "rails_http_lab/application", media: "all" %>
|
|
9
|
+
</head>
|
|
10
|
+
<body class="rhl-body">
|
|
11
|
+
<%= yield %>
|
|
12
|
+
<%= javascript_include_tag "rails_http_lab/application", defer: true %>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|