Skip to content

Commit

Permalink
Builder.Path: Allow relative paths, path stacking
Browse files Browse the repository at this point in the history
  • Loading branch information
earthboundkid committed Jun 28, 2021
1 parent 94dee2b commit cbdbf91
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 18 deletions.
39 changes: 26 additions & 13 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"mime"
"net/http"
"net/url"
"path"
"strings"

"golang.org/x/net/html"
Expand Down Expand Up @@ -52,15 +53,16 @@ import (
// function to add request specific details for the URL, parameters, headers,
// body, or handler. The zero value of Builder is usable.
type Builder struct {
baseurl string
scheme, host, path string
params []param
headers [][2]string
body BodyGetter
method string
cl *http.Client
validators []ResponseHandler
handler ResponseHandler
baseurl string
scheme, host string
paths []string
params []param
headers [][2]string
body BodyGetter
method string
cl *http.Client
validators []ResponseHandler
handler ResponseHandler
}

type param struct {
Expand Down Expand Up @@ -98,9 +100,11 @@ func (rb *Builder) Hostf(format string, a ...interface{}) *Builder {
return rb.Host(fmt.Sprintf(format, a...))
}

// Path sets the path for a request. It overrides the URL function.
// Path joins a path to a request. If the path begins with /, it overrides any
// existing path. If the path begins with ./ or ../, the final path will be
// rewritten in its absolute form.
func (rb *Builder) Path(path string) *Builder {
rb.path = path
rb.paths = append(rb.paths, path)
return rb
}

Expand Down Expand Up @@ -487,6 +491,7 @@ func (rb *Builder) ToWriter(w io.Writer) *Builder {
// Clone creates a new Builder suitable for independent mutation.
func (rb *Builder) Clone() *Builder {
rb2 := *rb
rb2.paths = rb2.paths[0:len(rb2.paths):len(rb2.paths)]
rb2.headers = rb2.headers[0:len(rb2.headers):len(rb2.headers)]
rb2.params = rb2.params[0:len(rb2.params):len(rb2.params)]
rb2.validators = rb2.validators[0:len(rb2.validators):len(rb2.validators)]
Expand All @@ -511,8 +516,16 @@ func (rb *Builder) Request(ctx context.Context) (req *http.Request, err error) {
if rb.host != "" {
u.Host = rb.host
}
if rb.path != "" {
u.Path = rb.path
for _, p := range rb.paths {
if strings.HasPrefix(p, "/") {
u.Path = p
} else {
if upath := path.Clean(u.Path); upath == "." || upath == "/" {
u.Path = path.Clean(p)
} else {
u.Path = path.Clean(path.Join(u.Path, p))
}
}
}
if len(rb.params) > 0 {
q := u.Query()
Expand Down
18 changes: 18 additions & 0 deletions builder_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,24 @@ func Example_getJSON() {
// sunt aut facere repellat provident occaecati excepturi optio reprehenderit
}

func ExampleBuilder_Path() {
// Add an ID to a base path
id := 1
var post placeholder
err := requests.
URL("https://jsonplaceholder.typicode.com/posts").
// inherits path /posts from baseurl
Pathf("%d", id).
// URL is now https://jsonplaceholder.typicode.com/posts/1
ToJSON(&post).
Fetch(context.Background())
if err != nil {
fmt.Println("could not connect to jsonplaceholder.typicode.com:", err)
}
fmt.Println(post.ID)
// Output:
// 1
}
func ExampleBuilder_CheckStatus() {
// Expect a specific status code
err := requests.
Expand Down
172 changes: 167 additions & 5 deletions builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,38 @@ import (
)

func TestClone(t *testing.T) {
{
t.Run("from URL", func(t *testing.T) {
rb1 := requests.
URL("example.com").
URL("http://example.com").
Path("a").
Header("a", "1").
Header("b", "2").
Param("a", "1").
Param("b", "2")
rb2 := rb1.Clone().
Host("host.example").
Path("b").
Header("b", "3").
Header("c", "4").
Param("b", "3").
Param("c", "4")
rb3 := rb1.Clone().
Host("host.example3").
Path("c").
Header("b", "5").
Header("c", "6").
Param("b", "5").
Param("c", "6")
req1, err := rb1.Request(context.Background())
if err != nil {
t.Fatal(err)
}
if req1.URL.Host != "example.com" {
t.Fatalf("bad host: %v", req1.URL)
}
if req1.URL.Path != "/a" {
t.Fatalf("bad path: %v", req1.URL)
}
if req1.Header.Get("b") != "2" || req1.Header.Get("c") != "" {
t.Fatalf("bad header: %v", req1.URL)
}
Expand All @@ -42,14 +54,33 @@ func TestClone(t *testing.T) {
if req2.URL.Host != "host.example" {
t.Fatalf("bad host: %v", req2.URL)
}
if req2.URL.Path != "/a/b" {
t.Fatalf("bad path: %v", req2.URL.Path)
}
if req2.Header.Get("b") != "3" || req2.Header.Get("c") != "4" {
t.Fatalf("bad header: %v", req2.URL)
}
if q := req2.URL.Query(); q.Get("b") != "3" || q.Get("c") != "4" {
t.Fatalf("bad query: %v", req2.URL)
}
}
{
req3, err := rb3.Request(context.Background())
if err != nil {
t.Fatal(err)
}
if req3.URL.Host != "host.example3" {
t.Fatalf("bad host: %v", req3.URL)
}
if req3.URL.Path != "/a/c" {
t.Fatalf("bad path: %v", req3.URL.Path)
}
if req3.Header.Get("b") != "5" || req3.Header.Get("c") != "6" {
t.Fatalf("bad header: %v", req3.URL)
}
if q := req3.URL.Query(); q.Get("b") != "5" || q.Get("c") != "6" {
t.Fatalf("bad query: %v", req3.URL)
}
})
t.Run("from new", func(t *testing.T) {
rb1 := new(requests.Builder).
Host("example.com").
Header("a", "1").
Expand All @@ -58,10 +89,18 @@ func TestClone(t *testing.T) {
Param("b", "2")
rb2 := rb1.Clone().
Host("host.example").
Path("/2").
Header("b", "3").
Header("c", "4").
Param("b", "3").
Param("c", "4")
rb3 := rb1.Clone().
Host("host.example3").
Path("/3").
Header("b", "5").
Header("c", "6").
Param("b", "5").
Param("c", "6")
req1, err := rb1.Request(context.Background())
if err != nil {
t.Fatal(err)
Expand All @@ -82,13 +121,32 @@ func TestClone(t *testing.T) {
if req2.URL.Host != "host.example" {
t.Fatalf("bad host: %v", req2.URL)
}
if req2.URL.Path != "/2" {
t.Fatalf("bad path: %v", req2.URL.Path)
}
if req2.Header.Get("b") != "3" || req2.Header.Get("c") != "4" {
t.Fatalf("bad header: %v", req2.URL)
}
if q := req2.URL.Query(); q.Get("b") != "3" || q.Get("c") != "4" {
t.Fatalf("bad query: %v", req2.URL)
}
}
req3, err := rb3.Request(context.Background())
if err != nil {
t.Fatal(err)
}
if req3.URL.Host != "host.example3" {
t.Fatalf("bad host: %v", req3.URL)
}
if req3.URL.Path != "/3" {
t.Fatalf("bad path: %v", req3.URL.Path)
}
if req3.Header.Get("b") != "5" || req3.Header.Get("c") != "6" {
t.Fatalf("bad header: %v", req3.URL)
}
if q := req3.URL.Query(); q.Get("b") != "5" || q.Get("c") != "6" {
t.Fatalf("bad query: %v", req3.URL)
}
})
}

func TestScheme(t *testing.T) {
Expand All @@ -115,3 +173,107 @@ An example response.`
t.Fatalf("%q != %q", s, expected)
}
}

func TestPath(t *testing.T) {
cases := map[string]struct {
base string
paths []string
result string
}{
"base-only": {
"example",
[]string{},
"https://example",
},
"base+abspath": {
"https://example",
[]string{"/a"},
"https://example/a",
},
"multi-abs-paths": {
"https://example",
[]string{"/a", "/b", "/c"},
"https://example/c",
},
"base+rel-path": {
"https://example/a",
[]string{"./b"},
"https://example/a/b",
},
"base+rel-paths": {
"https://example/a",
[]string{"./b", "./c"},
"https://example/a/b/c",
},
"rel-path": {
"https://example/",
[]string{"a", "./b"},
"https://example/a/b",
},
"base+multi-paths": {
"https://example/a",
[]string{"b", "c"},
"https://example/a/b/c",
},
"base+slash+multi-paths": {
"https://example/a/",
[]string{"b/", "c"},
"https://example/a/b/c",
},
"mutli-paths": {
"https://example/",
[]string{"a", "b", "c"},
"https://example/a/b/c",
},
"dot-dot-paths": {
"https://example/",
[]string{"a", "b", "../c"},
"https://example/a/c",
},
"more-dot-dot-paths": {
"https://example/",
[]string{"a/b/c", "../d", "../e"},
"https://example/a/b/e",
},
"more-dot-dot-paths+rel-path": {
"https://example/",
[]string{"a/b/c", "../d", "../e", "./f"},
"https://example/a/b/e/f",
},
"even-more-dot-dot-paths+base": {
"https://example/a/b/c",
[]string{"../../d"},
"https://example/a/d",
},
"too-many-dot-dot-paths": {
"https://example",
[]string{"../a"},
"https://example/../a",
},
"too-many-dot-dot-paths+base": {
"https://example/",
[]string{"../a"},
"https://example/../a",
},
"last-abs-path-wins": {
"https://example/a",
[]string{"b", "c", "/d"},
"https://example/d",
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
b := requests.URL(tc.base)
for _, p := range tc.paths {
b.Path(p)
}
r, err := b.Request(context.Background())
if err != nil {
t.Fatal(err)
}
if u := r.URL.String(); u != tc.result {
t.Fatalf("got %q; want %q", u, tc.result)
}
})
}
}

0 comments on commit cbdbf91

Please sign in to comment.