Skip to content

Commit

Permalink
add documentation to readme
Browse files Browse the repository at this point in the history
  • Loading branch information
crhntr committed Nov 18, 2024
1 parent 915b0bf commit 0f4afca
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 24 deletions.
109 changes: 107 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)" }}
<h1>{{.Hello}}</h1>
{{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

Expand Down
4 changes: 2 additions & 2 deletions routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
24 changes: 12 additions & 12 deletions template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}{}
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -118,7 +118,7 @@ func newTemplate(in string) (Template, error, bool) {

var (
pathSegmentPattern = regexp.MustCompile(`/\{([^}]*)}`)
templateNameMux = regexp.MustCompile(`^(?P<endpoint>(((?P<method>[A-Z]+)\s+)?)(?P<host>([^/])*)(?P<path>(/(\S)*)))(\s+(?P<code>(\d|http\.Status)\S+))?(?P<handler>.*)?$`)
templateNameMux = regexp.MustCompile(`^(?P<pattern>(((?P<METHOD>[A-Z]+)\s+)?)(?P<HOST>([^/])*)(?P<PATH>(/(\S)*)))(\s+(?P<HTTP_STATUS>(\d|http\.Status)\S+))?(?P<CALL>.*)?$`)
)

func (t Template) parsePathValueNames() []string {
Expand Down Expand Up @@ -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},
}}
}

Expand All @@ -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 {
Expand Down
14 changes: 7 additions & 7 deletions template_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
},
Expand All @@ -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)
},
},
Expand All @@ -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)
},
},
Expand All @@ -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)
},
},
Expand All @@ -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)
},
},
Expand All @@ -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)
},
},
Expand All @@ -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)
},
},
Expand Down

0 comments on commit 0f4afca

Please sign in to comment.