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) }, },