From 28eb9bc668825c3ec49cee2903776710a5971221 Mon Sep 17 00:00:00 2001 From: qm012 <67568757+qm012@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:19:08 +0800 Subject: [PATCH 1/7] add readme Signed-off-by: qm012 <67568757+qm012@users.noreply.github.com> --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++------------ sim.go | 19 ++++++++++++++++++ 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 7db4e93..3858162 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,14 @@ native performance untouched. Simple, not simplistic. See the [package documentation](https://pkg.go.dev/github.com/qm012/sim) for the full struct-tag rules. +**Response helpers** + +- Write complete responses in one call: `JSON`, `XML`, `Text`, `Bytes`, + `Stream` and `Attachment` +- JSON options: `EscapeForHTML` for safe HTML embedding, `Indented` + for readable output +- `Stream` for chunked large bodies, `Attachment` for file downloads + ## Installation Requires Go 1.26+. @@ -79,7 +87,7 @@ import ( func main() { app := sim.Default() app.Get("/", func(w http.ResponseWriter, _ *http.Request) { - _, _ = w.Write([]byte("hello, sim")) + _ = sim.Text(w, http.StatusOK, "hello, sim") }) _ = app.Run(context.Background(), ":8080") } @@ -94,7 +102,6 @@ package main import ( "context" - "fmt" "log" "net/http" "os" @@ -120,10 +127,10 @@ func main() { ) app.Get("/", func(w http.ResponseWriter, _ *http.Request) { - _, _ = w.Write([]byte("welcome")) + _ = sim.Text(w, http.StatusOK, "welcome") }) app.Any("/ping", func(w http.ResponseWriter, _ *http.Request) { - _, _ = w.Write([]byte("pong")) + _ = sim.Text(w, http.StatusOK, "pong") }) // Group routes under a common prefix. @@ -164,11 +171,23 @@ func listUsers(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) return } - _, _ = fmt.Fprintf(w, "list users, page %d\n", q.Page) + // In a real app, q.Page would drive database pagination. + _ = sim.JSON(w, http.StatusOK, []user{ + {Name: "alice", Age: 30}, + {Name: "bob", Age: 25}, + }) } func getUser(w http.ResponseWriter, r *http.Request) { - _, _ = fmt.Fprintf(w, "user %s", r.PathValue("id")) + // BindPath fills a struct from URL wildcards. + p, err := sim.BindPath[struct { + ID string `path:"id"` + }](r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + _ = sim.Text(w, http.StatusOK, "user "+p.ID) } // user is the payload createUser decodes from the request body. @@ -183,12 +202,19 @@ func createUser(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) return } - w.WriteHeader(http.StatusCreated) - _, _ = fmt.Fprintf(w, "user %s created\n", u.Name) + _ = sim.JSON(w, http.StatusCreated, u) } func updateUser(w http.ResponseWriter, r *http.Request) { - _, _ = fmt.Fprintf(w, "user %s updated", r.PathValue("id")) + // BindPath fills a struct from URL wildcards. + p, err := sim.BindPath[struct { + ID string `path:"id"` + }](r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + _ = sim.Text(w, http.StatusOK, "user "+p.ID+" updated") } func deleteUser(w http.ResponseWriter, _ *http.Request) { @@ -196,7 +222,7 @@ func deleteUser(w http.ResponseWriter, _ *http.Request) { } func adminPanel(w http.ResponseWriter, _ *http.Request) { - _, _ = fmt.Fprintln(w, "admin") + _ = sim.Text(w, http.StatusOK, "admin\n") } ``` @@ -207,12 +233,20 @@ go run main.go ``` Open http://localhost:8080/ to see "welcome", and -http://localhost:8080/api/users for the user list. The endpoints speak -HTTP, so try binding too: +http://localhost:8080/api/users for the user list. The endpoints return JSON. Try them: ```bash curl 'localhost:8080/api/users?page=2' -curl -X POST localhost:8080/api/users -d '{"name":"alice","age":30}' +curl -X POST localhost:8080/api/users \ + -H 'Content-Type: application/json' \ + -d '{"name":"alice","age":30}' +``` + +Responses: + +```text +[{"name":"alice","age":30},{"name":"bob","age":25}] +{"name":"alice","age":30} ``` ## Contributing diff --git a/sim.go b/sim.go index cddf42f..4ef88e0 100644 --- a/sim.go +++ b/sim.go @@ -107,6 +107,25 @@ // map[string][]string as the target type; the other binders require a // struct. // +// # Responding +// +// [JSON], [XML], [Text], [Bytes], [Stream] and [Attachment] write a +// complete response — status code, content type and body — in a single +// call: +// +// - [JSON] encodes data as JSON. [EscapeForHTML] and [Indented] +// control escaping and formatting. +// - [XML] encodes data as XML, prepending the standard XML header. +// - [Text] writes a plain-text string. +// - [Bytes] writes raw bytes with a caller-supplied content type. +// - [Stream] copies an [io.Reader] in chunks, suiting large or +// in-progress bodies such as file downloads or proxied responses. +// - [Attachment] wraps [Stream] with a Content-Disposition header +// that prompts browsers to save the body as a file. +// +// For static files that need Range requests or caching, prefer +// [http.ServeFile] or [http.ServeFileFS]. +// // See the documentation of [App] for the full routing API. package sim From f8b1e7660944c512415082f189cdd966f4c4cb4f Mon Sep 17 00:00:00 2001 From: qm012 <67568757+qm012@users.noreply.github.com> Date: Thu, 3 Sep 2026 20:37:14 +0800 Subject: [PATCH 2/7] add readme --- README.md | 29 +++++++++------- example_test.go | 92 +++++++++++++++++++++++++++++++++++++++++++++++++ responder.go | 9 +++++ sim.go | 15 ++++---- 4 files changed, 125 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 3858162..9a5317c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ native performance untouched. Simple, not simplistic. ## Features -**Core** +### Core - Zero dependencies — only the Go standard library - Method-based routing: `Get`, `Post`, `Put`, `Delete`, `Patch`, @@ -27,14 +27,13 @@ native performance untouched. Simple, not simplistic. context type to learn - Wrapper composition with `Chain` and `ChainFunc` - Conditional wrapper application with `Selector` -- Response helpers: `JSON` (with `EscapeForHTML` / `Indented` options), - `XML`, `Text`, `Bytes`, `Stream` for chunked streaming, and - `Attachment` for file downloads -- Request binding into structs with `query`, `form`, `path`, `header` - and `JSON`/`XML` tags +- [Request binding](#request-binding) into structs from the query, form, + path, headers and body +- [Response helpers](#response-helpers) that write a full response in one + call - Graceful shutdown with `Run` -**Built-in wrappers** +### Built-in wrappers | Wrapper | What it does | |----------------------|-----------------------------------------------------------------------------------------| @@ -44,7 +43,7 @@ native performance untouched. Simple, not simplistic. `Default` bundles all three wrappers, ready to use with no configuration. -**Request binding** +### Request binding - Bind requests into your own structs: `BindJSON`, `BindXML`, `BindQuery`, `BindForm`, `BindPath` and `BindHeader` @@ -56,13 +55,14 @@ native performance untouched. Simple, not simplistic. See the [package documentation](https://pkg.go.dev/github.com/qm012/sim) for the full struct-tag rules. -**Response helpers** +### Response helpers - Write complete responses in one call: `JSON`, `XML`, `Text`, `Bytes`, `Stream` and `Attachment` - JSON options: `EscapeForHTML` for safe HTML embedding, `Indented` for readable output -- `Stream` for chunked large bodies, `Attachment` for file downloads +- `Stream` for large or in-progress bodies without loading them into + memory, `Attachment` for file downloads ## Installation @@ -172,10 +172,13 @@ func listUsers(w http.ResponseWriter, r *http.Request) { return } // In a real app, q.Page would drive database pagination. - _ = sim.JSON(w, http.StatusOK, []user{ + _ = sim.JSON(w, http.StatusOK, struct { + Page int `json:"page"` + Users []user `json:"users"` + }{q.Page, []user{ {Name: "alice", Age: 30}, {Name: "bob", Age: 25}, - }) + }}) } func getUser(w http.ResponseWriter, r *http.Request) { @@ -245,7 +248,7 @@ curl -X POST localhost:8080/api/users \ Responses: ```text -[{"name":"alice","age":30},{"name":"bob","age":25}] +{"page":2,"users":[{"name":"alice","age":30},{"name":"bob","age":25}]} {"name":"alice","age":30} ``` diff --git a/example_test.go b/example_test.go index 688c49e..341f1a8 100644 --- a/example_test.go +++ b/example_test.go @@ -10,6 +10,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "encoding/xml" "errors" "fmt" "io" @@ -80,3 +81,94 @@ func ExampleDecoderFunc() { // valid: order.paid evt_9f3a // tampered: bad signature } + +type exampleUser struct { + XMLName xml.Name `json:"-" xml:"user"` + Name string `json:"name" xml:"name"` + Age int `json:"age" xml:"age"` +} + +func ExampleJSON() { + w := httptest.NewRecorder() + _ = sim.JSON(w, http.StatusOK, exampleUser{Name: "alice", Age: 30}) + fmt.Println(w.Code, w.Header().Get("Content-Type")) + fmt.Print(w.Body) + // Output: + // 200 application/json; charset=utf-8 + // {"name":"alice","age":30} +} + +func ExampleJSON_escapeForHTML() { + data := map[string]string{"url": "a + // alice30 +} + +func ExampleText() { + w := httptest.NewRecorder() + _ = sim.Text(w, http.StatusOK, "hello, sim") + fmt.Println(w.Header().Get("Content-Type")) + fmt.Print(w.Body) + // Output: + // text/plain; charset=utf-8 + // hello, sim +} + +func ExampleBytes() { + w := httptest.NewRecorder() + _ = sim.Bytes(w, http.StatusOK, "text/csv", []byte("name,age\nalice,30\n")) + fmt.Println(w.Header().Get("Content-Type")) + fmt.Print(w.Body) + // Output: + // text/csv + // name,age + // alice,30 +} + +func ExampleStream() { + w := httptest.NewRecorder() + _ = sim.Stream(w, http.StatusOK, "text/plain", strings.NewReader("chunked body")) + fmt.Print(w.Body) + // Output: chunked body +} + +func ExampleAttachment() { + w := httptest.NewRecorder() + _ = sim.Attachment(w, "report.txt", strings.NewReader("hello")) + fmt.Println(w.Header().Get("Content-Disposition")) + fmt.Print(w.Body) + // Output: + // attachment; filename=report.txt + // hello +} diff --git a/responder.go b/responder.go index 3d04022..cc27b52 100644 --- a/responder.go +++ b/responder.go @@ -34,6 +34,11 @@ func Indented(v bool) JSONEncoderOption { } // JSON writes data as JSON with the given status code. +// +// The status code is sent before data is encoded, so a value that cannot +// be encoded leaves the client with statusCode and an empty body. Such an +// error can only be logged: the response is already committed, and a later +// WriteHeader is ignored. func JSON(w http.ResponseWriter, statusCode int, data any, opts ...JSONEncoderOption) error { o := jsonEncoderOptions{escapeHTML: true} for _, opt := range opts { @@ -54,6 +59,10 @@ func JSON(w http.ResponseWriter, statusCode int, data any, opts ...JSONEncoderOp var xmlHeaderBytes = []byte(xml.Header) // XML writes data as XML with the given status code. +// +// data is marshaled before the status code is sent, so an encoding error +// leaves the response untouched and the handler can still write an error +// response. func XML(w http.ResponseWriter, statusCode int, data any) error { b, err := xml.Marshal(data) if err != nil { diff --git a/sim.go b/sim.go index 4ef88e0..47d8213 100644 --- a/sim.go +++ b/sim.go @@ -12,7 +12,7 @@ // // import ( // "context" -// "log/slog" +// "log" // "net/http" // // "github.com/qm012/sim" @@ -22,10 +22,10 @@ // app := sim.Default() // // app.Get("/", func(w http.ResponseWriter, _ *http.Request) { -// w.Write([]byte("root.")) +// _ = sim.Text(w, http.StatusOK, "root.") // }) // app.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) { -// w.Write([]byte("user " + r.PathValue("id"))) +// _ = sim.Text(w, http.StatusOK, "user "+r.PathValue("id")) // }) // app.Group("/api", func(r sim.Router) { // r.Post("/users", func(w http.ResponseWriter, _ *http.Request) { @@ -33,8 +33,8 @@ // }) // }) // -// if err := app.Run(context.Background(), ":3333"); err != nil { -// slog.Error("server failed", "err", err) +// if err := app.Run(context.Background(), ":8080"); err != nil { +// log.Fatal(err) // } // } // @@ -164,8 +164,9 @@ type Router interface { HandleFunc(pattern string, handler http.HandlerFunc) // Any Get Post Delete Patch Put Options Head Connect and Trace - // register handlerFunc on the given pattern for their respective HTTP - // methods; Any matches all methods. + // register handlerFunc on the given path for their respective HTTP + // methods; Any matches all methods. Unlike Handle and HandleFunc, the + // path takes no method prefix — the helper supplies it. Any(path string, handlerFunc http.HandlerFunc) Get(path string, handlerFunc http.HandlerFunc) Post(path string, handlerFunc http.HandlerFunc) From d9c29d6012914ffcd1ed4ebfdee67edeb3001432 Mon Sep 17 00:00:00 2001 From: qm012 <67568757+qm012@users.noreply.github.com> Date: Thu, 3 Sep 2026 21:12:29 +0800 Subject: [PATCH 3/7] update --- README.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9a5317c..71ed22c 100644 --- a/README.md +++ b/README.md @@ -190,11 +190,13 @@ func getUser(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) return } - _ = sim.Text(w, http.StatusOK, "user "+p.ID) + // In a real app, p.ID would drive a database lookup. + _ = sim.JSON(w, http.StatusOK, user{ID: p.ID, Name: "alice", Age: 30}) } -// user is the payload createUser decodes from the request body. +// user is the payload the API exchanges with its clients. type user struct { + ID string `json:"id,omitempty"` Name string `json:"name"` Age int `json:"age"` } @@ -209,7 +211,7 @@ func createUser(w http.ResponseWriter, r *http.Request) { } func updateUser(w http.ResponseWriter, r *http.Request) { - // BindPath fills a struct from URL wildcards. + // A PUT carries a path parameter and a body; bind each in turn. p, err := sim.BindPath[struct { ID string `path:"id"` }](r) @@ -217,7 +219,14 @@ func updateUser(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) return } - _ = sim.Text(w, http.StatusOK, "user "+p.ID+" updated") + u, err := sim.BindJSON[user](r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + u.ID = p.ID + // In a real app, the updated user would be stored here. + _ = sim.JSON(w, http.StatusOK, u) } func deleteUser(w http.ResponseWriter, _ *http.Request) { @@ -225,7 +234,7 @@ func deleteUser(w http.ResponseWriter, _ *http.Request) { } func adminPanel(w http.ResponseWriter, _ *http.Request) { - _ = sim.Text(w, http.StatusOK, "admin\n") + _ = sim.Text(w, http.StatusOK, "admin") } ``` @@ -240,16 +249,22 @@ http://localhost:8080/api/users for the user list. The endpoints return JSON. Tr ```bash curl 'localhost:8080/api/users?page=2' +curl localhost:8080/api/users/1 curl -X POST localhost:8080/api/users \ -H 'Content-Type: application/json' \ -d '{"name":"alice","age":30}' +curl -X PUT localhost:8080/api/users/1 \ + -H 'Content-Type: application/json' \ + -d '{"name":"alice","age":31}' ``` Responses: ```text {"page":2,"users":[{"name":"alice","age":30},{"name":"bob","age":25}]} +{"id":"1","name":"alice","age":30} {"name":"alice","age":30} +{"id":"1","name":"alice","age":31} ``` ## Contributing From b88bbf4b493bab646c0a4a6dfd32fa543cf51e58 Mon Sep 17 00:00:00 2001 From: qm012 <67568757+qm012@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:37:11 +0800 Subject: [PATCH 4/7] fix --- README.md | 13 +++++-------- example_test.go | 4 ++-- responder.go | 5 ----- sim.go | 19 +++++++++---------- 4 files changed, 16 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 71ed22c..cff96a1 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,10 @@ native performance untouched. Simple, not simplistic. context type to learn - Wrapper composition with `Chain` and `ChainFunc` - Conditional wrapper application with `Selector` -- [Request binding](#request-binding) into structs from the query, form, - path, headers and body -- [Response helpers](#response-helpers) that write a full response in one - call +- [Request binding](#request-binding): `BindJSON`, `BindXML`, `BindQuery`, + `BindForm`, `BindPath` and `BindHeader` +- [Response helpers](#response-helpers): `JSON`, `XML`, `Text`, `Bytes`, + `Stream` and `Attachment` - Graceful shutdown with `Run` ### Built-in wrappers @@ -171,7 +171,6 @@ func listUsers(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) return } - // In a real app, q.Page would drive database pagination. _ = sim.JSON(w, http.StatusOK, struct { Page int `json:"page"` Users []user `json:"users"` @@ -190,7 +189,6 @@ func getUser(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) return } - // In a real app, p.ID would drive a database lookup. _ = sim.JSON(w, http.StatusOK, user{ID: p.ID, Name: "alice", Age: 30}) } @@ -211,7 +209,6 @@ func createUser(w http.ResponseWriter, r *http.Request) { } func updateUser(w http.ResponseWriter, r *http.Request) { - // A PUT carries a path parameter and a body; bind each in turn. p, err := sim.BindPath[struct { ID string `path:"id"` }](r) @@ -225,7 +222,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) { return } u.ID = p.ID - // In a real app, the updated user would be stored here. + _ = sim.JSON(w, http.StatusOK, u) } diff --git a/example_test.go b/example_test.go index 341f1a8..e6717af 100644 --- a/example_test.go +++ b/example_test.go @@ -158,9 +158,9 @@ func ExampleBytes() { func ExampleStream() { w := httptest.NewRecorder() - _ = sim.Stream(w, http.StatusOK, "text/plain", strings.NewReader("chunked body")) + _ = sim.Stream(w, http.StatusOK, "text/plain", strings.NewReader("hello")) fmt.Print(w.Body) - // Output: chunked body + // Output: hello } func ExampleAttachment() { diff --git a/responder.go b/responder.go index cc27b52..1377277 100644 --- a/responder.go +++ b/responder.go @@ -34,11 +34,6 @@ func Indented(v bool) JSONEncoderOption { } // JSON writes data as JSON with the given status code. -// -// The status code is sent before data is encoded, so a value that cannot -// be encoded leaves the client with statusCode and an empty body. Such an -// error can only be logged: the response is already committed, and a later -// WriteHeader is ignored. func JSON(w http.ResponseWriter, statusCode int, data any, opts ...JSONEncoderOption) error { o := jsonEncoderOptions{escapeHTML: true} for _, opt := range opts { diff --git a/sim.go b/sim.go index 47d8213..6d946f5 100644 --- a/sim.go +++ b/sim.go @@ -109,18 +109,17 @@ // // # Responding // -// [JSON], [XML], [Text], [Bytes], [Stream] and [Attachment] write a -// complete response — status code, content type and body — in a single -// call: +// [JSON], [XML], [Text], [Bytes], [Stream] and [Attachment] write +// responses with a single call: // // - [JSON] encodes data as JSON. [EscapeForHTML] and [Indented] // control escaping and formatting. // - [XML] encodes data as XML, prepending the standard XML header. // - [Text] writes a plain-text string. // - [Bytes] writes raw bytes with a caller-supplied content type. -// - [Stream] copies an [io.Reader] in chunks, suiting large or -// in-progress bodies such as file downloads or proxied responses. -// - [Attachment] wraps [Stream] with a Content-Disposition header +// - [Stream] copies an [io.Reader] to the response, suitable for large +// or streaming bodies such as proxied responses. +// - [Attachment] streams a body with a Content-Disposition header for download. // that prompts browsers to save the body as a file. // // For static files that need Range requests or caching, prefer @@ -163,10 +162,10 @@ type Router interface { // with the same behavior as [http.ServeMux.HandleFunc] and [http.HandleFunc]. HandleFunc(pattern string, handler http.HandlerFunc) - // Any Get Post Delete Patch Put Options Head Connect and Trace - // register handlerFunc on the given path for their respective HTTP - // methods; Any matches all methods. Unlike Handle and HandleFunc, the - // path takes no method prefix — the helper supplies it. + // Any Get, Post, Delete, Patch, Put, Options, Head, Connect, and Trace + // register handlerFunc for their respective HTTP methods. + // Any matches all methods. Unlike Handle and HandleFunc, these helpers + // take a path without a method prefix. Any(path string, handlerFunc http.HandlerFunc) Get(path string, handlerFunc http.HandlerFunc) Post(path string, handlerFunc http.HandlerFunc) From 1fc15e035dfa6addab9e4d42a84f7e10c44b18ad Mon Sep 17 00:00:00 2001 From: qm012 <67568757+qm012@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:44:11 +0800 Subject: [PATCH 5/7] fix --- README.md | 6 +++--- sim.go | 8 +++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cff96a1..40f39f4 100644 --- a/README.md +++ b/README.md @@ -45,10 +45,10 @@ native performance untouched. Simple, not simplistic. ### Request binding -- Bind requests into your own structs: `BindJSON`, `BindXML`, - `BindQuery`, `BindForm`, `BindPath` and `BindHeader` +- Bind incoming request data into your own structs from JSON, XML, query, + form, path, and header values. - Struct tags with `default=` values, embedded structs, multipart file - uploads and map targets + uploads, and map targets - Validation via `Validator`, custom formats via `Decoder` - Read the request body more than once with `BufferBody` diff --git a/sim.go b/sim.go index 6d946f5..fb8d409 100644 --- a/sim.go +++ b/sim.go @@ -120,7 +120,6 @@ // - [Stream] copies an [io.Reader] to the response, suitable for large // or streaming bodies such as proxied responses. // - [Attachment] streams a body with a Content-Disposition header for download. -// that prompts browsers to save the body as a file. // // For static files that need Range requests or caching, prefer // [http.ServeFile] or [http.ServeFileFS]. @@ -162,10 +161,9 @@ type Router interface { // with the same behavior as [http.ServeMux.HandleFunc] and [http.HandleFunc]. HandleFunc(pattern string, handler http.HandlerFunc) - // Any Get, Post, Delete, Patch, Put, Options, Head, Connect, and Trace - // register handlerFunc for their respective HTTP methods. - // Any matches all methods. Unlike Handle and HandleFunc, these helpers - // take a path without a method prefix. + // Any matches all HTTP methods. Get, Post, Delete, Patch, Put, Options, + // Head, Connect, and Trace register handlerFunc for their respective HTTP methods. + // Unlike Handle and HandleFunc, these helpers take a path without a method prefix. Any(path string, handlerFunc http.HandlerFunc) Get(path string, handlerFunc http.HandlerFunc) Post(path string, handlerFunc http.HandlerFunc) From c7958ae36f47ce7aaf419e7c89f3d5f0edbf15e9 Mon Sep 17 00:00:00 2001 From: qm012 <67568757+qm012@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:48:38 +0800 Subject: [PATCH 6/7] fmt --- README.md | 3 +-- sim.go | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 40f39f4..bfb7b93 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,7 @@ for the full struct-tag rules. ### Response helpers -- Write complete responses in one call: `JSON`, `XML`, `Text`, `Bytes`, - `Stream` and `Attachment` +- Write JSON, XML, text, byte, streaming, and attachment responses - JSON options: `EscapeForHTML` for safe HTML embedding, `Indented` for readable output - `Stream` for large or in-progress bodies without loading them into diff --git a/sim.go b/sim.go index fb8d409..42135f5 100644 --- a/sim.go +++ b/sim.go @@ -162,8 +162,9 @@ type Router interface { HandleFunc(pattern string, handler http.HandlerFunc) // Any matches all HTTP methods. Get, Post, Delete, Patch, Put, Options, - // Head, Connect, and Trace register handlerFunc for their respective HTTP methods. - // Unlike Handle and HandleFunc, these helpers take a path without a method prefix. + // Head, Connect, and Trace register handlerFunc for their respective HTTP + // methods. Unlike Handle and HandleFunc, these helpers take a path without + // a method prefix. Any(path string, handlerFunc http.HandlerFunc) Get(path string, handlerFunc http.HandlerFunc) Post(path string, handlerFunc http.HandlerFunc) From 15efddce8bfce884be4e2d7d0f813b6aafbc37c0 Mon Sep 17 00:00:00 2001 From: qm012 <67568757+qm012@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:58:30 +0800 Subject: [PATCH 7/7] fmt --- README.md | 21 ++++++++++----------- responder.go | 4 ---- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index bfb7b93..0e42bb2 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ func listUsers(w http.ResponseWriter, r *http.Request) { } func getUser(w http.ResponseWriter, r *http.Request) { - // BindPath fills a struct from URL wildcards. + // BindPath fills a struct from path values. p, err := sim.BindPath[struct { ID string `path:"id"` }](r) @@ -241,26 +241,25 @@ go run main.go ``` Open http://localhost:8080/ to see "welcome", and -http://localhost:8080/api/users for the user list. The endpoints return JSON. Try them: +http://localhost:8080/api/users for the user list. The endpoints return +JSON. Try them: ```bash curl 'localhost:8080/api/users?page=2' +# {"page":2,"users":[{"name":"alice","age":30},{"name":"bob","age":25}]} + curl localhost:8080/api/users/1 +# {"id":"1","name":"alice","age":30} + curl -X POST localhost:8080/api/users \ -H 'Content-Type: application/json' \ -d '{"name":"alice","age":30}' +# {"name":"alice","age":30} + curl -X PUT localhost:8080/api/users/1 \ -H 'Content-Type: application/json' \ -d '{"name":"alice","age":31}' -``` - -Responses: - -```text -{"page":2,"users":[{"name":"alice","age":30},{"name":"bob","age":25}]} -{"id":"1","name":"alice","age":30} -{"name":"alice","age":30} -{"id":"1","name":"alice","age":31} +# {"id":"1","name":"alice","age":31} ``` ## Contributing diff --git a/responder.go b/responder.go index 1377277..3d04022 100644 --- a/responder.go +++ b/responder.go @@ -54,10 +54,6 @@ func JSON(w http.ResponseWriter, statusCode int, data any, opts ...JSONEncoderOp var xmlHeaderBytes = []byte(xml.Header) // XML writes data as XML with the given status code. -// -// data is marshaled before the status code is sent, so an encoding error -// leaves the response untouched and the handler can still write an error -// response. func XML(w http.ResponseWriter, statusCode int, data any) error { b, err := xml.Marshal(data) if err != nil {