Repository: ggicci/httpin
Pull request #113: Added UseHttpPathVariable to access path variables with httpin when using net/http
Author: login: chriss-de
Created: 2025-04-23T07:16:32Z | Merged: 2025-05-08T03:57:08Z

I wrote an integration for net/http.

what else to tell... If merged it would at least help me to not copy that file from project to project


Diff:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 165d0d7..fe63c05 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -15,13 +15,13 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v5
         with:
-          go-version: "1.20"
+          go-version: "1.23"
 
       - name: Test and generate coverage report
         run: make test/cover
 
       - name: Upload coverage to Codecov
-        uses: codecov/codecov-action@v3
+        uses: codecov/codecov-action@v5
         with:
           name: codecov-umbrella
           token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/go.mod b/go.mod
index b2364b0..726cfbc 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/ggicci/httpin
 
-go 1.20
+go 1.23
 
 require (
 	github.com/ggicci/owl v0.8.2
diff --git a/integration/http.go b/integration/http.go
new file mode 100644
index 0000000..b3b9489
--- /dev/null
+++ b/integration/http.go
@@ -0,0 +1,45 @@
+package integration
+
+import (
+	"github.com/ggicci/httpin/core"
+	"mime/multipart"
+	"net/http"
+)
+
+type HttpMuxVarsFunc func(*http.Request) map[string]string
+
+func UseHttpMux(name string) {
+	core.RegisterDirective(
+		name,
+		core.NewDirectivePath((&httpMuxVarsExtractor{}).Execute),
+		true,
+	)
+}
+
+func UseHttpPathMux() {
+	UseHttpMux("path")
+}
+
+type httpMuxVarsExtractor struct {
+	Vars http.ServeMux
+}
+
+func (mux *httpMuxVarsExtractor) Execute(rtm *core.DirectiveRuntime) error {
+	req := rtm.GetRequest()
+	kvs := make(map[string][]string)
+
+	for _, key := range rtm.Directive.Argv {
+		value := req.PathValue(key)
+		if value != "" {
+			kvs[key] = []string{value}
+		}
+	}
+
+	extractor := &core.FormExtractor{
+		Runtime: rtm,
+		Form: multipart.Form{
+			Value: kvs,
+		},
+	}
+	return extractor.Extract()
+}
diff --git a/integration/http_test.go b/integration/http_test.go
new file mode 100644
index 0000000..7316704
--- /dev/null
+++ b/integration/http_test.go
@@ -0,0 +1,58 @@
+package integration_test
+
+import (
+	"encoding/json"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"github.com/ggicci/httpin"
+	httpinIntegration "github.com/ggicci/httpin/integration"
+	"github.com/stretchr/testify/assert"
+)
+
+type HttpMuxPathInput struct {
+	Username string `in:"path=username"`
+	PostID   int64  `in:"path=pid"`
+}
+
+func TestUseHttpMux(t *testing.T) {
+	httpinIntegration.UseHttpMux("path")
+
+	srv := http.NewServeMux()
+	srv.HandleFunc("/users/{username}/posts/{pid}", func(w http.ResponseWriter, r *http.Request) {
+		param := &HttpMuxPathInput{}
+		core, err := httpin.New(param)
+		if err != nil {
+			t.Fatal(err)
+			return
+		}
+		v, err := core.Decode(r)
+		if err != nil {
+			t.Fatal(err)
+			return
+		}
+		jsonBytes, err := json.Marshal(v)
+		if err != nil {
+			t.Fatal(err)
+			return
+		}
+
+		w.WriteHeader(http.StatusOK)
+		w.Write(jsonBytes)
+	})
+	ts := httptest.NewServer(srv)
+	defer ts.Close()
+
+	resp, err := http.DefaultClient.Get(ts.URL + "/users/chriss-de/posts/456")
+	assert.NoError(t, err)
+	assert.Equal(t, http.StatusOK, resp.StatusCode)
+
+	bodyBytes, err := io.ReadAll(resp.Body)
+	assert.NoError(t, err)
+	bodyString := string(bodyBytes)
+
+	assert.Equal(t, `{"Username":"chriss-de","PostID":456}`, strings.TrimSpace(bodyString))
+}


Reviews:
user: login: ggicci
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-25T05:08:46Z
user: login: chriss-de
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T07:18:21Z
user: login: ggicci
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-06T04:26:41Z
user: login: ggicci
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-06T04:33:05Z
user: login: chriss-de
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T06:34:00Z

Review comments:
diff_hunk: @@ -0,0 +1,47 @@
+package integration
+
+import (
+	"mime/multipart"
+	"net/http"
+	"strings"
+
+	"github.com/ggicci/httpin/core"
+)
+
+type HttpMuxVarsFunc func(*http.Request) map[string]string
+
+func UseHttpMux(name string) {
+	core.RegisterDirective(
+		name,
+		core.NewDirectivePath((&httpMuxVarsExtractor{}).Execute),
+		true,
+	)
+}
+
+func UseHttpPathMux() {
+	UseHttpMux("path")
+}
+
+type httpMuxVarsExtractor struct {
+	Vars http.ServeMux
+}
+
+func (mux *httpMuxVarsExtractor) Execute(rtm *core.DirectiveRuntime) error {
+	req := rtm.GetRequest()
+	kvs := make(map[string][]string)
+
+	for _, value := range strings.Split(req.Pattern, "/") {
+		if strings.HasPrefix(value, "{") && strings.HasSuffix(value, "}") {
+			value = strings.TrimSuffix(strings.TrimPrefix(value, "{"), "}")
+			kvs[value] = []string{req.PathValue(value)}
+		}
+	}
path: integration/http.go
user: login: ggicci
body: You can simplify it by iterating `rtm.Directive.Argv` instead of parsing the "patterns". For example: https://github.com/ggicci/httpin/blob/main/integration/gochi.go#L43-L48
created_at: 2025-04-25T05:06:13Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,47 @@
+package integration
+
+import (
+	"mime/multipart"
+	"net/http"
+	"strings"
+
+	"github.com/ggicci/httpin/core"
+)
+
+type HttpMuxVarsFunc func(*http.Request) map[string]string
+
+func UseHttpMux(name string) {
+	core.RegisterDirective(
+		name,
+		core.NewDirectivePath((&httpMuxVarsExtractor{}).Execute),
+		true,
+	)
+}
+
+func UseHttpPathMux() {
+	UseHttpMux("path")
+}
+
+type httpMuxVarsExtractor struct {
+	Vars http.ServeMux
+}
+
+func (mux *httpMuxVarsExtractor) Execute(rtm *core.DirectiveRuntime) error {
+	req := rtm.GetRequest()
+	kvs := make(map[string][]string)
+
+	for _, value := range strings.Split(req.Pattern, "/") {
+		if strings.HasPrefix(value, "{") && strings.HasSuffix(value, "}") {
+			value = strings.TrimSuffix(strings.TrimPrefix(value, "{"), "}")
+			kvs[value] = []string{req.PathValue(value)}
+		}
+	}
path: integration/http.go
user: login: ggicci
body: ```go
for _, key := range rtm.Directive.Argv {
    value := req.PathValue(key)
    if value != "" {
        kvs[key] = []string{value}
    }
}
```
created_at: 2025-04-25T05:08:12Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,47 @@
+package integration
+
+import (
+	"mime/multipart"
+	"net/http"
+	"strings"
+
+	"github.com/ggicci/httpin/core"
+)
+
+type HttpMuxVarsFunc func(*http.Request) map[string]string
+
+func UseHttpMux(name string) {
+	core.RegisterDirective(
+		name,
+		core.NewDirectivePath((&httpMuxVarsExtractor{}).Execute),
+		true,
+	)
+}
+
+func UseHttpPathMux() {
+	UseHttpMux("path")
+}
+
+type httpMuxVarsExtractor struct {
+	Vars http.ServeMux
+}
+
+func (mux *httpMuxVarsExtractor) Execute(rtm *core.DirectiveRuntime) error {
+	req := rtm.GetRequest()
+	kvs := make(map[string][]string)
+
+	for _, value := range strings.Split(req.Pattern, "/") {
+		if strings.HasPrefix(value, "{") && strings.HasSuffix(value, "}") {
+			value = strings.TrimSuffix(strings.TrimPrefix(value, "{"), "}")
+			kvs[value] = []string{req.PathValue(value)}
+		}
+	}
path: integration/http.go
user: login: chriss-de
body: I looked into this and it is not like that. Argv is a list but always contains only one element of the pattern.
A URL path with multiple vars is not correctly in there. AFAIK this is code from you?! ;)
created_at: 2025-04-25T07:18:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+package integration
+
+import (
+	"mime/multipart"
+	"net/http"
+	"strings"
+
+	"github.com/ggicci/httpin/core"
+)
+
+type HttpMuxVarsFunc func(*http.Request) map[string]string
+
+func UseHttpMux(name string) {
+	core.RegisterDirective(
+		name,
+		core.NewDirectivePath((&httpMuxVarsExtractor{}).Execute),
+		true,
+	)
+}
+
+func UseHttpPathMux() {
+	UseHttpMux("path")
+}
+
+type httpMuxVarsExtractor struct {
+	Vars http.ServeMux
+}
+
+func (mux *httpMuxVarsExtractor) Execute(rtm *core.DirectiveRuntime) error {
+	req := rtm.GetRequest()
+	kvs := make(map[string][]string)
+
+	for _, value := range strings.Split(req.Pattern, "/") {
+		if strings.HasPrefix(value, "{") && strings.HasSuffix(value, "}") {
+			value = strings.TrimSuffix(strings.TrimPrefix(value, "{"), "}")
+			kvs[value] = []string{req.PathValue(value)}
+		}
+	}
path: integration/http.go
user: login: ggicci
body: Let's see an example usage of the `path` directive:

```go
Username string `in:"path=login,uid,username"`
```

For the `Username` field, `httpin` tries to extract the value from the URL path registered in these patterns: `login`, `uid`, `username` in order. Which means as long as the path has a matched pattern in any of the above, httpin uses it.

ex:

| Path Pattern |  Path | Target Pattern |  `Username` Value |
| --- | --- | --- | --- |
| `/sessions/{login}` | `/sessions/abc` | login | `abc` |
| `/users/{uid}/{username}` | `/users/123/abc` | uid | `123` |
| `/users/{uid}/{login}` | `/users/123/abc` | login | `abc` |
| `/edit/{user}` | `/edit/abc` | N/A | N/A |

The `kvs` here is a collection of the `{pattern: value}` and the pattern is defined in the directive. Each field creates its own `kvs` instance. So it doesn't have to be a full map of the `{pattern: value}` extracted from the path. Because for a given field, it only access the pattern values defined in the directive.

For example, for this path: `/users/{uid}/posts/{pid}/comments/{cid}`, there's no need to extract `pid` and `{cid}` to `kvs` for the `Username` field defined above, because `Username` field only respects patterns `login`, `uid` and `username`.
created_at: 2025-05-06T04:26:36Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,47 @@
+package integration
+
+import (
+	"mime/multipart"
+	"net/http"
+	"strings"
+
+	"github.com/ggicci/httpin/core"
+)
+
+type HttpMuxVarsFunc func(*http.Request) map[string]string
+
+func UseHttpMux(name string) {
+	core.RegisterDirective(
+		name,
+		core.NewDirectivePath((&httpMuxVarsExtractor{}).Execute),
+		true,
+	)
+}
+
+func UseHttpPathMux() {
+	UseHttpMux("path")
+}
+
+type httpMuxVarsExtractor struct {
+	Vars http.ServeMux
+}
+
+func (mux *httpMuxVarsExtractor) Execute(rtm *core.DirectiveRuntime) error {
+	req := rtm.GetRequest()
+	kvs := make(map[string][]string)
+
+	for _, value := range strings.Split(req.Pattern, "/") {
+		if strings.HasPrefix(value, "{") && strings.HasSuffix(value, "}") {
+			value = strings.TrimSuffix(strings.TrimPrefix(value, "{"), "}")
+			kvs[value] = []string{req.PathValue(value)}
+		}
+	}
path: integration/http.go
user: login: ggicci
body: Actually either logic works, populating a `kvs` by using all the `{pattern: value}` pairs extracted from a path would be quite useful if we reuse the `kvs` instance across all fields. However, this is not the reality as of now. The latter one would be easier to maintain IMO.
created_at: 2025-05-06T04:33:04Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,47 @@
+package integration
+
+import (
+	"mime/multipart"
+	"net/http"
+	"strings"
+
+	"github.com/ggicci/httpin/core"
+)
+
+type HttpMuxVarsFunc func(*http.Request) map[string]string
+
+func UseHttpMux(name string) {
+	core.RegisterDirective(
+		name,
+		core.NewDirectivePath((&httpMuxVarsExtractor{}).Execute),
+		true,
+	)
+}
+
+func UseHttpPathMux() {
+	UseHttpMux("path")
+}
+
+type httpMuxVarsExtractor struct {
+	Vars http.ServeMux
+}
+
+func (mux *httpMuxVarsExtractor) Execute(rtm *core.DirectiveRuntime) error {
+	req := rtm.GetRequest()
+	kvs := make(map[string][]string)
+
+	for _, value := range strings.Split(req.Pattern, "/") {
+		if strings.HasPrefix(value, "{") && strings.HasSuffix(value, "}") {
+			value = strings.TrimSuffix(strings.TrimPrefix(value, "{"), "}")
+			kvs[value] = []string{req.PathValue(value)}
+		}
+	}
path: integration/http.go
user: login: chriss-de
body: I changed my code. I verified the functionality with the test. Most likely there are some things I didn't understand in this function.

created_at: 2025-05-06T06:34:00Z
author_association: CONTRIBUTOR

PR comments:
user: login: ggicci
created_at: 2025-04-24T06:39:26Z
body: hey @chriss-de, thank you very much for the contribution, could you help fix the CI failure?
author_association: OWNER
user: login: chriss-de
created_at: 2025-04-24T08:41:37Z
body: that should do it. 
it needs go 1.22 for the net/http stuff
author_association: CONTRIBUTOR
user: login: ggicci
created_at: 2025-04-25T04:48:14Z
body: I guess there's a bug in the code cuz the tests failed.
author_association: OWNER
user: login: chriss-de
created_at: 2025-04-25T08:14:19Z
body: now its building (at least in my github workflow)
author_association: CONTRIBUTOR
user: login: ggicci
created_at: 2025-04-30T15:05:22Z
body: Sorry @chriss-de, I'm on vacation, I will take a look as soon as I'm back. Thanks for the update :)
author_association: OWNER
user: login: ggicci
created_at: 2025-05-06T04:35:19Z
body: Hey @chriss-de , thanks for the contribution, please take a moment and verify if the comment I left makes sense to you. BTW, if you're glad to update, could you please also update `workflows/go.yml` file to replace `codecov/codecov-action@v3` with `codecov/codecov-action@v5` so the Go Action won't fail again.
author_association: OWNER
user: login: chriss-de
created_at: 2025-05-06T06:34:21Z
body: I made all the changes.
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-06T19:11:43Z
body: ## [Codecov](https://app.codecov.io/gh/ggicci/httpin/pull/113?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Ggicci) Report
Attention: Patch coverage is `91.66667%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 90.72%. Comparing base [(`de7db1b`)](https://app.codecov.io/gh/ggicci/httpin/commit/de7db1bbe104805ccb4f6e9abec71e9d2d3e8b92?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Ggicci) to head [(`0fe93ba`)](https://app.codecov.io/gh/ggicci/httpin/commit/0fe93ba10e181e0f6f03c43419f1a9c5d23903cb?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Ggicci).
> Report is 6 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/ggicci/httpin/pull/113?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Ggicci) | Patch % | Lines |
|---|---|---|
| [integration/http.go](https://app.codecov.io/gh/ggicci/httpin/pull/113?src=pr&el=tree&filepath=integration%2Fhttp.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Ggicci#diff-aW50ZWdyYXRpb24vaHR0cC5nbw==) | 91.66% | [2 Missing :warning: ](https://app.codecov.io/gh/ggicci/httpin/pull/113?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Ggicci) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #113      +/-   ##
==========================================
+ Coverage   90.70%   90.72%   +0.01%     
==========================================
  Files          35       36       +1     
  Lines        1539     1563      +24     
==========================================
+ Hits         1396     1418      +22     
- Misses         99      101       +2     
  Partials       44       44              
```

| [Flag](https://app.codecov.io/gh/ggicci/httpin/pull/113/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Ggicci) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/ggicci/httpin/pull/113/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Ggicci) | `90.72% <91.66%> (+0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Ggicci#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/ggicci/httpin/pull/113?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Ggicci).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Ggicci).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE