diff options
| author | Shulhan <ms@kilabit.info> | 2024-09-27 00:25:09 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2024-09-30 23:04:56 +0700 |
| commit | 69966b7be999223dc7a1ab0c5a3339010e11433c (patch) | |
| tree | f14eecef9f91f3dbd20472af21d8fe1254d83b3a | |
| parent | a0f3864d5dc77e8fb496fc88fd48a6a7869ef000 (diff) | |
| download | pakakeh.go-69966b7be999223dc7a1ab0c5a3339010e11433c.tar.xz | |
lib/http: add Server method to register handler by function
The RegisterHandleFunc register a pattern with a handler, similar to
[http.ServeMux.HandleFunc].
The pattern follow the Go 1.22 format:
[METHOD] PATH
The METHOD is optional, default to GET.
The PATH must not contains the domain name and space.
Unlike standard library, variable in PATH is read using ":var" not
"{var}".
This endpoint will accept any content type and return the body as is;
it is up to the handler to read and set the content type and the response
headers.
If the METHOD and/or PATH is already registered it will panic.
| -rw-r--r-- | lib/http/server.go | 47 | ||||
| -rw-r--r-- | lib/http/server_example_test.go | 41 | ||||
| -rw-r--r-- | lib/http/server_test.go | 127 | ||||
| -rw-r--r-- | lib/http/testdata/Server_RegisterHandleFunc_test.txt | 34 |
4 files changed, 238 insertions, 11 deletions
diff --git a/lib/http/server.go b/lib/http/server.go index 4ca505e4..a9761582 100644 --- a/lib/http/server.go +++ b/lib/http/server.go @@ -1,6 +1,6 @@ -// Copyright 2018, Shulhan <ms@kilabit.info>. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// SPDX-FileCopyrightText: 2018 M. Shulhan <ms@kilabit.info> +// +// SPDX-License-Identifier: BSD-3-Clause package http @@ -131,6 +131,47 @@ func (srv *Server) RegisterEndpoint(ep Endpoint) (err error) { return nil } +// RegisterHandlerFunc register a pattern with a handler, similar to +// [http.ServeMux.HandleFunc]. +// The pattern follow the Go 1.22 format: +// +// [METHOD] PATH +// +// The METHOD is optional, default to GET. +// The PATH must not contains the domain name and space. +// Unlike standard library, variable in PATH is read using ":var" not +// "{var}". +// This endpoint will accept any content type and return the body as is; +// it is up to the handler to read and set the content type and the response +// headers. +// +// If the METHOD and/or PATH is already registered it will panic. +func (srv *Server) RegisterHandleFunc( + pattern string, + handler func(http.ResponseWriter, *http.Request), +) { + var ( + logp = `RegisterHandleFunc` + methodPath = strings.Fields(pattern) + ep = Endpoint{ + Call: func(epr *EndpointRequest) (resp []byte, err error) { + handler(epr.HTTPWriter, epr.HTTPRequest) + return nil, nil + }, + } + ) + if len(methodPath) == 1 { + ep.Path = methodPath[0] + } else if len(methodPath) > 1 { + ep.Method = RequestMethod(strings.ToUpper(methodPath[0])) + ep.Path = methodPath[1] + } + var err = srv.RegisterEndpoint(ep) + if err != nil { + log.Panicf(`%s: %s %q`, logp, err, ep.Path) + } +} + // RegisterSSE register Server-Sent Events endpoint. // It will return an error if the [SSEEndpoint.Call] field is not set or // [ErrEndpointAmbiguous] if the same path is already registered. diff --git a/lib/http/server_example_test.go b/lib/http/server_example_test.go index 048db1a7..3fc4f3c6 100644 --- a/lib/http/server_example_test.go +++ b/lib/http/server_example_test.go @@ -1,14 +1,17 @@ -// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// SPDX-FileCopyrightText: 2020 M. Shulhan <ms@kilabit.info> +// +// SPDX-License-Identifier: BSD-3-Clause package http import ( + "bytes" "encoding/json" "fmt" + "io" "log" "net/http" + "net/http/httptest" "time" ) @@ -91,3 +94,35 @@ func ExampleServer_customHTTPStatusCode() { // 400 // {"status":400} } + +func ExampleServer_RegisterHandleFunc() { + var serverOpts = ServerOptions{} + server, _ := NewServer(serverOpts) + server.RegisterHandleFunc(`PUT /api/book/:id`, + func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + fmt.Fprintf(w, "Request.URL: %s\n", r.URL) + fmt.Fprintf(w, "Request.Form: %+v\n", r.Form) + fmt.Fprintf(w, "Request.PostForm: %+v\n", r.PostForm) + }, + ) + + var respRec = httptest.NewRecorder() + + var body = []byte(`title=BahasaPemrogramanGo&author=Shulhan`) + var req = httptest.NewRequest(`PUT`, `/api/book/123`, bytes.NewReader(body)) + req.Header.Set(`Content-Type`, `application/x-www-form-urlencoded`) + + server.ServeHTTP(respRec, req) + + var resp = respRec.Result() + + body, _ = io.ReadAll(resp.Body) + fmt.Println(resp.Status) + fmt.Printf("%s", body) + // Output: + // 200 OK + // Request.URL: /api/book/123 + // Request.Form: map[id:[123]] + // Request.PostForm: map[author:[Shulhan] title:[BahasaPemrogramanGo]] +} diff --git a/lib/http/server_test.go b/lib/http/server_test.go index 0c2851a7..8961bace 100644 --- a/lib/http/server_test.go +++ b/lib/http/server_test.go @@ -13,6 +13,8 @@ import ( "log" "mime" "net/http" + "net/http/httptest" + "net/http/httputil" "os" "path/filepath" "strings" @@ -23,7 +25,7 @@ import ( "git.sr.ht/~shulhan/pakakeh.go/lib/memfs" libnet "git.sr.ht/~shulhan/pakakeh.go/lib/net" "git.sr.ht/~shulhan/pakakeh.go/lib/test" - "git.sr.ht/~shulhan/pakakeh.go/lib/test/httptest" + libhttptest "git.sr.ht/~shulhan/pakakeh.go/lib/test/httptest" ) func TestRegisterDelete(t *testing.T) { @@ -953,7 +955,7 @@ func TestServer_Options_HandleFS(t *testing.T) { func TestServer_handleDelete(t *testing.T) { type testCase struct { tag string - req httptest.SimulateRequest + req libhttptest.SimulateRequest } var ( @@ -985,20 +987,20 @@ func TestServer_handleDelete(t *testing.T) { var listCase = []testCase{{ tag: `valid`, - req: httptest.SimulateRequest{ + req: libhttptest.SimulateRequest{ Method: http.MethodDelete, Path: `/a/b/c/dddd/e`, }, }} var ( c testCase - result *httptest.SimulateResult + result *libhttptest.SimulateResult tag string exp string got []byte ) for _, c = range listCase { - result, err = httptest.Simulate(srv.ServeHTTP, &c.req) + result, err = libhttptest.Simulate(srv.ServeHTTP, &c.req) if err != nil { t.Fatal(err) } @@ -1252,6 +1254,108 @@ func TestServerHandleRangeBig(t *testing.T) { test.Assert(t, tag+`- response body size`, DefRangeLimit, len(res.Body)) } +func TestServer_RegisterHandleFunc(t *testing.T) { + var ( + serverOpts = ServerOptions{} + + server *Server + err error + ) + server, err = NewServer(serverOpts) + if err != nil { + t.Fatal(err) + } + server.RegisterHandleFunc(`/no/method`, testHandleFunc) + server.RegisterHandleFunc(`PUT /book/:id`, testHandleFunc) + + type testCase struct { + request *http.Request + tag string + } + var listCase = []testCase{{ + tag: `GET /no/method`, + request: mustHTTPRequest(`GET`, `/no/method`, nil), + }, { + tag: `POST /no/method`, + request: mustHTTPRequest(`POST`, `/no/method`, nil), + }, { + tag: `PUT /book/1`, + request: mustHTTPRequest(`PUT`, `/book/1`, nil), + }} + + var tdata *test.Data + tdata, err = test.LoadData(`testdata/Server_RegisterHandleFunc_test.txt`) + if err != nil { + t.Fatal(err) + } + + var ( + tcase testCase + httpResp *http.Response + got []byte + exp string + ) + for _, tcase = range listCase { + var respRec = httptest.NewRecorder() + server.ServeHTTP(respRec, tcase.request) + + httpResp = respRec.Result() + + got, err = httputil.DumpResponse(httpResp, true) + if err != nil { + t.Fatal(err) + } + got = bytes.ReplaceAll(got, []byte("\r"), []byte("")) + exp = string(tdata.Output[tcase.tag]) + test.Assert(t, tcase.tag, exp, string(got)) + } +} + +func testHandleFunc(httpwriter http.ResponseWriter, httpreq *http.Request) { + var ( + rawb []byte + err error + ) + rawb, err = httputil.DumpRequest(httpreq, true) + if err != nil { + log.Fatalf(`%s: %s`, httpreq.URL, err) + } + httpwriter.Write(rawb) + fmt.Fprintf(httpwriter, `Form: %+v`, httpreq.Form) +} + +func TestServer_RegisterHandleFunc_duplicate(t *testing.T) { + var ( + serverOpts = ServerOptions{} + + server *Server + err error + ) + server, err = NewServer(serverOpts) + if err != nil { + t.Fatal(err) + } + + defer func() { + var msg = recover() + test.Assert(t, `recover on duplicate pattern`, + `RegisterHandleFunc: RegisterEndpoint: ambigous endpoint "/no/method"`, + msg, + ) + }() + + server.RegisterHandleFunc(`/no/method`, + func(httpwriter http.ResponseWriter, httpreq *http.Request) { + return + }, + ) + server.RegisterHandleFunc(`GET /no/method`, + func(httpwriter http.ResponseWriter, httpreq *http.Request) { + return + }, + ) +} + func createBigFile(t *testing.T, path string, size int64) { var ( fbig *os.File @@ -1274,6 +1378,19 @@ func createBigFile(t *testing.T, path string, size int64) { } } +func mustHTTPRequest(method, url string, body []byte) (httpreq *http.Request) { + var ( + reqbody = bytes.NewBuffer(body) + err error + ) + + httpreq, err = http.NewRequest(method, url, io.NopCloser(reqbody)) + if err != nil { + panic(err.Error()) + } + return httpreq +} + func runServerFS(t *testing.T, address, dir string) (srv *Server) { var ( mfsOpts = &memfs.Options{ diff --git a/lib/http/testdata/Server_RegisterHandleFunc_test.txt b/lib/http/testdata/Server_RegisterHandleFunc_test.txt new file mode 100644 index 00000000..07d61cf4 --- /dev/null +++ b/lib/http/testdata/Server_RegisterHandleFunc_test.txt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2024 M. Shulhan <ms@kilabit.info> +// +// SPDX-License-Identifier: BSD-3-Clause + +>>> GET /no/method + +<<< GET /no/method +HTTP/1.1 200 OK +Connection: close +Content-Type: text/plain; charset=utf-8 + +GET /no/method HTTP/1.1 + +Form: map[] + + +>>> POST /no/method + +<<< POST /no/method +HTTP/1.1 404 Not Found +Connection: close + + + +>>> PUT /book/1 + +<<< PUT /book/1 +HTTP/1.1 200 OK +Connection: close +Content-Type: text/plain; charset=utf-8 + +PUT /book/1 HTTP/1.1 + +Form: map[id:[1]] |
