~repos /gromer
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.
87943fb6
—
Peter John 3 years ago
improve stuff
- _example/components/Status.go +59 -0
- _example/main.go +1 -2
- _example/makefile +2 -0
- _example/readme.md +1 -1
- _example/routes/404/get.go +0 -23
- _example/routes/get.go +2 -0
- _example/routes/post.go +2 -3
- _example/services/todos/todo.go +4 -4
- _example/tests/todos_test.go +47 -0
- collection/collection.go +2 -1
- go.mod +2 -4
- go.sum +3 -5
- gsx/gsx.go +4 -3
- gsx/parser.go +2 -2
- gsx/twx.go +2 -2
- http.go +65 -64
_example/components/Status.go
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
package not_found_404
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
. "github.com/pyros2097/gromer/gsx"
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
// var (
|
|
8
|
+
// Meta = M{
|
|
9
|
+
// "title": "Oops something wen't wrong",
|
|
10
|
+
// }
|
|
11
|
+
// Styles = M{
|
|
12
|
+
// "notfound": "flex flex-1 items-center justify-center w-screen h-screen",
|
|
13
|
+
// "back": "mt-6",
|
|
14
|
+
// "link": "underlined",
|
|
15
|
+
// }
|
|
16
|
+
// )
|
|
17
|
+
|
|
18
|
+
// var (
|
|
19
|
+
// Meta = M{
|
|
20
|
+
// "title": "Page Not Found",
|
|
21
|
+
// }
|
|
22
|
+
// Styles = M{
|
|
23
|
+
// "notfound": "flex flex-1 items-center justify-center w-screen h-screen",
|
|
24
|
+
// "back": "mt-6",
|
|
25
|
+
// "link": "underlined",
|
|
26
|
+
// }
|
|
27
|
+
// )
|
|
28
|
+
|
|
29
|
+
// func GET(c *Context) ([]*Tag, int, error) {
|
|
30
|
+
// return c.Render(`
|
|
31
|
+
// <div id="error" class="notfound " hx-swap-oob="true">
|
|
32
|
+
// <h1>"Page Not Found"</h1>
|
|
33
|
+
// <h2 class="mt-6">
|
|
34
|
+
// <a class="link" href="/">"Go Back"</a>
|
|
35
|
+
// </h2>
|
|
36
|
+
// </div>
|
|
37
|
+
// `), 404, nil
|
|
38
|
+
// }
|
|
39
|
+
|
|
40
|
+
var StatusStyles = M{
|
|
41
|
+
"container": "border-t-2 border-gray-100 text-2xl",
|
|
42
|
+
"row": "flex flex-row group",
|
|
43
|
+
"button-1": "ml-4 text-gray-400",
|
|
44
|
+
"label": "flex-1 min-w-0 flex items-center break-all ml-2 p-2 text-gray-800",
|
|
45
|
+
"striked": "text-gray-500 line-through",
|
|
46
|
+
"button-2": "mr-4 text-red-700 opacity-0 hover:opacity-100",
|
|
47
|
+
"unchecked": "text-gray-200",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func Status(c *Context, status int, err error) []*Tag {
|
|
51
|
+
return c.Render(`
|
|
52
|
+
<div id="error" class="notfound " hx-swap-oob="true">
|
|
53
|
+
<h1>"Oops something wen't wrong"</h1>
|
|
54
|
+
<h2 class="mt-6">
|
|
55
|
+
<a class="link" href="/">"Go Back"</a>
|
|
56
|
+
</h2>
|
|
57
|
+
</div>
|
|
58
|
+
`), 404, nil
|
|
59
|
+
}
|
_example/main.go
CHANGED
|
@@ -12,7 +12,6 @@ import (
|
|
|
12
12
|
"github.com/pyros2097/gromer/_example/assets"
|
|
13
13
|
"github.com/pyros2097/gromer/_example/components"
|
|
14
14
|
"github.com/pyros2097/gromer/_example/containers"
|
|
15
|
-
"github.com/pyros2097/gromer/_example/routes/404"
|
|
16
15
|
"github.com/pyros2097/gromer/_example/routes"
|
|
17
16
|
"github.com/pyros2097/gromer/_example/routes/about"
|
|
18
17
|
|
|
@@ -28,7 +27,7 @@ func init() {
|
|
|
28
27
|
func main() {
|
|
29
28
|
baseRouter := mux.NewRouter()
|
|
30
29
|
baseRouter.Use(gromer.LogMiddleware)
|
|
31
|
-
baseRouter.NotFoundHandler = gromer.StatusHandler(not_found_404.GET)
|
|
30
|
+
// baseRouter.NotFoundHandler = gromer.StatusHandler(404, not_found_404.GET, not_found_404.Meta, not_found_404.Styles)
|
|
32
31
|
|
|
33
32
|
staticRouter := baseRouter.NewRoute().Subrouter()
|
|
34
33
|
staticRouter.Use(gromer.CacheMiddleware)
|
_example/makefile
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
setup:
|
|
2
2
|
go install github.com/mitranim/gow@latest
|
|
3
3
|
go install github.com/pyros2097/gromer/cmd/gromer@latest
|
|
4
|
+
go install github.com/playwright-community/playwright-go/cmd/playwright@latest
|
|
5
|
+
playwright install --with-deps
|
|
4
6
|
|
|
5
7
|
update:
|
|
6
8
|
gromer -pkg github.com/pyros2097/gromer/_example
|
_example/readme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Example
|
|
2
2
|
|
|
3
|
-
This example demonstrates gromer with a simple web app.
|
|
3
|
+
This example demonstrates gromer with a simple todo web app.
|
|
4
4
|
|
|
5
5
|
# Requirements
|
|
6
6
|
|
_example/routes/404/get.go
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
package not_found_404
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
. "github.com/pyros2097/gromer/gsx"
|
|
5
|
-
)
|
|
6
|
-
|
|
7
|
-
var (
|
|
8
|
-
Meta = M{
|
|
9
|
-
"title": "Page Not Found",
|
|
10
|
-
}
|
|
11
|
-
Styles = M{}
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
func GET(c *Context) ([]*Tag, int, error) {
|
|
15
|
-
return c.Render(`
|
|
16
|
-
<main class="box center">
|
|
17
|
-
<h1>Page Not Found</h1>
|
|
18
|
-
<h2 class="mt-6">
|
|
19
|
-
<a class="is-underlined" href="/">Go Back</a>
|
|
20
|
-
</h2>
|
|
21
|
-
</main>
|
|
22
|
-
`), 404, nil
|
|
23
|
-
}
|
_example/routes/get.go
CHANGED
|
@@ -105,6 +105,8 @@ func GET(c *Context, params GetParams) ([]*Tag, int, error) {
|
|
|
105
105
|
</div>
|
|
106
106
|
</div>
|
|
107
107
|
</main>
|
|
108
|
+
<div id="error">
|
|
109
|
+
</div>
|
|
108
110
|
<footer class="footer">
|
|
109
111
|
<span class="subtitle">"Written by "
|
|
110
112
|
<a class="link" href="https://github.com/pyrossh/">"pyrossh"</a>
|
_example/routes/post.go
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
package routes
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"fmt"
|
|
5
|
-
|
|
6
4
|
_ "github.com/pyros2097/gromer/_example/components"
|
|
7
5
|
"github.com/pyros2097/gromer/_example/services/todos"
|
|
8
6
|
. "github.com/pyros2097/gromer/gsx"
|
|
7
|
+
"github.com/rotisserie/eris"
|
|
9
8
|
)
|
|
10
9
|
|
|
11
10
|
type PostParams struct {
|
|
@@ -93,5 +92,5 @@ func POST(c *Context, params PostParams) ([]*Tag, int, error) {
|
|
|
93
92
|
<Todo />
|
|
94
93
|
`), 200, nil
|
|
95
94
|
}
|
|
96
|
-
return nil, 404,
|
|
95
|
+
return nil, 404, eris.Errorf("Intent not specified: %s", params.Intent)
|
|
97
96
|
}
|
_example/services/todos/todo.go
CHANGED
|
@@ -2,10 +2,10 @@ package todos
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
|
-
"errors"
|
|
6
5
|
"time"
|
|
7
6
|
|
|
8
7
|
"github.com/google/uuid"
|
|
8
|
+
"github.com/rotisserie/eris"
|
|
9
9
|
)
|
|
10
10
|
|
|
11
11
|
type Todo struct {
|
|
@@ -48,7 +48,7 @@ func UpdateTodo(ctx context.Context, id string, params UpdateTodoParams) (*Todo,
|
|
|
48
48
|
globalTodos[updateIndex].UpdatedAt = time.Now()
|
|
49
49
|
return globalTodos[updateIndex], nil
|
|
50
50
|
}
|
|
51
|
-
return nil,
|
|
51
|
+
return nil, eris.New("Todo not found")
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
func DeleteTodo(ctx context.Context, id string) (string, error) {
|
|
@@ -62,7 +62,7 @@ func DeleteTodo(ctx context.Context, id string) (string, error) {
|
|
|
62
62
|
globalTodos = append(globalTodos[:deleteIndex], globalTodos[deleteIndex+1:]...)
|
|
63
63
|
return id, nil
|
|
64
64
|
}
|
|
65
|
-
return "",
|
|
65
|
+
return "", eris.New("Todo not found")
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
func GetTodo(ctx context.Context, id string) (*Todo, error) {
|
|
@@ -71,7 +71,7 @@ func GetTodo(ctx context.Context, id string) (*Todo, error) {
|
|
|
71
71
|
return todo, nil
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
-
return nil,
|
|
74
|
+
return nil, eris.New("Todo not found")
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
type GetAllTodoParams struct {
|
_example/tests/todos_test.go
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package tests
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/playwright-community/playwright-go"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestTodo(t *testing.T) {
|
|
11
|
+
pw, err := playwright.Run()
|
|
12
|
+
if err != nil {
|
|
13
|
+
t.Fatalf("could not start playwright: %v", err)
|
|
14
|
+
}
|
|
15
|
+
browser, err := pw.Chromium.Launch()
|
|
16
|
+
if err != nil {
|
|
17
|
+
t.Fatalf("could not launch browser: %v", err)
|
|
18
|
+
}
|
|
19
|
+
page, err := browser.NewPage()
|
|
20
|
+
if err != nil {
|
|
21
|
+
t.Fatalf("could not create page: %v", err)
|
|
22
|
+
}
|
|
23
|
+
if _, err = page.Goto("https://news.ycombinator.com"); err != nil {
|
|
24
|
+
t.Fatalf("could not goto: %v", err)
|
|
25
|
+
}
|
|
26
|
+
entries, err := page.QuerySelectorAll(".athing")
|
|
27
|
+
if err != nil {
|
|
28
|
+
t.Fatalf("could not get entries: %v", err)
|
|
29
|
+
}
|
|
30
|
+
for i, entry := range entries {
|
|
31
|
+
titleElement, err := entry.QuerySelector("td.title > a")
|
|
32
|
+
if err != nil {
|
|
33
|
+
t.Fatalf("could not get title element: %v", err)
|
|
34
|
+
}
|
|
35
|
+
title, err := titleElement.TextContent()
|
|
36
|
+
if err != nil {
|
|
37
|
+
t.Fatalf("could not get text content: %v", err)
|
|
38
|
+
}
|
|
39
|
+
fmt.Printf("%d: %s\n", i+1, title)
|
|
40
|
+
}
|
|
41
|
+
if err = browser.Close(); err != nil {
|
|
42
|
+
t.Fatalf("could not close browser: %v", err)
|
|
43
|
+
}
|
|
44
|
+
if err = pw.Stop(); err != nil {
|
|
45
|
+
t.Fatalf("could not stop Playwright: %v", err)
|
|
46
|
+
}
|
|
47
|
+
}
|
collection/collection.go
CHANGED
|
@@ -6,6 +6,7 @@ import (
|
|
|
6
6
|
"io"
|
|
7
7
|
"reflect"
|
|
8
8
|
|
|
9
|
+
"github.com/rotisserie/eris"
|
|
9
10
|
"github.com/rs/zerolog/log"
|
|
10
11
|
"gocloud.dev/docstore"
|
|
11
12
|
_ "gocloud.dev/docstore/gcpfirestore"
|
|
@@ -44,7 +45,7 @@ func (q *Query[S]) One(ctx context.Context) (S, int, error) {
|
|
|
44
45
|
}
|
|
45
46
|
arr := reflect.ValueOf(results)
|
|
46
47
|
if arr.Len() == 0 {
|
|
47
|
-
return *new(S), 404,
|
|
48
|
+
return *new(S), 404, eris.Errorf("%s not found", q.Parent.Type.Name())
|
|
48
49
|
}
|
|
49
50
|
return arr.Index(0).Interface().(S), 200, nil
|
|
50
51
|
}
|
go.mod
CHANGED
|
@@ -4,9 +4,7 @@ go 1.18
|
|
|
4
4
|
|
|
5
5
|
require (
|
|
6
6
|
github.com/alecthomas/participle/v2 v2.0.0-beta.3
|
|
7
|
-
github.com/alecthomas/repr v0.1.0
|
|
8
7
|
github.com/felixge/httpsnoop v1.0.1
|
|
9
|
-
github.com/go-errors/errors v1.4.2
|
|
10
8
|
github.com/go-playground/validator/v10 v10.9.0
|
|
11
9
|
github.com/gobuffalo/velvet v0.0.0-20170320144106-d97471bf5d8f
|
|
12
10
|
github.com/goneric/stack v0.0.0-20220131052059-5990ae324dbd
|
|
@@ -14,8 +12,9 @@ require (
|
|
|
14
12
|
github.com/gorilla/handlers v1.5.1
|
|
15
13
|
github.com/gorilla/mux v1.8.0
|
|
16
14
|
github.com/imdario/mergo v0.3.12
|
|
17
|
-
github.com/
|
|
15
|
+
github.com/rotisserie/eris v0.5.4
|
|
18
16
|
github.com/rs/zerolog v1.26.1
|
|
17
|
+
github.com/samber/lo v1.21.0
|
|
19
18
|
github.com/segmentio/go-camelcase v0.0.0-20160726192923-7085f1e3c734
|
|
20
19
|
github.com/stretchr/testify v1.7.0
|
|
21
20
|
gocloud.dev v0.24.0
|
|
@@ -46,7 +45,6 @@ require (
|
|
|
46
45
|
github.com/pkg/errors v0.9.1 // indirect
|
|
47
46
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
48
47
|
github.com/russross/blackfriday v1.5.2 // indirect
|
|
49
|
-
github.com/samber/lo v1.21.0 // indirect
|
|
50
48
|
github.com/sergi/go-diff v1.2.0 // indirect
|
|
51
49
|
github.com/shurcooL/github_flavored_markdown v0.0.0-20210228213109-c3a9aa474629 // indirect
|
|
52
50
|
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 // indirect
|
go.sum
CHANGED
|
@@ -103,7 +103,6 @@ github.com/alecthomas/assert/v2 v2.0.3 h1:WKqJODfOiQG0nEJKFKzDIG3E29CN2/4zR9XGJz
|
|
|
103
103
|
github.com/alecthomas/participle/v2 v2.0.0-beta.3 h1:9HnyNuDsqOG8sl63Dz+KubqHhU8aWqsrjKdecim8GW0=
|
|
104
104
|
github.com/alecthomas/participle/v2 v2.0.0-beta.3/go.mod h1:RC764t6n4L8D8ITAJv0qdokritYSNR3wV5cVwmIEaMM=
|
|
105
105
|
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
|
|
106
|
-
github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
|
107
106
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
|
108
107
|
github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
|
|
109
108
|
github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
|
@@ -174,8 +173,6 @@ github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5
|
|
|
174
173
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
|
175
174
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
|
176
175
|
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
|
177
|
-
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
|
178
|
-
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
|
179
176
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
|
180
177
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
|
181
178
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
|
@@ -308,8 +305,6 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
|
|
308
305
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
|
309
306
|
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
|
310
307
|
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
|
311
|
-
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
|
|
312
|
-
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
|
313
308
|
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
|
314
309
|
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
|
315
310
|
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
|
@@ -358,6 +353,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
|
|
358
353
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
|
359
354
|
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
|
360
355
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
|
356
|
+
github.com/rotisserie/eris v0.5.4 h1:Il6IvLdAapsMhvuOahHWiBnl1G++Q0/L5UIkI5mARSk=
|
|
357
|
+
github.com/rotisserie/eris v0.5.4/go.mod h1:Z/kgYTJiJtocxCbFfvRmO+QejApzG6zpyky9G1A4g9s=
|
|
361
358
|
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
|
362
359
|
github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
|
|
363
360
|
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
|
|
@@ -397,6 +394,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
|
|
|
397
394
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
398
395
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
|
399
396
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
397
|
+
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
|
|
400
398
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
|
401
399
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
|
402
400
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
gsx/gsx.go
CHANGED
|
@@ -9,6 +9,7 @@ import (
|
|
|
9
9
|
"strconv"
|
|
10
10
|
"strings"
|
|
11
11
|
|
|
12
|
+
"github.com/rotisserie/eris"
|
|
12
13
|
"github.com/samber/lo"
|
|
13
14
|
)
|
|
14
15
|
|
|
@@ -84,7 +85,7 @@ func (comp ComponentFunc) Render(c *Context, tag *Tag) []*Tag {
|
|
|
84
85
|
} else {
|
|
85
86
|
s, ok := data.(string)
|
|
86
87
|
if !ok {
|
|
87
|
-
panic(
|
|
88
|
+
panic(eris.Errorf("expected component %s: prop %s to be of type string but got %+v ", comp.Name, arg, data))
|
|
88
89
|
}
|
|
89
90
|
value, _ = strconv.Atoi(s)
|
|
90
91
|
}
|
|
@@ -97,7 +98,7 @@ func (comp ComponentFunc) Render(c *Context, tag *Tag) []*Tag {
|
|
|
97
98
|
} else {
|
|
98
99
|
s, ok := data.(string)
|
|
99
100
|
if !ok {
|
|
100
|
-
panic(
|
|
101
|
+
panic(eris.Errorf("expected component %s: prop %s to be of type string but got %+v ", comp.Name, arg, data))
|
|
101
102
|
}
|
|
102
103
|
value, _ = strconv.ParseBool(s)
|
|
103
104
|
}
|
|
@@ -141,7 +142,7 @@ func Write(c *Context, w io.Writer, tags []*Tag) {
|
|
|
141
142
|
w.Write([]byte(fmt.Sprintf(`<script src="%s"></script>`, src)))
|
|
142
143
|
}
|
|
143
144
|
}
|
|
144
|
-
w.Write([]byte(`</head><body>`))
|
|
145
|
+
w.Write([]byte(`</head><body _="on htmx:error(errorInfo) put errorInfo.xhr.response into #error">`))
|
|
145
146
|
}
|
|
146
147
|
out := RenderString(tags)
|
|
147
148
|
w.Write([]byte(out))
|
gsx/parser.go
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
package gsx
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"fmt"
|
|
5
4
|
"strings"
|
|
6
5
|
|
|
7
6
|
"github.com/alecthomas/participle/v2"
|
|
8
7
|
"github.com/alecthomas/participle/v2/lexer"
|
|
9
8
|
"github.com/goneric/stack"
|
|
9
|
+
"github.com/rotisserie/eris"
|
|
10
10
|
)
|
|
11
11
|
|
|
12
12
|
type Module struct {
|
|
@@ -205,7 +205,7 @@ func processTree(nodes []*AstNode) []*Tag {
|
|
|
205
205
|
if n.Close.Name == prevTag.Name {
|
|
206
206
|
prevTag, _ = stack.Pop()
|
|
207
207
|
} else {
|
|
208
|
-
panic(
|
|
208
|
+
panic(eris.Errorf("Brackets not matching for tag %s in line %d:%d, prevTag: %s", n.Close.Name, n.Close.Pos.Line, n.Close.Pos.Column, prevTag.Name))
|
|
209
209
|
}
|
|
210
210
|
} else if n.Content != nil {
|
|
211
211
|
newTag := &Tag{
|
gsx/twx.go
CHANGED
|
@@ -419,8 +419,8 @@ var twClassLookup = MS{
|
|
|
419
419
|
"select-text": "user-select: text;",
|
|
420
420
|
"select-all": "user-select: all;",
|
|
421
421
|
"select-auto": "user-select: auto;",
|
|
422
|
-
"w-screen": "100vw",
|
|
422
|
+
"w-screen": "width: 100vw;",
|
|
423
|
-
"h-screen": "100vh",
|
|
423
|
+
"h-screen": "height: 100vh;",
|
|
424
424
|
"static": "position: static;",
|
|
425
425
|
"fixed": "position: fixed;",
|
|
426
426
|
"absolute": "position: absolute;",
|
http.go
CHANGED
|
@@ -19,15 +19,14 @@ import (
|
|
|
19
19
|
"time"
|
|
20
20
|
|
|
21
21
|
"github.com/felixge/httpsnoop"
|
|
22
|
-
"github.com/go-errors/errors"
|
|
23
22
|
"github.com/go-playground/validator/v10"
|
|
24
23
|
"github.com/google/uuid"
|
|
25
24
|
"github.com/gorilla/handlers"
|
|
26
25
|
"github.com/gorilla/mux"
|
|
27
26
|
"github.com/pyros2097/gromer/gsx"
|
|
27
|
+
"github.com/rotisserie/eris"
|
|
28
28
|
"github.com/rs/zerolog"
|
|
29
29
|
"github.com/rs/zerolog/log"
|
|
30
|
-
"github.com/rs/zerolog/pkgerrors"
|
|
31
30
|
"github.com/segmentio/go-camelcase"
|
|
32
31
|
"xojoc.pw/useragent"
|
|
33
32
|
)
|
|
@@ -57,7 +56,9 @@ type RouteDefinition struct {
|
|
|
57
56
|
func init() {
|
|
58
57
|
IsCloundRun = os.Getenv("K_REVISION") != ""
|
|
59
58
|
info, _ = debug.ReadBuildInfo()
|
|
60
|
-
zerolog.ErrorStackMarshaler =
|
|
59
|
+
zerolog.ErrorStackMarshaler = func(err error) interface{} {
|
|
60
|
+
return eris.ToJSON(err, true)
|
|
61
|
+
}
|
|
61
62
|
if IsCloundRun {
|
|
62
63
|
zerolog.LevelFieldName = "severity"
|
|
63
64
|
zerolog.TimestampFieldName = "timestamp"
|
|
@@ -79,22 +80,39 @@ func getFunctionName(temp interface{}) string {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
func RespondError(w http.ResponseWriter, status int, err error) {
|
|
82
|
-
w.Header().Set("Content-Type", "
|
|
83
|
+
w.Header().Set("Content-Type", "text/html")
|
|
83
84
|
w.WriteHeader(status) // always write status last
|
|
84
85
|
merror := map[string]interface{}{
|
|
85
86
|
"error": err.Error(),
|
|
86
87
|
}
|
|
87
88
|
if status >= 500 {
|
|
88
89
|
merror["error"] = "Internal Server Error"
|
|
90
|
+
formattedStr := eris.ToCustomString(err, eris.StringFormat{
|
|
89
|
-
|
|
91
|
+
Options: eris.FormatOptions{
|
|
92
|
+
WithTrace: true,
|
|
93
|
+
InvertOutput: true,
|
|
94
|
+
InvertTrace: true,
|
|
95
|
+
},
|
|
96
|
+
MsgStackSep: "\n",
|
|
97
|
+
PreStackSep: "\t",
|
|
98
|
+
StackElemSep: " | ",
|
|
99
|
+
ErrorSep: "\n",
|
|
100
|
+
})
|
|
90
|
-
log.Error().Msg(err.Error() + "\n" +
|
|
101
|
+
log.Error().Msg(err.Error() + "\n" + formattedStr)
|
|
91
102
|
}
|
|
92
103
|
validationErrors, ok := err.(validator.ValidationErrors)
|
|
93
104
|
if ok {
|
|
94
105
|
merror["error"] = GetValidationError(validationErrors)
|
|
95
106
|
}
|
|
107
|
+
c := gsx.NewContext(context.TODO(), &gsx.HX{})
|
|
108
|
+
c.Set("funcName", "error")
|
|
96
|
-
|
|
109
|
+
c.Set("error", err.Error())
|
|
110
|
+
tags := c.Render(`
|
|
111
|
+
<div style="color: red;">
|
|
112
|
+
<h1>{error}</h1>
|
|
113
|
+
</div>
|
|
114
|
+
`)
|
|
97
|
-
|
|
115
|
+
gsx.Write(c, w, tags)
|
|
98
116
|
}
|
|
99
117
|
|
|
100
118
|
func GetRouteParams(route string) []string {
|
|
@@ -179,7 +197,7 @@ func PerformRequest(route string, h interface{}, c *gsx.Context, w http.Response
|
|
|
179
197
|
return
|
|
180
198
|
}
|
|
181
199
|
} else {
|
|
182
|
-
RespondError(w, 400,
|
|
200
|
+
RespondError(w, 400, eris.Errorf("Illegal Content-Type tag found %s", contentType))
|
|
183
201
|
return
|
|
184
202
|
}
|
|
185
203
|
c.Set("params", instance.Elem().Interface())
|
|
@@ -190,7 +208,7 @@ func PerformRequest(route string, h interface{}, c *gsx.Context, w http.Response
|
|
|
190
208
|
responseStatus := values[1].Interface().(int)
|
|
191
209
|
responseError := values[2].Interface()
|
|
192
210
|
if responseError != nil {
|
|
193
|
-
RespondError(w, responseStatus, responseError.(error))
|
|
211
|
+
RespondError(w, responseStatus, eris.Wrap(responseError.(error), "Render failed"))
|
|
194
212
|
return
|
|
195
213
|
}
|
|
196
214
|
w.Header().Set("Content-Type", "text/html")
|
|
@@ -215,7 +233,7 @@ func LogMiddleware(next http.Handler) http.Handler {
|
|
|
215
233
|
defer func() {
|
|
216
234
|
if err := recover(); err != nil {
|
|
217
235
|
log.Error().Msgf("%s 599 %s %s", r.Method, ua, url)
|
|
218
|
-
RespondError(w, 599,
|
|
236
|
+
RespondError(w, 599, eris.Errorf("%+v", err))
|
|
219
237
|
}
|
|
220
238
|
}()
|
|
221
239
|
m := httpsnoop.CaptureMetrics(next, w, r)
|
|
@@ -237,37 +255,6 @@ func CacheMiddleware(next http.Handler) http.Handler {
|
|
|
237
255
|
})
|
|
238
256
|
}
|
|
239
257
|
|
|
240
|
-
func StatusHandler(h interface{}) http.Handler {
|
|
241
|
-
return LogMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
242
|
-
ctx := context.WithValue(context.WithValue(r.Context(), "url", r.URL), "header", r.Header)
|
|
243
|
-
var hx *gsx.HX
|
|
244
|
-
if r.Header.Get("HX-Request") == "true" {
|
|
245
|
-
hx = &gsx.HX{
|
|
246
|
-
Boosted: r.Header.Get("HX-Boosted") == "true",
|
|
247
|
-
CurrentUrl: r.Header.Get("HX-Current-URL"),
|
|
248
|
-
Prompt: r.Header.Get("HX-Prompt"),
|
|
249
|
-
Target: r.Header.Get("HX-Target"),
|
|
250
|
-
TriggerName: r.Header.Get("HX-Trigger-Name"),
|
|
251
|
-
TriggerID: r.Header.Get("HX-Trigger"),
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
renderContext := gsx.NewContext(ctx, hx)
|
|
255
|
-
values := reflect.ValueOf(h).Call([]reflect.Value{reflect.ValueOf(renderContext)})
|
|
256
|
-
response := values[0].Interface()
|
|
257
|
-
responseStatus := values[1].Interface().(int)
|
|
258
|
-
responseError := values[2].Interface()
|
|
259
|
-
if responseError != nil {
|
|
260
|
-
RespondError(w, responseStatus, responseError.(error))
|
|
261
|
-
return
|
|
262
|
-
}
|
|
263
|
-
w.Header().Set("Content-Type", "text/html")
|
|
264
|
-
|
|
265
|
-
// This has to be at end always after headers are set
|
|
266
|
-
w.WriteHeader(responseStatus)
|
|
267
|
-
gsx.Write(renderContext, w, response.([]*gsx.Tag))
|
|
268
|
-
})).(http.Handler)
|
|
269
|
-
}
|
|
270
|
-
|
|
271
258
|
func StaticRoute(router *mux.Router, path string, fs embed.FS) {
|
|
272
259
|
router.PathPrefix(path).Methods("GET").Handler(http.StripPrefix(path, http.FileServer(http.FS(fs))))
|
|
273
260
|
}
|
|
@@ -315,32 +302,46 @@ func ComponentStylesRoute(router *mux.Router, route string) {
|
|
|
315
302
|
})
|
|
316
303
|
}
|
|
317
304
|
|
|
305
|
+
func createCtx(r *http.Request, route, key string, meta, styles gsx.M) *gsx.Context {
|
|
306
|
+
newCtx := context.WithValue(context.WithValue(r.Context(), "url", r.URL), "header", r.Header)
|
|
307
|
+
var hx *gsx.HX
|
|
308
|
+
if r.Header.Get("HX-Request") == "true" {
|
|
309
|
+
hx = &gsx.HX{
|
|
310
|
+
Boosted: r.Header.Get("HX-Boosted") == "true",
|
|
311
|
+
CurrentUrl: r.Header.Get("HX-Current-URL"),
|
|
312
|
+
Prompt: r.Header.Get("HX-Prompt"),
|
|
313
|
+
Target: r.Header.Get("HX-Target"),
|
|
314
|
+
TriggerName: r.Header.Get("HX-Trigger-Name"),
|
|
315
|
+
TriggerID: r.Header.Get("HX-Trigger"),
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
c := gsx.NewContext(newCtx, hx)
|
|
319
|
+
c.Set("funcName", route)
|
|
320
|
+
c.Set("requestId", uuid.NewString())
|
|
321
|
+
c.Link("stylesheet", GetPageStylesUrl(key), "", "")
|
|
322
|
+
c.Link("stylesheet", GetComponentsStylesUrl(), "", "")
|
|
323
|
+
c.Link("icon", "/assets/favicon.ico", "image/x-icon", "image")
|
|
324
|
+
c.Script("/gromer/js/htmx@1.7.0.js", false)
|
|
325
|
+
c.Script("/gromer/js/hyperscript@0.9.6.js", false)
|
|
326
|
+
// c.Script("/gromer/js/alpinejs@3.9.6.js", true)
|
|
327
|
+
c.Meta(meta)
|
|
328
|
+
return c
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
func StatusHandler(status int, h interface{}, meta, styles gsx.M) http.Handler {
|
|
332
|
+
route := fmt.Sprintf("%d", status)
|
|
333
|
+
gsx.SetClasses(route, styles)
|
|
334
|
+
return LogMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
335
|
+
c := createCtx(r, route, route, meta, styles)
|
|
336
|
+
PerformRequest(route, h, c, w, r)
|
|
337
|
+
})).(http.Handler)
|
|
338
|
+
}
|
|
339
|
+
|
|
318
340
|
func Handle(router *mux.Router, method, route string, h interface{}, meta, styles gsx.M) {
|
|
319
341
|
key := camelcase.Camelcase(route)
|
|
320
342
|
gsx.SetClasses(key, styles)
|
|
321
343
|
router.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
|
|
322
|
-
newCtx := context.WithValue(context.WithValue(r.Context(), "url", r.URL), "header", r.Header)
|
|
323
|
-
var hx *gsx.HX
|
|
324
|
-
if r.Header.Get("HX-Request") == "true" {
|
|
325
|
-
hx = &gsx.HX{
|
|
326
|
-
Boosted: r.Header.Get("HX-Boosted") == "true",
|
|
327
|
-
CurrentUrl: r.Header.Get("HX-Current-URL"),
|
|
328
|
-
Prompt: r.Header.Get("HX-Prompt"),
|
|
329
|
-
Target: r.Header.Get("HX-Target"),
|
|
330
|
-
TriggerName: r.Header.Get("HX-Trigger-Name"),
|
|
331
|
-
TriggerID: r.Header.Get("HX-Trigger"),
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
c :=
|
|
344
|
+
c := createCtx(r, route, key, meta, styles)
|
|
335
|
-
c.Set("funcName", route)
|
|
336
|
-
c.Set("requestId", uuid.NewString())
|
|
337
|
-
c.Link("stylesheet", GetPageStylesUrl(key), "", "")
|
|
338
|
-
c.Link("stylesheet", GetComponentsStylesUrl(), "", "")
|
|
339
|
-
c.Link("icon", "/assets/favicon.ico", "image/x-icon", "image")
|
|
340
|
-
c.Script("/gromer/js/htmx@1.7.0.js", false)
|
|
341
|
-
c.Script("/gromer/js/hyperscript@0.9.6.js", false)
|
|
342
|
-
// c.Script("/gromer/js/alpinejs@3.9.6.js", true)
|
|
343
|
-
c.Meta(meta)
|
|
344
345
|
PerformRequest(route, h, c, w, r)
|
|
345
346
|
}).Methods(method)
|
|
346
347
|
}
|