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.


3ce8bcf — Peter John 2021-11-28T20:07:10+05:30
rename package and fix bugs

Generated files not shown: go.sum

Files changed (10) hide show
  1. api_explorer.go +3 -3
  2. cmd/{wapp → gromer}/main.go +31 -23
  3. css.go +3 -2
  4. go.mod +4 -1
  5. hooks.go +1 -1
  6. hooks_test.go +1 -1
  7. html.go +1 -118
  8. html_test.go +1 -1
  9. http.go +133 -0
  10. readme.md +10 -15
api_explorer.go CHANGED
@@ -1,4 +1,4 @@
1
- package wapp
1
+ package gromer
2
2
 
3
3
  import (
4
4
  "context"
@@ -51,8 +51,8 @@ func ApiExplorer(apiDefs []ApiDefinition) func(c context.Context) (HtmlPage, int
51
51
  Head(
52
52
  Title("Example"),
53
53
  Meta("description", "Example"),
54
- Meta("author", "pyros2097"),
54
+ Meta("author", "pyros.sh"),
55
- Meta("keywords", "wapp,pyros2097"),
55
+ Meta("keywords", "pyros.sh, gromer"),
56
56
  Meta("viewport", "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover"),
57
57
  Link("icon", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAMFBMVEU0OkArMjhobHEoPUPFEBIuO0L+AAC2FBZ2JyuNICOfGx7xAwTjCAlCNTvVDA1aLzQ3COjMAAAAVUlEQVQI12NgwAaCDSA0888GCItjn0szWGBJTVoGSCjWs8TleQCQYV95evdxkFT8Kpe0PLDi5WfKd4LUsN5zS1sKFolt8bwAZrCaGqNYJAgFDEpQAAAzmxafI4vZWwAAAABJRU5ErkJggg=="),
58
58
  Link("stylesheet", "https://cdn.jsdelivr.net/npm/[email protected]/lib/codemirror.css"),
cmd/{wapp → gromer}/main.go RENAMED
@@ -2,6 +2,7 @@ package main
2
2
 
3
3
  import (
4
4
  "bytes"
5
+ "flag"
5
6
  "fmt"
6
7
  "io/ioutil"
7
8
  "log"
@@ -10,7 +11,7 @@ import (
10
11
  "strings"
11
12
 
12
13
  "github.com/gobuffalo/velvet"
13
- "github.com/pyros2097/wapp"
14
+ "github.com/pyros2097/gromer"
14
15
  "golang.org/x/mod/modfile"
15
16
  )
16
17
 
@@ -78,7 +79,7 @@ func rewritePkg(pkg string) string {
78
79
  return lastItem
79
80
  }
80
81
 
81
- func getApiFunc(method, route string, params []string) wapp.ApiDefinition {
82
+ func getApiFunc(method, route string, params []string) gromer.ApiDefinition {
82
83
  muxRoute := bytes.NewBuffer(nil)
83
84
  foundStart := false
84
85
  for _, v := range route {
@@ -92,7 +93,7 @@ func getApiFunc(method, route string, params []string) wapp.ApiDefinition {
92
93
  muxRoute.WriteString(string(v))
93
94
  }
94
95
  }
95
- return wapp.ApiDefinition{
96
+ return gromer.ApiDefinition{
96
97
  Method: method,
97
98
  PathParams: params,
98
99
  Path: muxRoute.String(),
@@ -154,19 +155,26 @@ func getApiFunc(method, route string, params []string) wapp.ApiDefinition {
154
155
  // }
155
156
 
156
157
  func main() {
158
+ moduleName := ""
159
+ pkgFlag := flag.String("pkg", "", "specify a package name")
160
+ flag.Parse()
161
+ if pkgFlag == nil || *pkgFlag == "" {
157
- data, err := ioutil.ReadFile("go.mod")
162
+ data, err := ioutil.ReadFile("go.mod")
158
- if err != nil {
163
+ if err != nil {
159
- log.Fatalf("go.mod file not found %s", err.Error())
164
+ log.Fatalf("go.mod file not found %s", err.Error())
160
- }
165
+ }
161
- modTree, err := modfile.Parse("go.mod", data, nil)
166
+ modTree, err := modfile.Parse("go.mod", data, nil)
162
- if err != nil {
167
+ if err != nil {
163
- log.Fatalf("could not parse go.mod %s", err.Error())
168
+ log.Fatalf("could not parse go.mod %s", err.Error())
169
+ }
170
+ moduleName = modTree.Module.Mod.Path
171
+ } else {
172
+ moduleName = *pkgFlag
164
173
  }
165
- moduleName := modTree.Module.Mod.Path
166
174
  routes := []*Route{}
167
- apiDefs := []wapp.ApiDefinition{}
175
+ apiDefs := []gromer.ApiDefinition{}
168
176
  allPkgs := map[string]string{}
169
- err = filepath.Walk("pages",
177
+ err := filepath.Walk("pages",
170
178
  func(filesrc string, info os.FileInfo, err error) error {
171
179
  if err != nil {
172
180
  return err
@@ -182,7 +190,7 @@ func main() {
182
190
  pkg = "pages"
183
191
  }
184
192
  routePath := rewritePath(path)
185
- params := wapp.GetRouteParams(routePath)
193
+ params := gromer.GetRouteParams(routePath)
186
194
  routes = append(routes, &Route{
187
195
  Method: method,
188
196
  Path: routePath,
@@ -206,7 +214,7 @@ func main() {
206
214
  ctx.Set("routes", routes)
207
215
  ctx.Set("apiDefs", apiDefs)
208
216
  ctx.Set("tick", "`")
209
- s, err := velvet.Render(`// Code generated by wapp. DO NOT EDIT.
217
+ s, err := velvet.Render(`// Code generated by gromer. DO NOT EDIT.
210
218
  package main
211
219
 
212
220
  import (
@@ -217,7 +225,7 @@ import (
217
225
 
218
226
  "github.com/apex/gateway/v2"
219
227
  "github.com/gorilla/mux"
220
- "github.com/pyros2097/wapp"
228
+ "github.com/pyros2097/gromer"
221
229
  "github.com/rs/zerolog/log"
222
230
  "gocloud.dev/server"
223
231
 
@@ -234,7 +242,7 @@ func main() {
234
242
  r := mux.NewRouter()
235
243
  r.NotFoundHandler = http.HandlerFunc(notFound)
236
244
  r.PathPrefix("/assets/").Handler(wrapCache(http.FileServer(http.FS(assetsFS))))
237
- handle(r, "GET", "/api", wapp.ApiExplorer(apiDefinitions()))
245
+ handle(r, "GET", "/api", gromer.ApiExplorer(apiDefinitions()))
238
246
  {{#each routes as |route| }}handle(r, "{{ route.Method }}", "{{ route.Path }}", {{ route.Pkg }}.{{ route.Method }})
239
247
  {{/each}}
240
248
  if !isLambda {
@@ -259,14 +267,14 @@ func wrapCache(h http.Handler) http.Handler {
259
267
  }
260
268
 
261
269
  func notFound(w http.ResponseWriter, r *http.Request) {
262
- wapp.LogReq(404, r)
270
+ gromer.LogReq(404, r)
263
271
  }
264
272
 
265
273
  func handle(router *mux.Router, method, route string, h interface{}) {
266
274
  router.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
267
275
  var status int
268
276
  defer func() {
269
- wapp.LogReq(status, r)
277
+ gromer.LogReq(status, r)
270
278
  }()
271
279
  ctx, err := context.WithContext(c.WithValue(
272
280
  c.WithValue(
@@ -274,18 +282,18 @@ func handle(router *mux.Router, method, route string, h interface{}) {
274
282
  "url", r.URL),
275
283
  "header", r.Header))
276
284
  if err != nil {
277
- wapp.RespondError(w, 500, err)
285
+ gromer.RespondError(w, 500, err)
278
286
  return
279
287
  }
280
- status, err = wapp.PerformRequest(route, h, ctx, w, r)
288
+ status, err = gromer.PerformRequest(route, h, ctx, w, r)
281
289
  if err != nil {
282
290
  log.Error().Stack().Err(err).Msg("")
283
291
  }
284
292
  }).Methods(method)
285
293
  }
286
294
 
287
- func apiDefinitions() []wapp.ApiDefinition {
295
+ func apiDefinitions() []gromer.ApiDefinition {
288
- return []wapp.ApiDefinition{
296
+ return []gromer.ApiDefinition{
289
297
  {{#each apiDefs as |api| }}
290
298
  {
291
299
  Method: "{{api.Method}}",
css.go CHANGED
@@ -1,4 +1,4 @@
1
- package wapp
1
+ package gromer
2
2
 
3
3
  type M map[string]interface{}
4
4
  type MS map[string]string
@@ -467,8 +467,9 @@ func mapApply(obj KeyValues) {
467
467
  twClassLookup[className] = vstring + ": " + vv + ";"
468
468
  }
469
469
  if varr, ok := v.(Arr); ok {
470
+ twClassLookup[className] = ""
470
471
  for _, kk := range varr {
471
- twClassLookup[className] = kk.(string) + ": " + vv + ";"
472
+ twClassLookup[className] += kk.(string) + ": " + vv + ";"
472
473
  }
473
474
  }
474
475
  }
go.mod CHANGED
@@ -1,9 +1,10 @@
1
- module github.com/pyros2097/wapp
1
+ module github.com/pyros2097/gromer
2
2
 
3
3
  go 1.16
4
4
 
5
5
  require (
6
6
  git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999 // indirect
7
+ github.com/apex/gateway/v2 v2.0.0 // indirect
7
8
  github.com/aymerick/raymond v2.0.2+incompatible // indirect
8
9
  github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect
9
10
  github.com/bradleyjkemp/cupaloy v2.3.0+incompatible
@@ -19,6 +20,7 @@ require (
19
20
  github.com/gobuffalo/velvet v0.0.0-20170320144106-d97471bf5d8f
20
21
  github.com/gogo/protobuf v1.1.1 // indirect
21
22
  github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 // indirect
23
+ github.com/google/uuid v1.3.0 // indirect
22
24
  github.com/googleapis/gax-go v2.0.0+incompatible // indirect
23
25
  github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
24
26
  github.com/gorilla/context v1.1.1 // indirect
@@ -28,6 +30,7 @@ require (
28
30
  github.com/jonboulle/clockwork v0.1.0 // indirect
29
31
  github.com/jtolds/gls v4.2.1+incompatible // indirect
30
32
  github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
33
+ github.com/lib/pq v1.10.4 // indirect
31
34
  github.com/markbates/inflect v1.0.4 // indirect
32
35
  github.com/mattn/go-isatty v0.0.14 // indirect
33
36
  github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
hooks.go CHANGED
@@ -1,4 +1,4 @@
1
- package wapp
1
+ package gromer
2
2
 
3
3
  type Context struct {
4
4
  index int
hooks_test.go CHANGED
@@ -1,4 +1,4 @@
1
- package wapp
1
+ package gromer
2
2
 
3
3
  import (
4
4
  "testing"
html.go CHANGED
@@ -1,28 +1,13 @@
1
- package wapp
1
+ package gromer
2
2
 
3
3
  import (
4
4
  "bytes"
5
- "encoding/json"
6
5
  "fmt"
7
6
  "io"
8
- "net/http"
9
- "os"
10
- "reflect"
11
- "regexp"
12
7
  "strconv"
13
8
  "strings"
14
-
15
- "github.com/fatih/color"
16
- "github.com/gorilla/mux"
17
- "github.com/rs/zerolog"
18
- "github.com/rs/zerolog/log"
19
9
  )
20
10
 
21
- func init() {
22
- zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
23
- log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
24
- }
25
-
26
11
  func writeIndent(w io.Writer, indent int) {
27
12
  for i := 0; i < indent*2; i++ {
28
13
  w.Write([]byte(" "))
@@ -413,105 +398,3 @@ func XData(v string) Attribute {
413
398
  func XText(v string) Attribute {
414
399
  return Attribute{"x-text", v}
415
400
  }
416
-
417
- func RespondError(w http.ResponseWriter, status int, err error) {
418
- w.WriteHeader(status)
419
- w.Header().Set("Content-Type", "application/json")
420
- data, _ := json.Marshal(map[string]string{
421
- "error": err.Error(),
422
- })
423
- w.Write(data)
424
- }
425
-
426
- var pathParamsRegex = regexp.MustCompile(`{(.*?)}`)
427
-
428
- func GetRouteParams(route string) []string {
429
- params := []string{}
430
- found := pathParamsRegex.FindAllString(route, -1)
431
- for _, v := range found {
432
- params = append(params, strings.Replace(strings.Replace(v, "}", "", 1), "{", "", 1))
433
- }
434
- return params
435
- }
436
-
437
- func PerformRequest(route string, h interface{}, ctx interface{}, w http.ResponseWriter, r *http.Request) (int, error) {
438
- params := GetRouteParams(route)
439
- args := []reflect.Value{reflect.ValueOf(ctx)}
440
- funcType := reflect.TypeOf(h)
441
- icount := funcType.NumIn()
442
- vars := mux.Vars(r)
443
- for _, k := range params {
444
- args = append(args, reflect.ValueOf(vars[k]))
445
- }
446
- if len(args) != icount {
447
- structType := funcType.In(icount - 1)
448
- instance := reflect.New(structType)
449
- if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {
450
- err := json.NewDecoder(r.Body).Decode(instance.Interface())
451
- if err != nil {
452
- RespondError(w, 500, err)
453
- return 500, err
454
- }
455
- } else if r.Method == "GET" {
456
- rv := instance.Elem()
457
- for i := 0; i < structType.NumField(); i++ {
458
- if f := rv.Field(i); f.CanSet() {
459
- jsonName := structType.Field(i).Tag.Get("json")
460
- jsonValue := r.URL.Query().Get(jsonName)
461
- if f.Kind() == reflect.String {
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
- }
473
- }
474
- }
475
- }
476
- args = append(args, instance.Elem())
477
- }
478
- values := reflect.ValueOf(h).Call(args)
479
- response := values[0].Interface()
480
- responseStatus := values[1].Interface().(int)
481
- responseError := values[2].Interface()
482
- if responseError != nil {
483
- RespondError(w, responseStatus, responseError.(error))
484
- return responseStatus, responseError.(error)
485
- }
486
- if v, ok := response.(HtmlPage); ok {
487
- w.Header().Set("Content-Type", "text/html")
488
- // This has to be at end always
489
- w.WriteHeader(responseStatus)
490
- v.WriteHtml(w)
491
- return 200, nil
492
- }
493
- w.Header().Set("Content-Type", "application/json")
494
- // This has to be at end always
495
- w.WriteHeader(responseStatus)
496
- data, _ := json.Marshal(response)
497
- w.Write(data)
498
- return 200, nil
499
- }
500
-
501
- func LogReq(status int, r *http.Request) {
502
- a := color.FgGreen
503
- if status >= 500 {
504
- a = color.FgRed
505
- } else if status >= 400 {
506
- a = color.FgYellow
507
- }
508
- m := color.FgCyan
509
- if r.Method == "POST" {
510
- m = color.FgYellow
511
- } else if r.Method == "PUT" {
512
- m = color.FgMagenta
513
- } else if r.Method == "DELETE" {
514
- m = color.FgRed
515
- }
516
- log.Info().Msgf("%3s %s %s", color.New(a).Sprint(status), color.New(m).Sprintf("%-4s", r.Method), color.WhiteString(r.URL.Path))
517
- }
html_test.go CHANGED
@@ -1,4 +1,4 @@
1
- package wapp
1
+ package gromer
2
2
 
3
3
  import (
4
4
  "bytes"
http.go ADDED
@@ -0,0 +1,133 @@
1
+ package gromer
2
+
3
+ import (
4
+ "encoding/json"
5
+ "net/http"
6
+ "os"
7
+ "reflect"
8
+ "regexp"
9
+ "strconv"
10
+ "strings"
11
+
12
+ "github.com/fatih/color"
13
+ "github.com/gorilla/mux"
14
+ "github.com/rs/zerolog"
15
+ "github.com/rs/zerolog/log"
16
+ )
17
+
18
+ func init() {
19
+ zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
20
+ log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
21
+ }
22
+
23
+ func RespondError(w http.ResponseWriter, status int, err error) {
24
+ w.WriteHeader(status)
25
+ w.Header().Set("Content-Type", "application/json")
26
+ data, _ := json.Marshal(map[string]string{
27
+ "error": err.Error(),
28
+ })
29
+ w.Write(data)
30
+ }
31
+
32
+ var pathParamsRegex = regexp.MustCompile(`{(.*?)}`)
33
+
34
+ func GetRouteParams(route string) []string {
35
+ params := []string{}
36
+ found := pathParamsRegex.FindAllString(route, -1)
37
+ for _, v := range found {
38
+ params = append(params, strings.Replace(strings.Replace(v, "}", "", 1), "{", "", 1))
39
+ }
40
+ return params
41
+ }
42
+
43
+ func PerformRequest(route string, h interface{}, ctx interface{}, w http.ResponseWriter, r *http.Request) (int, error) {
44
+ params := GetRouteParams(route)
45
+ args := []reflect.Value{reflect.ValueOf(ctx)}
46
+ funcType := reflect.TypeOf(h)
47
+ icount := funcType.NumIn()
48
+ vars := mux.Vars(r)
49
+ for _, k := range params {
50
+ args = append(args, reflect.ValueOf(vars[k]))
51
+ }
52
+ if len(args) != icount {
53
+ structType := funcType.In(icount - 1)
54
+ instance := reflect.New(structType)
55
+ if structType.Kind() != reflect.Struct {
56
+ panic(route + " func final param should be a struct")
57
+ }
58
+ if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {
59
+ err := json.NewDecoder(r.Body).Decode(instance.Interface())
60
+ if err != nil {
61
+ RespondError(w, 500, err)
62
+ return 500, err
63
+ }
64
+ } else if r.Method == "GET" {
65
+ rv := instance.Elem()
66
+ for i := 0; i < structType.NumField(); i++ {
67
+ if f := rv.Field(i); f.CanSet() {
68
+ jsonName := structType.Field(i).Tag.Get("json")
69
+ jsonValue := r.URL.Query().Get(jsonName)
70
+ if f.Kind() == reflect.String {
71
+ f.SetString(jsonValue)
72
+ } else if f.Kind() == reflect.Int64 {
73
+ v, err := strconv.ParseInt(jsonValue, 10, 64)
74
+ if err != nil {
75
+ RespondError(w, 500, err)
76
+ return 500, err
77
+ }
78
+ f.SetInt(v)
79
+ } else if f.Kind() == reflect.Int32 {
80
+ v, err := strconv.ParseInt(jsonValue, 10, 32)
81
+ if err != nil {
82
+ RespondError(w, 500, err)
83
+ return 500, err
84
+ }
85
+ f.SetInt(v)
86
+ } else {
87
+ panic("Uknown query param: " + jsonValue)
88
+ }
89
+ }
90
+ }
91
+ }
92
+ args = append(args, instance.Elem())
93
+ }
94
+ values := reflect.ValueOf(h).Call(args)
95
+ response := values[0].Interface()
96
+ responseStatus := values[1].Interface().(int)
97
+ responseError := values[2].Interface()
98
+ if responseError != nil {
99
+ RespondError(w, responseStatus, responseError.(error))
100
+ return responseStatus, responseError.(error)
101
+ }
102
+ if v, ok := response.(HtmlPage); ok {
103
+ w.Header().Set("Content-Type", "text/html")
104
+ // This has to be at end always
105
+ w.WriteHeader(responseStatus)
106
+ v.WriteHtml(w)
107
+ return 200, nil
108
+ }
109
+ w.Header().Set("Content-Type", "application/json")
110
+ // This has to be at end always
111
+ w.WriteHeader(responseStatus)
112
+ data, _ := json.Marshal(response)
113
+ w.Write(data)
114
+ return 200, nil
115
+ }
116
+
117
+ func LogReq(status int, r *http.Request) {
118
+ a := color.FgGreen
119
+ if status >= 500 {
120
+ a = color.FgRed
121
+ } else if status >= 400 {
122
+ a = color.FgYellow
123
+ }
124
+ m := color.FgCyan
125
+ if r.Method == "POST" {
126
+ m = color.FgYellow
127
+ } else if r.Method == "PUT" {
128
+ m = color.FgMagenta
129
+ } else if r.Method == "DELETE" {
130
+ m = color.FgRed
131
+ }
132
+ log.Info().Msgf("%3s %s %s", color.New(a).Sprint(status), color.New(m).Sprintf("%-4s", r.Method), color.WhiteString(r.URL.Path))
133
+ }
readme.md CHANGED
@@ -1,28 +1,23 @@
1
1
  <p align="center">
2
- <a href="https://goreportcard.com/report/github.com/pyros2097/wapp"><img src="https://goreportcard.com/badge/github.com/pyros2097/wapp" alt="Go Report Card"></a>
2
+ <a href="https://goreportcard.com/report/github.com/pyros2097/gromer"><img src="https://goreportcard.com/badge/github.com/pyros2097/gromer" alt="Go Report Card"></a>
3
- <a href="https://GitHub.com/pyros2097/wapp/releases/"><img src="https://img.shields.io/github/release/pyros2097/wapp.svg" alt="GitHub release"></a>
3
+ <a href="https://GitHub.com/pyros2097/gromer/releases/"><img src="https://img.shields.io/github/release/pyros2097/gromer.svg" alt="GitHub release"></a>
4
- <a href="https://pkg.go.dev/github.com/pyros2097/wapp"><img src="https://img.shields.io/badge/dev-reference-007d9c?logo=go&logoColor=white&style=flat" alt="pkg.go.dev docs"></a>
4
+ <a href="https://pkg.go.dev/github.com/pyros2097/gromer"><img src="https://img.shields.io/badge/dev-reference-007d9c?logo=go&logoColor=white&style=flat" alt="pkg.go.dev docs"></a>
5
5
  </p>
6
6
 
7
- # wapp
7
+ # gromer
8
8
 
9
- **wapp** is a framework to build web apps in golang.
9
+ **gromer** is a framework to build web apps in golang.
10
10
  It uses a declarative syntax using funcs that allows creating and dealing with HTML elements only by using Go, and without writing any HTML markup. It is highly opioninated and integrates uses tailwind css and alpinejs.
11
11
 
12
- # Install
13
-
14
- ```sh
15
- go mod init
16
- go get -u -v github.com/pyros2097/wapp
17
- ```
18
-
19
12
  # Install Cli
20
13
 
21
14
  ```sh
22
15
  go mod init
23
- go get -u -v github.com/pyros2097/wapp/cmd/wapp
16
+ go get -u -v github.com/pyros2097/wapp/cmd/gromer
24
17
  ```
25
18
 
26
- # Example
19
+ # Using
20
+ You need to follow this directory structure similar to nextjs for the api route handlers to be generated
21
+ Please look at the example for now,
27
22
 
28
- https://github.com/pyros2097/wapp-example
23
+ [Example](https://github.com/pyros2097/gromer/tree/master/example)