summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2024-09-27 00:25:09 +0700
committerShulhan <ms@kilabit.info>2024-09-30 23:04:56 +0700
commit69966b7be999223dc7a1ab0c5a3339010e11433c (patch)
treef14eecef9f91f3dbd20472af21d8fe1254d83b3a
parenta0f3864d5dc77e8fb496fc88fd48a6a7869ef000 (diff)
downloadpakakeh.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.go47
-rw-r--r--lib/http/server_example_test.go41
-rw-r--r--lib/http/server_test.go127
-rw-r--r--lib/http/testdata/Server_RegisterHandleFunc_test.txt34
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]]