gromer

#golang#htmx#ssr

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

Files changed (4) hide show
  1. api_explorer.go +50 -32
  2. cmd/wapp/main.go +18 -9
  3. go.mod +34 -1
  4. 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", "/assets/icon.png"),
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: 100vh;
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
- // border-bottom: 1px solid rgb(204, 204, 204);
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 w-screen h-screen"),
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 w-full h-full"),
173
+ Div(Css("flex"),
163
- Div(Css("h-full"), Style("width: 50%;"),
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
- Section("Headers"),
177
+ Section("Headers"),
165
- Table(ID("headersTable"), Css("w-full"),
178
+ Table(ID("headersTable"),
166
- TR(
179
+ TR(
167
- TD(Style("width: 50%;"), Input(Css("w-full p-1"), Attr("value", "Authorization"))),
180
+ TD(Input(Css("w-full p-1"), Attr("value", "Authorization"))),
168
- TD(Style("width: 50%;"), Input(Css("w-full p-1"))),
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
- Div(ID("right"), Css("h-full border-l border-l-gray-200 text-lg"), Style("width: 50%;")),
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', lineNumbers: true })")),
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: 50%;";
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: 50%;";
262
+ cell1.style = "width: 30%; border-left: 0px;";
245
- cell2.style = "width: 50%;";
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 := &http.Server{
243
+ srv := server.New(http.DefaultServeMux, nil)
243
- Handler: r,
244
- Addr: "127.0.0.1:3000",
244
+ if err := srv.ListenAndServe(":3000"); err != nil {
245
- WriteTimeout: 30 * time.Second,
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
- log.Fatal().Stack().Err(gateway.ListenAndServe(":3000", r)).Msg("failed to listen")
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
- c.WithValue(r.Context(), "url", r.URL),
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/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
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
- document.addEventListener('alpine:init', () => {%s
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
- f.SetString(jsonValue)
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