gromer
git clone https://git.pyrossh.dev/gromer
gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.
9e6f931
— Peter John
2021-11-20T00:01:30+05:30
improve api explorer
Generated files not shown: go.sum
- api_explorer.go +50 -32
- cmd/wapp/main.go +18 -9
- go.mod +34 -1
- html.go +20 -7
api_explorer.go
CHANGED
|
@@ -54,16 +54,19 @@ func ApiExplorer(apiDefs []ApiDefinition) func(c context.Context) (HtmlPage, int
|
|
|
54
54
|
Meta("author", "pyros2097"),
|
|
55
55
|
Meta("keywords", "wapp,pyros2097"),
|
|
56
56
|
Meta("viewport", "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover"),
|
|
57
|
-
Link("icon", "/
|
|
57
|
+
Link("icon", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAMFBMVEU0OkArMjhobHEoPUPFEBIuO0L+AAC2FBZ2JyuNICOfGx7xAwTjCAlCNTvVDA1aLzQ3COjMAAAAVUlEQVQI12NgwAaCDSA0888GCItjn0szWGBJTVoGSCjWs8TleQCQYV95evdxkFT8Kpe0PLDi5WfKd4LUsN5zS1sKFolt8bwAZrCaGqNYJAgFDEpQAAAzmxafI4vZWwAAAABJRU5ErkJggg=="),
|
|
58
58
|
Link("stylesheet", "https://cdn.jsdelivr.net/npm/[email protected]/lib/codemirror.css"),
|
|
59
59
|
StyleTag(Text(`
|
|
60
60
|
html, body {
|
|
61
61
|
height: 100vh;
|
|
62
|
-
overflow: hidden;
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
.CodeMirror {
|
|
64
|
+
#left .CodeMirror {
|
|
66
|
-
height:
|
|
65
|
+
height: 400px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#right .CodeMirror {
|
|
69
|
+
height: calc(100vh - 60px);
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
.form-select {
|
|
@@ -93,9 +96,17 @@ func ApiExplorer(apiDefs []ApiDefinition) func(c context.Context) (HtmlPage, int
|
|
|
93
96
|
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
|
|
94
97
|
border-color: #63b3ed;
|
|
95
98
|
}
|
|
99
|
+
|
|
100
|
+
table {
|
|
101
|
+
width: 100%;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
tr {
|
|
105
|
+
width: 100%;
|
|
106
|
+
}
|
|
96
107
|
|
|
97
108
|
td, th {
|
|
98
|
-
|
|
109
|
+
border-bottom: 1px solid rgb(204, 204, 204);
|
|
99
110
|
border-left: 1px solid rgb(204, 204, 204);
|
|
100
111
|
text-align: left;
|
|
101
112
|
}
|
|
@@ -149,7 +160,7 @@ func ApiExplorer(apiDefs []ApiDefinition) func(c context.Context) (HtmlPage, int
|
|
|
149
160
|
Script(Src("https://cdn.jsdelivr.net/npm/[email protected]/mode/javascript/javascript.js")),
|
|
150
161
|
),
|
|
151
162
|
Body(
|
|
152
|
-
Div(Css("flex flex-col
|
|
163
|
+
Div(Css("flex flex-col"),
|
|
153
164
|
Div(Css("flex w-full p-2 bg-gray-50 border-b border-gray-200 items-center justify-start"),
|
|
154
165
|
Div(Css("flex mr-4 text-gray-700 text-2xl font-bold"), Text("API Explorer")),
|
|
155
166
|
Div(Css("text-xl"),
|
|
@@ -159,37 +170,43 @@ func ApiExplorer(apiDefs []ApiDefinition) func(c context.Context) (HtmlPage, int
|
|
|
159
170
|
Text("RUN"),
|
|
160
171
|
)),
|
|
161
172
|
),
|
|
162
|
-
Div(Css("flex
|
|
173
|
+
Div(Css("flex"),
|
|
163
|
-
Div(Css("
|
|
174
|
+
Div(Css("flex flex-row"), Style("width: 50%;"),
|
|
175
|
+
Div(Css("pr-8 border-r border-gray-300"), Style("background: #f7f7f7;")),
|
|
176
|
+
Div(Css("w-full"),
|
|
164
|
-
|
|
177
|
+
Section("Headers"),
|
|
165
|
-
|
|
178
|
+
Table(ID("headersTable"),
|
|
166
|
-
|
|
179
|
+
TR(
|
|
167
|
-
|
|
180
|
+
TD(Input(Css("w-full p-1"), Attr("value", "Authorization"))),
|
|
168
|
-
|
|
181
|
+
TD(Input(Css("w-full p-1"))),
|
|
182
|
+
),
|
|
169
183
|
),
|
|
184
|
+
Section("Path Params"),
|
|
185
|
+
Table(ID("pathParamsTable"),
|
|
186
|
+
TR(
|
|
187
|
+
TD(Css("text-gray-700"), Style("width: 50%;"), Div(Css("p-1"), Text("123"))),
|
|
188
|
+
TD(Style("width: 50%;"), Input(Css("w-full p-1"))),
|
|
170
|
-
|
|
189
|
+
),
|
|
171
|
-
Section("Path Params"),
|
|
172
|
-
Table(ID("pathParamsTable"), Css("w-full"),
|
|
173
|
-
TR(
|
|
174
|
-
TD(Css("text-gray-700"), Style("width: 50%;"), Div(Css("p-1"), Text("123"))),
|
|
175
|
-
TD(Style("width: 50%;"), Input(Css("w-full p-1"))),
|
|
176
190
|
),
|
|
191
|
+
Section("Query Params"),
|
|
192
|
+
Table(ID("queryParamsTable"),
|
|
193
|
+
TR(
|
|
194
|
+
TD(Css("text-gray-700"), Style("width: 50%;"), Div(Css("p-1"), Text("123"))),
|
|
195
|
+
TD(Style("width: 50%;"), Input(Css("w-full p-1"))),
|
|
177
|
-
|
|
196
|
+
),
|
|
178
|
-
Section("Query Params"),
|
|
179
|
-
Table(ID("queryParamsTable"), Css("w-full"),
|
|
180
|
-
TR(
|
|
181
|
-
TD(Css("text-gray-700"), Style("width: 50%;"), Div(Css("p-1"), Text("123"))),
|
|
182
|
-
TD(Style("width: 50%;"), Input(Css("w-full p-1"))),
|
|
183
197
|
),
|
|
198
|
+
Section("Body"),
|
|
199
|
+
Div(ID("left"), Css("border-b border-gray-200 text-md")),
|
|
184
200
|
),
|
|
185
|
-
Section("Body"),
|
|
186
|
-
Div(ID("left"), Style("height: 400px; overflow: scroll;"), Css("border-b border-gray-200 text-lg")),
|
|
187
201
|
),
|
|
202
|
+
Div(Css("flex flex-row"), Style("width: 50%;"),
|
|
188
|
-
|
|
203
|
+
Div(ID("right"), Css("w-full border-l border-l-gray-200 text-md")),
|
|
204
|
+
Div(Css("pr-8 border-l border-gray-300"), Style("background: #f7f7f7;")),
|
|
205
|
+
),
|
|
189
206
|
),
|
|
190
207
|
),
|
|
191
208
|
Script(Text("window.apiDefs = "+string(data))),
|
|
192
|
-
Script(Text("window.codeLeft = CodeMirror(document.getElementById('left'), {value: '{}',mode: 'javascript'
|
|
209
|
+
Script(Text("window.codeLeft = CodeMirror(document.getElementById('left'), {value: '{}',mode: 'javascript' })")),
|
|
193
210
|
Script(Text("window.codeRight = CodeMirror(document.getElementById('right'), {value: '',mode: 'javascript', lineNumbers: true, readOnly: true, lineWrapping: true })")),
|
|
194
211
|
Script(Text(`
|
|
195
212
|
const getCurrentApiCall = () => {
|
|
@@ -208,8 +225,9 @@ func ApiExplorer(apiDefs []ApiDefinition) func(c context.Context) (HtmlPage, int
|
|
|
208
225
|
const row = table.insertRow(0);
|
|
209
226
|
const cell1 = row.insertCell(0);
|
|
210
227
|
const cell2 = row.insertCell(1);
|
|
211
|
-
cell1.style = "width:
|
|
228
|
+
cell1.style = "width: 30%; border-left: 0px;";
|
|
212
229
|
cell1.class = "text-gray-700";
|
|
230
|
+
cell2.style = "width: 70%;";
|
|
213
231
|
cell1.innerHTML = "<div class='p-1'>" + param + "</div>";
|
|
214
232
|
cell2.innerHTML = "<input id='path-param-" + param + "' class='w-full p-1'>";
|
|
215
233
|
}
|
|
@@ -241,8 +259,8 @@ func ApiExplorer(apiDefs []ApiDefinition) func(c context.Context) (HtmlPage, int
|
|
|
241
259
|
const row = table.insertRow(0);
|
|
242
260
|
const cell1 = row.insertCell(0);
|
|
243
261
|
const cell2 = row.insertCell(1);
|
|
244
|
-
cell1.style = "width:
|
|
262
|
+
cell1.style = "width: 30%; border-left: 0px;";
|
|
245
|
-
cell2.style = "width:
|
|
263
|
+
cell2.style = "width: 70%;";
|
|
246
264
|
cell1.innerHTML = "<input value='" + key + "' class='w-full p-1'>";
|
|
247
265
|
cell2.innerHTML = "<input value='" + value + "' class='w-full p-1'>";
|
|
248
266
|
}
|
cmd/wapp/main.go
CHANGED
|
@@ -220,6 +220,7 @@ import (
|
|
|
220
220
|
"github.com/gorilla/mux"
|
|
221
221
|
"github.com/pyros2097/wapp"
|
|
222
222
|
"github.com/rs/zerolog/log"
|
|
223
|
+
"gocloud.dev/server"
|
|
223
224
|
|
|
224
225
|
"{{ moduleName }}/context"
|
|
225
226
|
{{#each allPkgs }}"{{ moduleName }}/pages{{ @key }}"
|
|
@@ -233,25 +234,31 @@ func main() {
|
|
|
233
234
|
isLambda := os.Getenv("_LAMBDA_SERVER_PORT") != ""
|
|
234
235
|
r := mux.NewRouter()
|
|
235
236
|
r.NotFoundHandler = http.HandlerFunc(notFound)
|
|
236
|
-
r.PathPrefix("/assets/").Handler(http.FileServer(http.FS(assetsFS)))
|
|
237
|
+
r.PathPrefix("/assets/").Handler(wrapCache(http.FileServer(http.FS(assetsFS))))
|
|
237
238
|
handle(r, "GET", "/api", wapp.ApiExplorer(apiDefinitions()))
|
|
238
239
|
{{#each routes as |route| }}handle(r, "{{ route.Method }}", "{{ route.Path }}", {{ route.Pkg }}.{{ route.Method }})
|
|
239
240
|
{{/each}}
|
|
240
241
|
if !isLambda {
|
|
241
242
|
println("http server listening on http://localhost:3000")
|
|
242
|
-
srv :=
|
|
243
|
+
srv := server.New(http.DefaultServeMux, nil)
|
|
243
|
-
Handler: r,
|
|
244
|
-
|
|
244
|
+
if err := srv.ListenAndServe(":3000"); err != nil {
|
|
245
|
-
|
|
245
|
+
log.Fatal().Stack().Err(err).Msg("failed to listen")
|
|
246
|
-
ReadTimeout: 30 * time.Second,
|
|
247
246
|
}
|
|
248
|
-
log.Fatal().Stack().Err(srv.ListenAndServe()).Msg("failed to listen")
|
|
249
247
|
} else {
|
|
250
248
|
log.Print("running in lambda mode")
|
|
249
|
+
if err := gateway.ListenAndServe(":3000", r); err != nil {
|
|
251
|
-
|
|
250
|
+
log.Fatal().Stack().Err(err).Msg("failed to listen")
|
|
251
|
+
}
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
func wrapCache(h http.Handler) http.Handler {
|
|
256
|
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
257
|
+
w.Header().Set("Cache-Control", "public, max-age=2592000")
|
|
258
|
+
h.ServeHTTP(w, r)
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
255
262
|
func notFound(w http.ResponseWriter, r *http.Request) {
|
|
256
263
|
wapp.LogReq(404, r)
|
|
257
264
|
}
|
|
@@ -263,7 +270,9 @@ func handle(router *mux.Router, method, route string, h interface{}) {
|
|
|
263
270
|
wapp.LogReq(status, r)
|
|
264
271
|
}()
|
|
265
272
|
ctx, err := context.WithContext(c.WithValue(
|
|
273
|
+
c.WithValue(
|
|
266
|
-
|
|
274
|
+
c.WithValue(r.Context(), "assetsFS", assetsFS),
|
|
275
|
+
"url", r.URL),
|
|
267
276
|
"header", r.Header))
|
|
268
277
|
if err != nil {
|
|
269
278
|
wapp.RespondError(w, 500, err)
|
go.mod
CHANGED
|
@@ -3,14 +3,38 @@ module github.com/pyros2097/wapp
|
|
|
3
3
|
go 1.16
|
|
4
4
|
|
|
5
5
|
require (
|
|
6
|
+
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999 // indirect
|
|
6
7
|
github.com/aymerick/raymond v2.0.2+incompatible // indirect
|
|
8
|
+
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect
|
|
7
9
|
github.com/bradleyjkemp/cupaloy v2.3.0+incompatible
|
|
10
|
+
github.com/coreos/bbolt v1.3.1-coreos.6 // indirect
|
|
11
|
+
github.com/coreos/etcd v3.3.10+incompatible // indirect
|
|
12
|
+
github.com/coreos/go-semver v0.2.0 // indirect
|
|
13
|
+
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d // indirect
|
|
14
|
+
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect
|
|
15
|
+
github.com/dnaeon/go-vcr v1.0.1 // indirect
|
|
8
16
|
github.com/fatih/color v1.13.0
|
|
9
17
|
github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf
|
|
18
|
+
github.com/go-ini/ini v1.39.0 // indirect
|
|
10
19
|
github.com/gobuffalo/velvet v0.0.0-20170320144106-d97471bf5d8f
|
|
20
|
+
github.com/gogo/protobuf v1.1.1 // indirect
|
|
21
|
+
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 // indirect
|
|
22
|
+
github.com/googleapis/gax-go v2.0.0+incompatible // indirect
|
|
23
|
+
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
|
|
24
|
+
github.com/gorilla/context v1.1.1 // indirect
|
|
11
25
|
github.com/gorilla/mux v1.8.0
|
|
26
|
+
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 // indirect
|
|
27
|
+
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
|
28
|
+
github.com/jonboulle/clockwork v0.1.0 // indirect
|
|
29
|
+
github.com/jtolds/gls v4.2.1+incompatible // indirect
|
|
30
|
+
github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
|
|
12
31
|
github.com/markbates/inflect v1.0.4 // indirect
|
|
32
|
+
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
|
13
33
|
github.com/microcosm-cc/bluemonday v1.0.15 // indirect
|
|
34
|
+
github.com/openzipkin/zipkin-go v0.1.1 // indirect
|
|
35
|
+
github.com/prometheus/client_golang v0.9.0 // indirect
|
|
36
|
+
github.com/prometheus/common v0.0.0-20181015124227-bcb74de08d37 // indirect
|
|
37
|
+
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d // indirect
|
|
14
38
|
github.com/rs/zerolog v1.26.0
|
|
15
39
|
github.com/sergi/go-diff v1.2.0 // indirect
|
|
16
40
|
github.com/shurcooL/github_flavored_markdown v0.0.0-20210228213109-c3a9aa474629 // indirect
|
|
@@ -20,10 +44,19 @@ require (
|
|
|
20
44
|
github.com/shurcooL/highlight_go v0.0.0-20191220051317-782971ddf21b // indirect
|
|
21
45
|
github.com/shurcooL/octicon v0.0.0-20191102190552-cbb32d6a785c // indirect
|
|
22
46
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
|
47
|
+
github.com/sirupsen/logrus v1.1.1 // indirect
|
|
48
|
+
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
|
|
49
|
+
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect
|
|
50
|
+
github.com/soheilhy/cmux v0.1.4 // indirect
|
|
23
51
|
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect
|
|
24
52
|
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect
|
|
53
|
+
github.com/streadway/amqp v0.0.0-20181107104731-27835f1a64e9 // indirect
|
|
25
54
|
github.com/stretchr/testify v1.7.0 // indirect
|
|
55
|
+
github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 // indirect
|
|
56
|
+
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect
|
|
57
|
+
gocloud.dev v0.24.0 // indirect
|
|
26
58
|
golang.org/x/mod v0.5.1
|
|
27
59
|
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf
|
|
60
|
+
gopkg.in/ini.v1 v1.39.0 // indirect
|
|
28
|
-
gopkg.in/
|
|
61
|
+
gopkg.in/pipe.v2 v2.0.0-20140414041502-3c2ca4d52544 // indirect
|
|
29
62
|
)
|
html.go
CHANGED
|
@@ -92,10 +92,10 @@ func (p *HtmlPage) WriteHtml(w io.Writer) {
|
|
|
92
92
|
p.computeCss(p.Body.children)
|
|
93
93
|
p.Head.children = append(p.Head.children, StyleTag(Text(normalizeStyles+p.css.String())))
|
|
94
94
|
p.Head.writeHtmlIndent(w, 1)
|
|
95
|
-
p.Body.children = append(p.Body.children, Script(Text(fmt.Sprintf(`
|
|
95
|
+
// p.Body.children = append(p.Body.children, Script(Text(fmt.Sprintf(`
|
|
96
|
-
|
|
96
|
+
// document.addEventListener('alpine:init', () => {%s
|
|
97
|
-
|
|
97
|
+
// });
|
|
98
|
-
`, p.js.String()))))
|
|
98
|
+
// `, p.js.String()))))
|
|
99
99
|
p.Body.writeHtmlIndent(w, 1)
|
|
100
100
|
w.Write([]byte("\n</html>"))
|
|
101
101
|
}
|
|
@@ -458,7 +458,18 @@ func PerformRequest(route string, h interface{}, ctx interface{}, w http.Respons
|
|
|
458
458
|
if f := rv.Field(i); f.CanSet() {
|
|
459
459
|
jsonName := structType.Field(i).Tag.Get("json")
|
|
460
460
|
jsonValue := r.URL.Query().Get(jsonName)
|
|
461
|
+
if f.Kind() == reflect.String {
|
|
461
|
-
|
|
462
|
+
f.SetString(jsonValue)
|
|
463
|
+
} else if f.Kind() == reflect.Int64 {
|
|
464
|
+
v, err := strconv.ParseInt(jsonValue, 10, 64)
|
|
465
|
+
if err != nil {
|
|
466
|
+
RespondError(w, 500, err)
|
|
467
|
+
return 500, err
|
|
468
|
+
}
|
|
469
|
+
f.SetInt(v)
|
|
470
|
+
} else {
|
|
471
|
+
panic("Uknown query param: " + jsonValue)
|
|
472
|
+
}
|
|
462
473
|
}
|
|
463
474
|
}
|
|
464
475
|
}
|
|
@@ -473,13 +484,15 @@ func PerformRequest(route string, h interface{}, ctx interface{}, w http.Respons
|
|
|
473
484
|
return responseStatus, responseError.(error)
|
|
474
485
|
}
|
|
475
486
|
if v, ok := response.(HtmlPage); ok {
|
|
476
|
-
w.WriteHeader(responseStatus)
|
|
477
487
|
w.Header().Set("Content-Type", "text/html")
|
|
488
|
+
// This has to be at end always
|
|
489
|
+
w.WriteHeader(responseStatus)
|
|
478
490
|
v.WriteHtml(w)
|
|
479
491
|
return 200, nil
|
|
480
492
|
}
|
|
481
|
-
w.WriteHeader(responseStatus)
|
|
482
493
|
w.Header().Set("Content-Type", "application/json")
|
|
494
|
+
// This has to be at end always
|
|
495
|
+
w.WriteHeader(responseStatus)
|
|
483
496
|
data, _ := json.Marshal(response)
|
|
484
497
|
w.Write(data)
|
|
485
498
|
return 200, nil
|