summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMathias Magnusson <mathias@magnusson.space>2024-11-15 12:14:28 +0100
committerMathias Magnusson <mathias@magnusson.space>2024-11-15 12:14:28 +0100
commit14db9705ae1c2a10bb5e62a66a72f5cbc7aa7b13 (patch)
treece40cde93704fcd6c811a7d70b518ae522f342ea
parentaed837bec26d2855096b33bb249ddac36a10ad8f (diff)
downloadhh-14db9705ae1c2a10bb5e62a66a72f5cbc7aa7b13.tar.gz
Add some unfinished work in progress code
-rw-r--r--cmd/generate/main.go170
-rw-r--r--examples/basic.go17
-rw-r--r--go.mod3
-rw-r--r--hh.go14
4 files changed, 204 insertions, 0 deletions
diff --git a/cmd/generate/main.go b/cmd/generate/main.go
new file mode 100644
index 0000000..9f1f839
--- /dev/null
+++ b/cmd/generate/main.go
@@ -0,0 +1,170 @@
+package main
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "go/ast"
+ "go/parser"
+ "go/scanner"
+ "go/token"
+ "net/http"
+ "strconv"
+ "strings"
+)
+
+func main() {
+ if err := run(); err != nil {
+ panic(err)
+ }
+}
+
+func run() error {
+ var fset token.FileSet
+ f, err := parser.ParseFile(&fset, "examples/basic.go", nil, parser.ParseComments|parser.SkipObjectResolution)
+ if err != nil {
+ return err
+ }
+ for _, decl := range f.Decls {
+ f, ok := decl.(*ast.FuncDecl)
+ if !ok {
+ continue
+ }
+ hhRoute := f.Doc.List[len(f.Doc.List)-1].Text
+ var routeSpec string
+ if routeSpec, ok = strings.CutPrefix(hhRoute, "//hh:route "); !ok {
+ continue
+ }
+ rs, err := parseRouteSpec(routeSpec)
+ if err != nil {
+ return err
+ }
+ var wrapper bytes.Buffer
+ wrapper.WriteString("func httpWrapper_")
+ wrapper.WriteString(f.Name.String())
+ wrapper.WriteString("[S any](s S, r *http.Request, w http.ResponseWriter) {")
+ i := 0
+ for _, p := range f.Type.Params.List {
+ for _, n := range p.Names {
+ wrapper.WriteString("\n\tvar")
+ wrapper.WriteString(strconv.Itoa(i))
+ wrapper.WriteString(" := ")
+ wrapper.WriteString(n.Name)
+ i++
+ }
+ }
+ wrapper.WriteString("\t")
+ wrapper.WriteString(f.Name.Name)
+ wrapper.WriteString("(")
+ i = 0
+ for _, p := range f.Type.Params.List {
+ for range p.Names {
+ if i > 0 {
+ wrapper.WriteString(", ")
+ }
+ wrapper.WriteString("var")
+ wrapper.WriteString(strconv.Itoa(i))
+ i++
+ }
+ }
+ wrapper.WriteString(")\n")
+ wrapper.WriteString("}\n")
+
+ fmt.Printf("`%v`\n`%v`\n`%v`\n", routeSpec, f.Name.Name, rs)
+ }
+ return nil
+}
+
+type routeSpec struct {
+ method string
+ path string
+ parameters []routeSpecParam
+}
+
+type routeSpecParam struct {
+ name string
+ extractor string
+}
+
+func parseRouteSpec(s string) (routeSpec, error) {
+ s = strings.TrimRight(s, " \n")
+ var rs routeSpec
+ for _, method := range []string{
+ http.MethodGet,
+ http.MethodHead,
+ http.MethodPost,
+ http.MethodPut,
+ http.MethodPatch,
+ http.MethodDelete,
+ http.MethodConnect,
+ http.MethodOptions,
+ http.MethodTrace,
+ } {
+ if rest, ok := strings.CutPrefix(s, method+" "); ok {
+ s = rest
+ rs.method = method
+ break
+ }
+ }
+
+ for {
+ if commaPos := strings.IndexByte(s, ','); commaPos != -1 {
+ rs.path = s[:commaPos]
+ s = s[commaPos+1:]
+ } else {
+ rs.path = s
+ return rs, nil
+ }
+
+ s = strings.TrimLeft(s, " ")
+ if s == "" {
+ break
+ }
+
+ end := -1
+ for i := 0; i < len(s); i++ {
+ if 'a' <= s[i] && s[i] <= 'z' ||
+ 'A' <= s[i] && s[i] <= 'Z' ||
+ s[i] == '_' {
+ continue
+ }
+ if s[i] == ':' {
+ end = i
+ break
+ }
+ return rs, errors.New("Expected ':' to mark end of parameter name, got " + s[i:i+1])
+ }
+ if end == -1 {
+ return rs, errors.New("Expected ':' to mark end of parameter name, got end of line")
+ }
+ var p routeSpecParam
+ p.name = s[:end]
+ s = s[end+1:]
+
+ s = strings.TrimLeft(s, " ")
+
+ expr, err := parser.ParseExpr(s)
+ if el, ok := err.(scanner.ErrorList); err == nil || ok && el[0].Msg == "expected 'EOF', found ','" {
+ switch expr := expr.(type) {
+ case *ast.Ident:
+ switch expr.Name {
+ case "query":
+ p.extractor = ":" + expr.Name
+ default:
+ return rs, errors.New("Unexpected extractor " + expr.Name)
+ }
+ case *ast.CallExpr:
+ p.extractor = s[expr.Pos()-1 : expr.End()-1]
+ default:
+ return rs, errors.New("Unexpected extractor" + s[expr.Pos()-1:expr.End()-1])
+ }
+ } else {
+ return rs, err
+ }
+ s = s[expr.End()-1:]
+
+ rs.parameters = append(rs.parameters, p)
+ }
+
+ return rs, nil
+}
diff --git a/examples/basic.go b/examples/basic.go
new file mode 100644
index 0000000..467460f
--- /dev/null
+++ b/examples/basic.go
@@ -0,0 +1,17 @@
+package examples
+
+import (
+ "log/slog"
+ "net/http"
+)
+
+// Big bungus function here!
+//
+//hh:route GET /admin/users, query: query, nextURL: hh.Cookie(r, "logout_next_url")
+func adminUsersForm(query struct {
+ search, year string
+ offset int
+}, w http.ResponseWriter) {
+ _, _ = w.Write([]byte("ahahaha"))
+ slog.Info("get admin users form", "search", query.search, "offset", query.offset)
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..188be38
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module codeberg.org/foodelevator/hh
+
+go 1.22.6
diff --git a/hh.go b/hh.go
new file mode 100644
index 0000000..e6593ed
--- /dev/null
+++ b/hh.go
@@ -0,0 +1,14 @@
+package hh
+
+import (
+ "errors"
+ "net/http"
+)
+
+func Cookie(r http.Request, name string) (string, error) {
+ cookie, _ := r.Cookie(name)
+ if cookie == nil {
+ return "", errors.New("Bad request: missing cookie " + name)
+ }
+ return cookie.Value, nil
+}