~repos /gromer

#golang#htmx#ssr

git clone https://pyrossh.dev/repos/gromer.git

gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.


ccb16c71 Peter John

tag: v0.16.0

v0.16.0

3 years ago
improve logging and 404 handling
_example/main.go CHANGED
@@ -11,6 +11,7 @@ import (
11
11
 
12
12
  "github.com/pyros2097/gromer/_example/assets"
13
13
  "github.com/pyros2097/gromer/_example/components"
14
+ "github.com/pyros2097/gromer/_example/pages/404"
14
15
  "github.com/pyros2097/gromer/_example/pages"
15
16
  "github.com/pyros2097/gromer/_example/pages/about"
16
17
  "github.com/pyros2097/gromer/_example/pages/api/recover"
@@ -31,7 +32,9 @@ func main() {
31
32
  r := mux.NewRouter()
32
33
  r.Use(gromer.CorsMiddleware)
33
34
  r.Use(gromer.LogMiddleware)
35
+
34
- r.NotFoundHandler = gromer.NotFoundHandler
36
+ r.NotFoundHandler = gromer.StatusHandler(not_found_404.GET)
37
+
35
38
  gromer.Static(r, "/assets/", assets.FS)
36
39
  gromer.Handle(r, "GET", "/api", gromer.ApiExplorer)
37
40
  gromer.Handle(r, "GET", "/", pages.GET)
_example/makefile CHANGED
@@ -9,7 +9,7 @@ setup:
9
9
  generate: export DATABASE_URL=postgres://postgres:demo@127.0.0.1:5432/postgres?sslmode=disable
10
10
  generate:
11
11
  sqlc generate -f db/sqlc.yaml
12
- gromer -pkg github.com/pyros2097/gromer/example
12
+ gromer -pkg github.com/pyros2097/gromer/_example
13
13
  dbmate migrate
14
14
 
15
15
  run: export PORT=3000
_example/pages/404/get.go ADDED
@@ -0,0 +1,21 @@
1
+ package not_found_404
2
+
3
+ import (
4
+ "context"
5
+
6
+ . "github.com/pyros2097/gromer/handlebars"
7
+ )
8
+
9
+ func GET(c context.Context) (HtmlContent, int, error) {
10
+ return Html(`
11
+ {{#Page title="Page Not Found"}}
12
+ {{#Header}}{{/Header}}
13
+ <main class="box center">
14
+ <h1>Page Not Found</h1>
15
+ <h2 class="mt-6">
16
+ <a class="is-underlined" href="/">Go Back</a>
17
+ </h1>
18
+ </main>
19
+ {{/Page}}
20
+ `).RenderWithStatus(404)
21
+ }
cmd/gromer/main.go CHANGED
@@ -78,6 +78,7 @@ func lowerFirst(s string) string {
78
78
 
79
79
  func main() {
80
80
  moduleName := ""
81
+ notFoundPkg := ""
81
82
  pkgFlag := flag.String("pkg", "", "specify a package name")
82
83
  flag.Parse()
83
84
  if pkgFlag == nil || *pkgFlag == "" {
@@ -110,8 +111,13 @@ func main() {
110
111
  return err
111
112
  }
112
113
  lines := strings.Split(string(data), "\n")
114
+ pkg := strings.Replace(""+lines[0], "package ", "", 1)
115
+ if strings.Contains(filesrc, "/404/") {
116
+ notFoundPkg = pkg
117
+ return nil
118
+ }
113
119
  gromer.RouteDefs = append(gromer.RouteDefs, gromer.RouteDefinition{
114
- Pkg: strings.Replace(""+lines[0], "package ", "", 1),
120
+ Pkg: pkg,
115
121
  PkgPath: getRoute(method, route),
116
122
  Method: method,
117
123
  Path: rewritePath(path),
@@ -170,6 +176,7 @@ import (
170
176
 
171
177
  "{{ moduleName }}/assets"
172
178
  "{{ moduleName }}/components"
179
+ {{#if notFoundPkg}}"{{ moduleName }}/pages/404"{{/if}}
173
180
  {{#each routeImports as |route| }}"{{ moduleName }}/pages{{ route.PkgPath }}"
174
181
  {{/each}}
175
182
  )
@@ -184,7 +191,9 @@ func main() {
184
191
  r := mux.NewRouter()
185
192
  r.Use(gromer.CorsMiddleware)
186
193
  r.Use(gromer.LogMiddleware)
194
+ {{#if notFoundPkg}}
187
- r.NotFoundHandler = gromer.NotFoundHandler
195
+ r.NotFoundHandler = gromer.StatusHandler({{ notFoundPkg }}.GET)
196
+ {{/if}}
188
197
  gromer.Static(r, "/assets/", assets.FS)
189
198
  gromer.Handle(r, "GET", "/api", gromer.ApiExplorer)
190
199
  {{#each routes as |route| }}gromer.Handle(r, "{{ route.Method }}", "{{ route.Path }}", {{ route.Pkg }}.{{ route.Method }})
@@ -200,6 +209,7 @@ func main() {
200
209
  "routes", gromer.RouteDefs,
201
210
  "routeImports", routeImports,
202
211
  "componentNames", componentNames,
212
+ "notFoundPkg", notFoundPkg,
203
213
  "tick", "`",
204
214
  ).Render()
205
215
  if err != nil {
go.mod CHANGED
@@ -22,6 +22,7 @@ require (
22
22
  cloud.google.com/go/firestore v1.5.0 // indirect
23
23
  github.com/aymerick/douceur v0.2.0 // indirect
24
24
  github.com/aymerick/raymond v2.0.2+incompatible // indirect
25
+ github.com/blang/semver v3.5.1+incompatible // indirect
25
26
  github.com/carlmjohnson/versioninfo v0.22.1 // indirect
26
27
  github.com/davecgh/go-spew v1.1.1 // indirect
27
28
  github.com/go-playground/locales v0.14.0 // indirect
@@ -39,6 +40,7 @@ require (
39
40
  github.com/microcosm-cc/bluemonday v1.0.16 // indirect
40
41
  github.com/pkg/errors v0.9.1 // indirect
41
42
  github.com/pmezard/go-difflib v1.0.0 // indirect
43
+ github.com/rs/xid v1.3.0 // indirect
42
44
  github.com/russross/blackfriday v1.5.2 // indirect
43
45
  github.com/sergi/go-diff v1.2.0 // indirect
44
46
  github.com/shurcooL/github_flavored_markdown v0.0.0-20210228213109-c3a9aa474629 // indirect
@@ -64,4 +66,5 @@ require (
64
66
  google.golang.org/grpc v1.40.0 // indirect
65
67
  google.golang.org/protobuf v1.27.1 // indirect
66
68
  gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
69
+ xojoc.pw/useragent v0.0.0-20200116211053-1ec61d55e8fe // indirect
67
70
  )
go.sum CHANGED
@@ -130,6 +130,8 @@ github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd3
130
130
  github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0=
131
131
  github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
132
132
  github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
133
+ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
134
+ github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
133
135
  github.com/bradleyjkemp/cupaloy v2.3.0+incompatible h1:UafIjBvWQmS9i/xRg+CamMrnLTKNzo+bdmT/oH34c2Y=
134
136
  github.com/bradleyjkemp/cupaloy v2.3.0+incompatible/go.mod h1:Au1Xw1sgaJ5iSFktEhYsS0dbQiS1B0/XMXl+42y9Ilk=
135
137
  github.com/carlmjohnson/versioninfo v0.22.1 h1:NVwTmCUpSoxBxy8+z10CbyBazeRZ4R2n6QgrNi3Wd6M=
@@ -346,6 +348,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
346
348
  github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
347
349
  github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
348
350
  github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
351
+ github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=
349
352
  github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
350
353
  github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
351
354
  github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
@@ -837,3 +840,5 @@ nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0
837
840
  rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
838
841
  rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
839
842
  rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
843
+ xojoc.pw/useragent v0.0.0-20200116211053-1ec61d55e8fe h1:KHyqPlOEFFT7OPh4WR7qFzNNndwj1VuwV+rZ+Tb3bio=
844
+ xojoc.pw/useragent v0.0.0-20200116211053-1ec61d55e8fe/go.mod h1:71om/Qz9HbIEjbUrkrzmJiF26FSh6tcwqSFdBBkLtJQ=
handlebars/template.go CHANGED
@@ -47,7 +47,7 @@ func (t *Template) Parse() error {
47
47
  }
48
48
 
49
49
  // Exec the template using the content and return the results
50
- func (t *Template) Render() (HtmlContent, int, error) {
50
+ func (t *Template) RenderWithStatus(status int) (HtmlContent, int, error) {
51
51
  err := t.Parse()
52
52
  if err != nil {
53
53
  return HtmlContent("Server Erorr"), 500, errors.WithStack(err)
@@ -56,7 +56,7 @@ func (t *Template) Render() (HtmlContent, int, error) {
56
56
  r := t.program.Accept(v)
57
57
  switch rp := r.(type) {
58
58
  case string:
59
- return HtmlContent(rp), 200, nil
59
+ return HtmlContent(rp), status, nil
60
60
  case error:
61
61
  return HtmlContent("Server Erorr"), 500, rp
62
62
  case nil:
@@ -66,6 +66,10 @@ func (t *Template) Render() (HtmlContent, int, error) {
66
66
  }
67
67
  }
68
68
 
69
+ func (t *Template) Render() (HtmlContent, int, error) {
70
+ return t.RenderWithStatus(200)
71
+ }
72
+
69
73
  func (t *Template) Prop(key string, v any) *Template {
70
74
  t.Context.Set(key, v)
71
75
  return t
http.go CHANGED
@@ -23,9 +23,21 @@ import (
23
23
  "github.com/rs/zerolog"
24
24
  "github.com/rs/zerolog/log"
25
25
  "github.com/rs/zerolog/pkgerrors"
26
+ "xojoc.pw/useragent"
26
27
  )
27
28
 
28
29
  var info *debug.BuildInfo
30
+ var IsCloundRun bool
31
+
32
+ func init() {
33
+ IsCloundRun = os.Getenv("K_REVISION") != ""
34
+ info, _ = debug.ReadBuildInfo()
35
+ zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
36
+ log.Logger = log.Output(zerolog.ConsoleWriter{
37
+ Out: os.Stdout,
38
+ TimeFormat: zerolog.TimeFormatUnix,
39
+ })
40
+ }
29
41
 
30
42
  var RouteDefs []RouteDefinition
31
43
 
@@ -85,15 +97,6 @@ func RegisterComponent(fn any, props ...string) {
85
97
  })
86
98
  }
87
99
 
88
- func init() {
89
- info, _ = debug.ReadBuildInfo()
90
- zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
91
- log.Logger = log.Output(zerolog.ConsoleWriter{
92
- Out: os.Stdout,
93
- TimeFormat: zerolog.TimeFormatUnix,
94
- })
95
- }
96
-
97
100
  func RespondError(w http.ResponseWriter, status int, err error) {
98
101
  w.Header().Set("Content-Type", "application/json")
99
102
  w.WriteHeader(status) // always write status last
@@ -275,6 +278,9 @@ var LogMiddleware = mux.MiddlewareFunc(func(next http.Handler) http.Handler {
275
278
  RespondError(w, 599, fmt.Errorf("panic: %+v\n %s", err, stack))
276
279
  }
277
280
  }()
281
+ if IsCloundRun {
282
+ return
283
+ }
278
284
  startTime := time.Now()
279
285
  logRespWriter := NewLogResponseWriter(w)
280
286
  next.ServeHTTP(logRespWriter, r)
@@ -286,21 +292,27 @@ var LogMiddleware = mux.MiddlewareFunc(func(next http.Handler) http.Handler {
286
292
  if logRespWriter.err != nil {
287
293
  logger = log.WithLevel(zerolog.ErrorLevel).Err(logRespWriter.err).Stack()
288
294
  }
295
+ ua := useragent.Parse(r.UserAgent())
296
+ logger.Msgf("%s %d %.2f KB %3s %s %s", r.Method,
297
+ logRespWriter.responseStatusCode,
298
+ float32(logRespWriter.responseContentLength)/1024.0,
299
+ time.Since(startTime).Round(time.Millisecond).String(), ua.Name, r.URL.Path)
300
+
289
- logger.
301
+ // logger.
290
- Str("method", r.Method).
302
+ // Str("method", r.Method).
291
- Str("url", r.URL.String()).
303
+ // Str("url", r.URL.String()).
292
- Int("header_size", int(headerSize(r.Header))).
304
+ // Int("header_size", int(headerSize(r.Header))).
293
- Int64("body_size", r.ContentLength).
305
+ // Int64("body_size", r.ContentLength).
294
- Str("host", r.Host).
306
+ // Str("host", r.Host).
295
- // Str("agent", r.UserAgent()).
307
+ // // Str("agent", r.UserAgent()).
296
- Str("referer", r.Referer()).
308
+ // Str("referer", r.Referer()).
297
- Str("proto", r.Proto).
309
+ // Str("proto", r.Proto).
298
- Str("remote_ip", ip).
310
+ // Str("remote_ip", ip).
299
- Int("status", logRespWriter.responseStatusCode).
311
+ // Int("status", logRespWriter.responseStatusCode).
300
- Int("resp_header_size", logRespWriter.responseHeaderSize).
312
+ // Int("resp_header_size", logRespWriter.responseHeaderSize).
301
- Int("resp_body_size", logRespWriter.responseContentLength).
313
+ // Int("resp_body_size", logRespWriter.responseContentLength).
302
- Str("latency", time.Since(startTime).String()).
314
+ // Str("latency", time.Since(startTime).String()).
303
- Msg("")
315
+ // Msgf("")
304
316
  })
305
317
  })
306
318
 
@@ -317,9 +329,24 @@ func CorsMiddleware(next http.Handler) http.Handler {
317
329
  })
318
330
  }
319
331
 
332
+ func StatusHandler(h interface{}) http.Handler {
320
- var NotFoundHandler = LogMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
333
+ return LogMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
334
+ ctx := context.WithValue(context.WithValue(r.Context(), "url", r.URL), "header", r.Header)
335
+ values := reflect.ValueOf(h).Call([]reflect.Value{reflect.ValueOf(ctx)})
336
+ response := values[0].Interface()
337
+ responseStatus := values[1].Interface().(int)
338
+ responseError := values[2].Interface()
339
+ if responseError != nil {
321
- RespondError(w, 404, fmt.Errorf("path '%s' not found", r.URL.String()))
340
+ RespondError(w, responseStatus, responseError.(error))
341
+ return
322
- }))
342
+ }
343
+ w.Header().Set("Content-Type", "text/html")
344
+
345
+ // This has to be at end always
346
+ w.WriteHeader(responseStatus)
347
+ w.Write([]byte(response.(handlebars.HtmlContent)))
348
+ })).(http.Handler)
349
+ }
323
350
 
324
351
  func WrapCache(h http.Handler) http.Handler {
325
352
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -329,7 +356,7 @@ func WrapCache(h http.Handler) http.Handler {
329
356
  }
330
357
 
331
358
  func Static(router *mux.Router, path string, fs embed.FS) {
332
- router.PathPrefix(path).Handler(http.StripPrefix(path, http.FileServer(http.FS(fs))))
359
+ router.PathPrefix(path).Handler(http.StripPrefix(path, WrapCache(http.FileServer(http.FS(fs)))))
333
360
  }
334
361
 
335
362
  func Handle(router *mux.Router, method, route string, h interface{}) {