From 0f4afcaeee90592adfccf1ad158d975c71f023c9 Mon Sep 17 00:00:00 2001
From: Chrstopher Hunter <8398225+crhntr@users.noreply.github.com>
Date: Sun, 17 Nov 2024 18:05:13 -0800
Subject: [PATCH] add documentation to readme
---
README.md | 109 +++++++++++++++++++++++++++++++++++++-
routes.go | 4 +-
routes_test.go | 2 +-
template.go | 24 ++++-----
template_internal_test.go | 14 ++---
5 files changed, 129 insertions(+), 24 deletions(-)
diff --git a/README.md b/README.md
index f8525a4..f7e74d1 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,113 @@
**Early WIP (not yet tested in prod)**
-Sometimes as a developer it is nice to stay in an HTML headspace.
-This tool helps you do that and generates helpful test seams.
+Sometimes as a developer it is nice to stay in an HTML headspace. This Go code generator helps you do that.
+It also provides a nice test seam between your http and endpoint handlers.
+
+Muxt generates Go code. It does not require you to add any dependencies outside the Go standard library.
+
+- It allows you to register HTTP routes from [HTML templates](https://pkg.go.dev/html/template)
+- It generates handler functions and registers them on an [`*http.ServeMux`](https://pkg.go.dev/net/http#ServeMux)
+- It generates code in handler functions to parse path parameters and form fields
+- It generates a receiver interface to represent the boundary between your app code and HTTP/HTML
+ - Use this to mock out your server and test the view layer of your application
+
+## Installation
+
+You can install it using the Go toolchain.
+```bash
+cd # Change outside of your module (so you don't add muxt to your dependency chain)
+go install github.com/crhntr/muxt@latest
+cd -
+```
+
+You do not need to add this tool to your module ([unless you want to use the tools pattern](https://play-with-go.dev/tools-as-dependencies_go119_en/)).
+
+## Usage
+
+### Templates
+
+`muxt generate` will read your HTML templates and generate and register [`http.HandlerFunc`](https://pkg.go.dev/net/http#HandlerFunc)
+on a for templates with names that an expected patten.
+
+Since Go 1.22, the standard library route **mu**ltiple**x**er can parse path parameters.
+
+It has expects strings like this
+
+`[METHOD ][HOST]/[PATH]`
+
+Muxt extends this a little bit.
+
+`[METHOD ][HOST]/[PATH ][HTTP_STATUS ][CALL]`
+
+A template name that muxt understands looks like this:
+
+```gotemplate
+{{define "GET /greet/{language} 200 Greeting(ctx, language)" }}
+
{{.Hello}}
+{{end}}
+```
+
+In this template name
+- Passed through to `http.ServeMux`
+ - we define the HTTP Method `GET`0,
+ - the path prefix `/greet/`
+ - the path parameter called `language` (available in the call scope as `language`)
+- Used by muxt to generate a `http.HandlerFunc`
+ - the status code to use when muxt calls WriteHeader is `200` aka `http.StatusOK`
+ - the method name on the configured receiver to call is `Greeting`
+ - the parameters to pass to `Greeting` are `ctx` and `language`
+
+#### [`*http.ServeMux`](https://pkg.go.dev/net/http#ServeMux) PatternsĀ¶
+
+Here is an excerpt from [the standard libary documentation.](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)
+
+> Patterns can match the method, host and path of a request. Some examples:
+> - "/index.html" matches the path "/index.html" for any host and method.
+> - "GET /static/" matches a GET request whose path begins with "/static/".
+> - "example.com/" matches any request to the host "example.com".
+> - "example.com/{$}" matches requests with host "example.com" and path "/".
+> - "/b/{bucket}/o/{objectname...}" matches paths whose first segment is "b" and whose third segment is "o". The name "bucket" denotes the second segment and "objectname" denotes the remainder of the path.
+
+#### The Method Call Scope
+
+There are three parameters you can pass to a method that always generate the same code
+
+- `ctx` -> `http.Request.Context`
+- `request` -> `*http.Request`
+- `response` -> `http.ResponseWriter` (if you use this, muxt won't generate code to call WriteHeader, you have to do this)
+
+Using these three, the generated code will look something like this.
+
+Given `{{define "GET / F(ctx, response, request)"}}Hello{{end}}`,
+
+You will get a handler generated like this:
+
+```
+mux.HandleFunc("GET /", func(response http.ResponseWriter, request *http.Request) {
+ ctx := request.Context()
+ data := receiver.F(ctx, resposne, request)
+ execute(response, request, false, "GET / F(ctx, response, request)", http.StatusOK, data)
+})
+```
+
+You can also map path values from the path pattern to identifiers and pass them to your handler.
+
+
+Given `{{define "GET /articles/:id ReadArticle(ctx, id)"}}{{end}}`,
+
+You will get a handler generated like this:
+
+```
+mux.HandleFunc("GET /", func(response http.ResponseWriter, request *http.Request) {
+ ctx := request.Context()
+ id := request.PathValue("id")
+ data := receiver.ReadArticle(ctx, id)
+ execute(response, request, true, "GET /articles/:id ReadArticle(ctx, id)", http.StatusOK, data)
+})
+```
+
+_TODO add more documentation on form and typed arguments_
## Examples
diff --git a/routes.go b/routes.go
index b77967f..0470dc4 100644
--- a/routes.go
+++ b/routes.go
@@ -138,7 +138,7 @@ func TemplateRoutesFile(wd string, templates []Template, logger *log.Logger, con
}
for _, t := range templates {
- logger.Printf("routes has route for %s", t.endpoint)
+ logger.Printf("routes has route for %s", t.pattern)
if t.fun == nil {
hf := t.httpRequestReceiverTemplateHandlerFunc(imports, t.statusCode)
routesFunc.Body.List = append(routesFunc.Body.List, t.callHandleFunc(hf))
@@ -158,7 +158,7 @@ func TemplateRoutesFile(wd string, templates []Template, logger *log.Logger, con
}
sig := methodObj.Type().(*types.Signature)
if sig.Results().Len() == 0 {
- return "", fmt.Errorf("method for endpoint %q has no results it should have one or two", t.name)
+ return "", fmt.Errorf("method for pattern %q has no results it should have one or two", t.name)
}
if handlerFunc.Body.List, err = appendParseArgumentStatements(handlerFunc.Body.List, t, imports, nil, receiver, t.call); err != nil {
return "", err
diff --git a/routes_test.go b/routes_test.go
index b3cd7d8..4d26381 100644
--- a/routes_test.go
+++ b/routes_test.go
@@ -1595,7 +1595,7 @@ type T struct{}
func (T) F() {}
` + executeGo,
- ExpectedError: `method for endpoint "GET / F()" has no results it should have one or two`,
+ ExpectedError: `method for pattern "GET / F()" has no results it should have one or two`,
},
{
Name: "wrong argument type path value",
diff --git a/template.go b/template.go
index 33d8a7d..afdef37 100644
--- a/template.go
+++ b/template.go
@@ -28,7 +28,7 @@ func Templates(ts *template.Template) ([]Template, error) {
return templateNames, err
}
if _, exists := routes[mt.method+mt.path]; exists {
- return templateNames, fmt.Errorf("duplicate route pattern: %s", mt.endpoint)
+ return templateNames, fmt.Errorf("duplicate route pattern: %s", mt.pattern)
}
mt.template = t
routes[mt.method+mt.path] = struct{}{}
@@ -42,8 +42,8 @@ type Template struct {
// name has the full unaltered template name
name string
- // method, host, path, and endpoint are parsed sub-parts of the string passed to mux.Handle
- method, host, path, endpoint string
+ // method, host, path, and pattern are parsed sub-parts of the string passed to mux.Handle
+ method, host, path, pattern string
// handler is used to generate the method interface
handler string
@@ -68,15 +68,15 @@ func newTemplate(in string) (Template, error, bool) {
matches := templateNameMux.FindStringSubmatch(in)
p := Template{
name: in,
- method: matches[templateNameMux.SubexpIndex("method")],
- host: matches[templateNameMux.SubexpIndex("host")],
- path: matches[templateNameMux.SubexpIndex("path")],
- handler: strings.TrimSpace(matches[templateNameMux.SubexpIndex("handler")]),
- endpoint: matches[templateNameMux.SubexpIndex("endpoint")],
+ method: matches[templateNameMux.SubexpIndex("METHOD")],
+ host: matches[templateNameMux.SubexpIndex("HOST")],
+ path: matches[templateNameMux.SubexpIndex("PATH")],
+ handler: strings.TrimSpace(matches[templateNameMux.SubexpIndex("CALL")]),
+ pattern: matches[templateNameMux.SubexpIndex("pattern")],
fileSet: token.NewFileSet(),
statusCode: http.StatusOK,
}
- httpStatusCode := matches[templateNameMux.SubexpIndex("code")]
+ httpStatusCode := matches[templateNameMux.SubexpIndex("HTTP_STATUS")]
if httpStatusCode != "" {
if strings.HasPrefix(httpStatusCode, "http.Status") {
code, err := source.HTTPStatusName(httpStatusCode)
@@ -118,7 +118,7 @@ func newTemplate(in string) (Template, error, bool) {
var (
pathSegmentPattern = regexp.MustCompile(`/\{([^}]*)}`)
- templateNameMux = regexp.MustCompile(`^(?P(((?P[A-Z]+)\s+)?)(?P([^/])*)(?P(/(\S)*)))(\s+(?P(\d|http\.Status)\S+))?(?P.*)?$`)
+ templateNameMux = regexp.MustCompile(`^(?P(((?P[A-Z]+)\s+)?)(?P([^/])*)(?P(/(\S)*)))(\s+(?P(\d|http\.Status)\S+))?(?P.*)?$`)
)
func (t Template) parsePathValueNames() []string {
@@ -303,7 +303,7 @@ func (t Template) callHandleFunc(handlerFuncLit *ast.FuncLit) *ast.ExprStmt {
X: ast.NewIdent(muxVarIdent),
Sel: ast.NewIdent(httpHandleFuncIdent),
},
- Args: []ast.Expr{source.String(t.endpoint), handlerFuncLit},
+ Args: []ast.Expr{source.String(t.pattern), handlerFuncLit},
}}
}
@@ -312,7 +312,7 @@ func (t Template) callReceiverMethod(imports *source.Imports, dataVarIdent strin
okIdent = "ok"
)
if method.Results == nil || len(method.Results.List) == 0 {
- return nil, fmt.Errorf("method for endpoint %q has no results it should have one or two", t)
+ return nil, fmt.Errorf("method for pattern %q has no results it should have one or two", t)
} else if len(method.Results.List) > 1 {
_, lastResultType, ok := source.FieldIndex(method.Results.List, method.Results.NumFields()-1)
if !ok {
diff --git a/template_internal_test.go b/template_internal_test.go
index 5d36a78..ca00c98 100644
--- a/template_internal_test.go
+++ b/template_internal_test.go
@@ -130,7 +130,7 @@ func TestNewTemplateName(t *testing.T) {
assert.Equal(t, http.MethodGet, pat.method)
assert.Equal(t, "", pat.host)
assert.Equal(t, "/", pat.path)
- assert.Equal(t, "GET /", pat.endpoint)
+ assert.Equal(t, "GET /", pat.pattern)
assert.Equal(t, "", pat.handler)
},
},
@@ -142,7 +142,7 @@ func TestNewTemplateName(t *testing.T) {
assert.Equal(t, http.MethodGet, pat.method)
assert.Equal(t, "", pat.host)
assert.Equal(t, "/", pat.path)
- assert.Equal(t, "GET /", pat.endpoint)
+ assert.Equal(t, "GET /", pat.pattern)
assert.Equal(t, "", pat.handler)
},
},
@@ -154,7 +154,7 @@ func TestNewTemplateName(t *testing.T) {
assert.Equal(t, http.MethodPost, pat.method)
assert.Equal(t, "", pat.host)
assert.Equal(t, "/", pat.path)
- assert.Equal(t, "POST /", pat.endpoint)
+ assert.Equal(t, "POST /", pat.pattern)
assert.Equal(t, "", pat.handler)
},
},
@@ -166,7 +166,7 @@ func TestNewTemplateName(t *testing.T) {
assert.Equal(t, http.MethodPatch, pat.method)
assert.Equal(t, "", pat.host)
assert.Equal(t, "/", pat.path)
- assert.Equal(t, "PATCH /", pat.endpoint)
+ assert.Equal(t, "PATCH /", pat.pattern)
assert.Equal(t, "", pat.handler)
},
},
@@ -178,7 +178,7 @@ func TestNewTemplateName(t *testing.T) {
assert.Equal(t, http.MethodDelete, pat.method)
assert.Equal(t, "", pat.host)
assert.Equal(t, "/", pat.path)
- assert.Equal(t, "DELETE /", pat.endpoint)
+ assert.Equal(t, "DELETE /", pat.pattern)
assert.Equal(t, "", pat.handler)
},
},
@@ -190,7 +190,7 @@ func TestNewTemplateName(t *testing.T) {
assert.Equal(t, http.MethodPut, pat.method)
assert.Equal(t, "", pat.host)
assert.Equal(t, "/", pat.path)
- assert.Equal(t, "PUT /", pat.endpoint)
+ assert.Equal(t, "PUT /", pat.pattern)
assert.Equal(t, "", pat.handler)
},
},
@@ -202,7 +202,7 @@ func TestNewTemplateName(t *testing.T) {
assert.Equal(t, http.MethodPut, pat.method)
assert.Equal(t, "", pat.host)
assert.Equal(t, "/ping/pong/{$}", pat.path)
- assert.Equal(t, "PUT /ping/pong/{$}", pat.endpoint)
+ assert.Equal(t, "PUT /ping/pong/{$}", pat.pattern)
assert.Equal(t, "", pat.handler)
},
},