Repository: amp-labs/connectors
Pull request #1477 (2025-03-12 23:14:28)

feat(urlbuilder): Construct from url.URL
# Background

With the newly refactored deep connectors, we often work directly with raw `url.URL`. This bypasses the query parameter utilities provided by `urlbuilder`, making simple query parameter modifications cumbersome.

# Change
`urlbuilder.URL` can now be created from a raw `url.URL`.

Cobalt0s: * **#1477** <a href="https://app.graphite.dev/github/pr/amp-labs/connectors/1477?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/amp-labs/connectors/1477?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* `main`


<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->

Patch:
diff --git a/common/urlbuilder/url.go b/common/urlbuilder/url.go
index daec160e..f5492920 100644
--- a/common/urlbuilder/url.go
+++ b/common/urlbuilder/url.go
@@ -40,6 +40,21 @@ func New(base string, path ...string) (*URL, error) {
 	return u, nil
 }
 
+// FromRawURL converts a core Go `url.URL` into `urlbuilder.URL`,
+// providing better control over query parameters and encoding.
+func FromRawURL(rawURL *url.URL) (*URL, error) {
+	values, err := url.ParseQuery(rawURL.RawQuery)
+	if err != nil {
+		return nil, errors.Join(err, ErrInvalidURL)
+	}
+
+	return &URL{
+		delegate:           rawURL,
+		queryParams:        values,
+		encodingExceptions: nil,
+	}, nil
+}
+
 func (u *URL) WithQueryParamList(name string, values []string) {
 	u.queryParams[name] = values
 }


Test patch:
diff --git a/common/urlbuilder/url_test.go b/common/urlbuilder/url_test.go
index 0cdbbbb1..e8f60a1d 100644
--- a/common/urlbuilder/url_test.go
+++ b/common/urlbuilder/url_test.go
@@ -2,9 +2,12 @@ package urlbuilder
 
 import (
 	"errors"
+	"net/url"
 	"reflect"
 	"strings"
 	"testing"
+
+	"github.com/amp-labs/connectors/test/utils/testutils"
 )
 
 func TestNewURL(t *testing.T) { // nolint:funlen
@@ -189,3 +192,65 @@ func TestAddPath(t *testing.T) { // nolint:funlen
 		})
 	}
 }
+
+func TestFromRawURL(t *testing.T) { // nolint:funlen
+	t.Parallel()
+
+	tests := []struct {
+		name     string
+		input    string
+		expected string
+	}{
+		{
+			name:     "URL with path",
+			input:    "http://video.google.co.uk:80/videoplay",
+			expected: "http://video.google.co.uk:80/videoplay",
+		},
+		{
+			name:     "Trailing slash is preserved",
+			input:    "http://video.google.co.uk:80/",
+			expected: "http://video.google.co.uk:80/",
+		},
+		{
+			name:     "URL with query parameters",
+			input:    "https://video.google.co.uk:80/videoplay?docid=-7246927612831078230&hl=en",
+			expected: "https://video.google.co.uk:80/videoplay?docid=-7246927612831078230&hl=en",
+		},
+		{
+			name:     "Fragment identifier",
+			input:    "https://example.com/page#section",
+			expected: "https://example.com/page#section",
+		},
+		{
+			name:     "Spaces in query params are encoded with plus sign (+)",
+			input:    "https://example.com/data?id=123&info=Hello%20World",
+			expected: "https://example.com/data?id=123&info=Hello+World",
+		},
+		{
+			name:     "IP address instead of hostname",
+			input:    "http://192.168.1.1/admin",
+			expected: "http://192.168.1.1/admin",
+		},
+		{
+			name:     "WebSocket protocol",
+			input:    "ws://example.com/socket",
+			expected: "ws://example.com/socket",
+		},
+	}
+
+	for _, tt := range tests { // nolint:varnamelen
+		// nolint:varnamelen
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			endpoint, err := url.Parse(tt.input)
+			if err != nil {
+				t.Fatalf("%s: is an invalid test, check input", tt.name)
+			}
+
+			outputURL, err := FromRawURL(endpoint)
+			output := outputURL.String()
+			testutils.CheckOutputWithError(t, tt.name, tt.expected, nil, output, err)
+		})
+	}
+}