diff --git a/go.mod b/go.mod index 7f10cfa..f709a0c 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,6 @@ module github.com/kashifkhan0771/utils go 1.18 -require golang.org/x/text v0.14.0 +require golang.org/x/text v0.20.0 + +require golang.org/x/net v0.31.0 // indirect diff --git a/go.sum b/go.sum index c9c7c64..ac0443a 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= diff --git a/url/url.go b/url/url.go new file mode 100644 index 0000000..863d4b7 --- /dev/null +++ b/url/url.go @@ -0,0 +1,324 @@ +/* +Package url defines url utilities helpers. +*/ +package url + +import ( + "errors" + "golang.org/x/net/publicsuffix" + "net/url" + "regexp" + "strings" +) + +// BuildURL constructs a URL by combining a scheme, host, path, and query parameters. +// +// Parameters: +// - scheme: The URL scheme (e.g., "http", "https") to use. +// - host: The host or domain name (e.g., "example.com"). +// - path: The path part of the URL (e.g., "path/to/resource"). +// - query: A map[string]string containing query parameters to append to the URL. +// +// Returns: +// - string: The constructed URL with the scheme, host, path, and query parameters. +// - error: An error if the URL could not be parsed or if any issues occurred during URL construction. +// +// Behavior: +// - The function concatenates the scheme, host, and path into a URL string. +// - It then attempts to parse the constructed URL and add the query parameters. +// - If the URL parsing fails, an error is returned. +// - Query parameters are added one by one using the `url.Values.Add` method to ensure proper encoding. +// +// Example: +// +// scheme := "https" +// host := "example.com" +// path := "search" +// query := map[string]string{ +// "q": "golang", +// "page": "1", +// } +// fullURL, err := BuildURL(scheme, host, path, query) +// if err != nil { +// fmt.Println("Error:", err) +// } else { +// fmt.Println(fullURL) +// // Output: https://example.com/search?q=golang&page=1 +// } +// +// Notes: +// - If any query parameters are provided, they will be encoded and appended to the URL. +// - If the path is empty, a trailing slash will be included after the host. +// - The function ensures the proper encoding of query parameters and safely constructs the final URL. +// +// Usage: +// +// To construct a URL with query parameters: +// queryParams := map[string]string{"key": "value", "anotherKey": "anotherValue"} +// url, err := BuildURL("http", "example.com", "search", queryParams) +// fmt.Println("Constructed URL:", url) +func BuildURL(scheme, host, path string, query map[string]string) (string, error) { + var errMessage []string + if scheme == "" { + errMessage = append(errMessage, "scheme is required") + } + if host != "" { + re := regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])(\.[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])*$`) + if !re.MatchString(host) { + errMessage = append(errMessage, "the host is not valid") + } + } + if host == "" { + errMessage = append(errMessage, "host is required") + } + + if path != "" { + re := regexp.MustCompile("^[a-zA-Z]+(\\/[a-zA-Z]+)*$") + if !re.MatchString(path) { + errMessage = append(errMessage, "path is permitted with a-z character and multiple path segments") + } + } + + if errMessage != nil { + return "", errors.New(strings.Join(errMessage, "; ")) + } + + parsedUrl := &url.URL{ + Scheme: scheme, + Host: host, + } + + if path == "" { + parsedUrl.Path = "/" + } else if !strings.HasPrefix(path, "/") { + parsedUrl.Path = "/" + path + } else { + parsedUrl.Path = path + } + queryParams := parsedUrl.Query() + for key, value := range query { + queryParams.Add(key, value) + } + parsedUrl.RawQuery = queryParams.Encode() + + return parsedUrl.String(), nil +} + +// AddQueryParams adds multiple query parameters to a given URL and returns the updated URL. +// +// Parameters: +// - urlStr: A string representing the base URL to which query parameters should be added. +// - params: A map[string]string containing key-value pairs of query parameters to add. +// +// Returns: +// - string: The updated URL with the new query parameters appended. +// - error: An error if the URL cannot be parsed. +// +// Behavior: +// - The function parses the provided URL string using `net/url.Parse`. +// - Iterates through the `params` map, adding each key-value pair as a query parameter to the URL. +// - Encodes the updated query parameters back into the URL. +// +// Example: +// +// baseURL := "https://example.com/path" +// params := map[string]string{ +// "param1": "value1", +// "param2": "value2", +// } +// updatedURL, err := AddQueryParams(baseURL, params) +// if err != nil { +// fmt.Println("Error:", err) +// } else { +// fmt.Println(updatedURL) +// // Output: https://example.com/path?param1=value1¶m2=value2 +// } +// +// Notes: +// - If a query parameter key already exists in the URL, `Add` appends the new value instead of overwriting it. +// - Use this function when you need to dynamically construct URLs with multiple query parameters. +// - The function ensures proper encoding of query parameters. +// +// Usage: +// +// To add query parameters to a URL: +// params := map[string]string{"key": "value", "foo": "bar"} +// updatedURL, err := AddQueryParams("http://example.com", params) +// fmt.Println("Result:", updatedURL) +func AddQueryParams(urlStr string, params map[string]string) (string, error) { + parsedURL, err := url.Parse(urlStr) + if err != nil { + return "", errors.New("URL could not be parsed") + } + switch parsedURL.Scheme { + case "http", "https", "ws", "wss", "ftp": + default: + return "", errors.New("invalid URL scheme") + } + queryParams := parsedURL.Query() + for key, value := range params { + re := regexp.MustCompile("^[a-zA-Z0-9-]+$") + if !re.MatchString(value) || !re.MatchString(key) || value == "" { + return "", errors.New("the query parameter is not valid") + } + queryParams.Add(key, value) + } + parsedURL.RawQuery = queryParams.Encode() + return parsedURL.String(), nil +} + +// IsValidURL checks whether a given URL string is valid and its scheme matches the allowed list. +// +// Parameters: +// - urlStr: A string representing the URL to validate. +// - allowedReqSchemes: A slice of strings containing the allowed schemes (e.g., "http", "https"). +// +// Returns: +// - bool: `true` if the URL is valid and its scheme is in the allowed list; otherwise, `false`. +// +// Behavior: +// - The function attempts to parse the provided URL string using `net/url.Parse`. +// - If the URL is invalid or its scheme is not in the allowed list, the function returns `false`. +// - If the URL is valid and the scheme is allowed, the function returns `true`. +// +// Example: +// +// url := "https://example.com" +// allowed := []string{"http", "https"} +// isValid := IsValidURL(url, allowed) +// if isValid { +// fmt.Println("URL is valid and uses an allowed scheme.") +// } else { +// fmt.Println("Invalid URL or scheme.") +// } +// +// Notes: +// - The function does not check other parts of the URL (e.g., hostname, path, query parameters). +// - Use this function when you need to validate both the structure and scheme of a URL. +// +// Usage: +// +// To validate URLs and restrict their schemes: +// valid := IsValidURL("http://example.com", []string{"http", "https"}) +// fmt.Println("Is valid:", valid) +func IsValidURL(urlStr string, allowedReqSchemes []string) bool { + if urlStr == "" { + return false + } + parsedURL, err := url.Parse(urlStr) + if err != nil { + return false + } + for _, scheme := range allowedReqSchemes { + if scheme == "" { + return false + } + if parsedURL.Scheme == scheme { + return true + } + } + return false +} + +// ExtractDomain extracts the domain (hostname) from a given URL string. +// +// Parameters: +// - urlStr: A string representing the URL from which to extract the domain. +// +// Returns: +// - string: The domain (hostname) extracted from the URL. +// - error: An error if the URL is invalid or the domain cannot be determined. +// +// Errors: +// - Returns an error if the provided URL string is invalid or cannot be parsed. +// - Returns "parameter not found" error if the URL does not contain a hostname. +// +// Example: +// +// url := "https://example.com/path?query=value" +// domain, err := ExtractDomain(url) +// if err != nil { +// log.Println("Error:", err) +// } else { +// log.Println("Domain:", domain) // Output: "example.com" +// +// Notes: +// - This function uses `net/url.ParseRequestURI` to validate and parse the URL. +// - It extracts the hostname part of the URL and ignores the port, path, query, or fragment. +// +// Usage: +// +// To extract the domain from a URL: +// domain, err := ExtractDomain("http://example.com/some-path") +// if err != nil { +// fmt.Println("Error:", err) +// } else { +// fmt.Println("Domain:", domain) +func ExtractDomain(urlStr string) (string, error) { + parsedURL, err := url.Parse(urlStr) + if err != nil { + return "", errors.New("URL could not be parsed") + } + + host, err := publicsuffix.EffectiveTLDPlusOne(parsedURL.Hostname()) + if err != nil { + return "", errors.New("could not extract public suffix") + } + + if host == "" { + return "", errors.New("parameter not found") + } + + return host, nil +} + +// GetQueryParam extracts the value of a specified query parameter from a given URL string. +// +// Parameters: +// - urlStr: A string representing the URL containing query parameters. +// - param: The name of the query parameter to retrieve. +// +// Returns: +// - string: The value of the specified query parameter. +// - error: An error if the URL is invalid or the parameter is not found. +// +// Errors: +// - Returns an error if the provided URL string is invalid or cannot be parsed. +// - Returns "parameter not found" error if the specified parameter does not exist. +// +// Example: +// +// url := "https://example.com?foo=bar&baz=qux" +// value, err := GetQueryParam(url, "foo") +// if err != nil { +// log.Println("Error:", err) +// } else { +// log.Println("Value:", value) // Output: "bar" +// +// Notes: +// - This function uses the `net/url` package for robust URL parsing. +// - It assumes the URL is properly formatted with query parameters starting after a "?". +// +// Usage: +// +// To retrieve the value of a query parameter from a URL: +// value, err := GetQueryParam("http://example.com?key=value", "key") +// if err != nil { +// fmt.Println("Error:", err) +// } else { +// fmt.Println("Value:", value) +func GetQueryParam(urlStr, param string) (string, error) { + parsedURL, err := url.Parse(urlStr) + if err != nil { + return "", err + } + + queryParams := parsedURL.Query() + value, exists := queryParams[param] + + if !exists || len(value) == 0 { + return "", errors.New("parameter not found") + } + + return value[0], nil +} diff --git a/url/url_test.go b/url/url_test.go new file mode 100644 index 0000000..7d15c27 --- /dev/null +++ b/url/url_test.go @@ -0,0 +1,412 @@ +/* +Package url defines url utilities helpers. +*/ +package url + +import "testing" + +func TestBuildURL(t *testing.T) { + type args struct { + scheme string + host string + path string + queryParams map[string]string + } + + tests := []struct { + name string + args args + want string + }{ + { + name: "success - simple URL with single query param", + args: args{scheme: "http", host: "example.com", path: "onePath", queryParams: map[string]string{"queryParamOne": "valueQueryParamOne"}}, + want: "http://example.com/onePath?queryParamOne=valueQueryParamOne", + }, + { + name: "success - URL with multiple path segments and query params", + args: args{scheme: "http", host: "example.com", path: "onePath/otherPath/other", queryParams: map[string]string{"queryParamOne": "valueQueryParamOne", "queryParamTwo": "valueQueryParamTwo"}}, + want: "http://example.com/onePath/otherPath/other?queryParamOne=valueQueryParamOne&queryParamTwo=valueQueryParamTwo", + }, + { + name: "success - subdomain URL with multiple query params", + args: args{scheme: "http", host: "subdomain.example.com", path: "onePath", queryParams: map[string]string{"queryParamOne": "valueQueryParamOne", "queryParamTwo": "valueQueryParamTwo"}}, + want: "http://subdomain.example.com/onePath?queryParamOne=valueQueryParamOne&queryParamTwo=valueQueryParamTwo", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if value, _ := BuildURL(tt.args.scheme, tt.args.host, tt.args.path, tt.args.queryParams); value != tt.want { + t.Errorf("BuildURL() = got %v, want %v", value, tt.want) + } + }) + } +} + +func TestBuildURLError(t *testing.T) { + type args struct { + scheme string + host string + path string + queryParams map[string]string + } + + tests := []struct { + name string + args args + want string + }{ + { + name: "error - build URL", + args: args{scheme: "", host: "example.com", path: "onePath", queryParams: map[string]string{"queryParamOne": "valueQueryParamOne"}}, + want: "scheme is required", + }, + { + name: "error - build URL", + args: args{scheme: "http", host: "", path: "onePath", queryParams: map[string]string{"queryParamOne": "valueQueryParamOne", "queryParamTwo": "valueQueryParamTwo"}}, + want: "host is required", + }, + { + name: "error - build URL", + args: args{scheme: "http", host: "example.com", path: "one1Path2", queryParams: map[string]string{"queryParamOne": "valueQueryParamOne", "queryParamTwo": "valueQueryParamTwo"}}, + want: "path is permitted with a-z character and multiple path segments", + }, + { + name: "error - build URL", + args: args{scheme: "", host: "", path: "onePath", queryParams: map[string]string{"queryParamOne": "valueQueryParamOne", "queryParamTwo": "valueQueryParamTwo"}}, + want: "scheme is required; host is required", + }, + { + name: "error - build URL", + args: args{scheme: "http", host: "ex@ample.com", path: "onePath", queryParams: map[string]string{"queryParamOne": "valueQueryParamOne", "queryParamTwo": "valueQueryParamTwo"}}, + want: "the host is not valid", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := BuildURL(tt.args.scheme, tt.args.host, tt.args.path, tt.args.queryParams); err.Error() != tt.want { + t.Errorf("BuildURL() = got %v, want %v", err.Error(), tt.want) + } + }) + } +} + +func TestAddQueryParams(t *testing.T) { + type args struct { + urlStr string + queryParams map[string]string + } + + tests := []struct { + name string + args args + want string + }{ + { + name: "success - add query params", + args: args{urlStr: "http://example.com", queryParams: map[string]string{"queryParamOne": "valueQueryParamOne"}}, + want: "http://example.com?queryParamOne=valueQueryParamOne", + }, + { + name: "success - add query params", + args: args{urlStr: "http://subdomain.example.com", queryParams: map[string]string{"queryParamOne": "valueQueryParamOne", "queryParamTwo": "valueQueryParamTwo"}}, + want: "http://subdomain.example.com?queryParamOne=valueQueryParamOne&queryParamTwo=valueQueryParamTwo", + }, + { + name: "success - add query params", + args: args{urlStr: "http://subdomain.example.com?firstQueryParam=anyValidValue", queryParams: map[string]string{"queryParamOne": "valueQueryParamOne", "queryParamTwo": "valueQueryParamTwo"}}, + want: "http://subdomain.example.com?firstQueryParam=anyValidValue&queryParamOne=valueQueryParamOne&queryParamTwo=valueQueryParamTwo", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if valid, _ := AddQueryParams(tt.args.urlStr, tt.args.queryParams); valid != tt.want { + t.Errorf("AddQueryParams() = got %v, want %v", valid, tt.want) + } + }) + } +} + +func TestAddQueryParamsError(t *testing.T) { + type args struct { + urlStr string + queryParams map[string]string + } + + tests := []struct { + name string + args args + want string + }{ + { + name: "error - add query params", + args: args{urlStr: "htt@p://example.com", queryParams: map[string]string{"queryParamOne": "valueQueryParamOne"}}, + want: "URL could not be parsed", + }, + { + name: "error - add query params", + args: args{urlStr: "http://subdomain.example.com", queryParams: map[string]string{"queryParam@One": "valueQueryParamOne", "queryParamTwo": "valueQueryParamTwo"}}, + want: "the query parameter is not valid", + }, + { + name: "error - add query params", + args: args{urlStr: "http://subdomain.example.com", queryParams: map[string]string{"queryParamOne": "valueQuery@ParamOne", "queryParamTwo": "valueQueryParamTwo"}}, + want: "the query parameter is not valid", + }, + { + name: "error - add query params", + args: args{urlStr: "http://subdomain.example.com", queryParams: map[string]string{"queryParamOne": ""}}, + want: "the query parameter is not valid", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := AddQueryParams(tt.args.urlStr, tt.args.queryParams); err.Error() != tt.want { + t.Errorf("AddQueryParams() = got %v, want %v", err.Error(), tt.want) + } + }) + } +} + +func TestIsValidURL(t *testing.T) { + type args struct { + urlStr string + validScheme []string + } + + tests := []struct { + name string + args args + want bool + }{ + { + name: "success - is valid URL", + args: args{urlStr: "http://example.com?someQuery=oneValue&otherQuery=otherValue", validScheme: []string{"https"}}, + want: false, + }, + { + name: "success - is valid URL", + args: args{urlStr: "http://subdomain.example.com?someQuery=oneValue&otherQuery=otherValue", validScheme: []string{"http", "https"}}, + want: true, + }, + { + name: "success - is valid URL", + args: args{urlStr: "ws://subdomain.example.com?someQuery=oneValue&otherQuery=otherValue", validScheme: []string{"http", "https", "ftp", "ws", "wss"}}, + want: true, + }, + { + name: "success - is valid URL", + args: args{urlStr: "ftp://subdomain.example.com?someQuery=oneValue&otherQuery=otherValue", validScheme: []string{"http", "https", "ftp", "ws", "wss"}}, + want: true, + }, + { + name: "success - is valid URL", + args: args{urlStr: "wss://subdomain.example.com?someQuery=oneValue&otherQuery=otherValue", validScheme: []string{"http", "https", "ftp", "ws", "wss"}}, + want: true, + }, + { + name: "success - is valid URL", + args: args{urlStr: "", validScheme: []string{"http", "https"}}, + want: false, + }, + { + name: "success - is valid URL", + args: args{urlStr: "example.com", validScheme: []string{"http", "https"}}, + want: false, + }, + { + name: "success - is valid URL", + args: args{urlStr: "example.com", validScheme: []string{""}}, + want: false, + }, + { + name: "success - is valid URL", + args: args{urlStr: "exam@ple.com", validScheme: []string{"http", "https"}}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if valid := IsValidURL(tt.args.urlStr, tt.args.validScheme); valid != tt.want { + t.Errorf("IsValidURL() = got %v, want %v", valid, tt.want) + } + }) + } +} + +func TestExtractDomain(t *testing.T) { + type args struct { + urlStr string + } + + tests := []struct { + name string + args args + want string + }{ + { + name: "success - extract domain", + args: args{urlStr: "http://example.com?someQuery=oneValue&otherQuery=otherValue"}, + want: "example.com", + }, + { + name: "success - domain with multiple subdomains", + args: args{urlStr: "https://a.b.c.example.com"}, + want: "example.com", + }, + { + name: "success - domain with port", + args: args{urlStr: "https://example.com:8080"}, + want: "example.com", + }, + { + name: "success - international domain", + args: args{urlStr: "https://münchen.de"}, + want: "münchen.de", + }, + { + name: "error - invalid URL", + args: args{urlStr: "not-a-url"}, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got, _ := ExtractDomain(tt.args.urlStr); got != tt.want { + t.Errorf("ExtractDomain() = %s, want %s", got, tt.want) + } + }) + } +} + +func TestExtractDomainError(t *testing.T) { + type args struct { + urlStr string + } + + tests := []struct { + name string + args args + want string + }{ + { + name: "success - domain with multiple subdomains", + args: args{urlStr: "htt@ps://exam@ple.com"}, + want: "URL could not be parsed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := ExtractDomain(tt.args.urlStr); err.Error() != tt.want { + t.Errorf("ExtractDomain() = %s, want %s", err.Error(), tt.want) + } + }) + } +} + +func TestGetQueryParam(t *testing.T) { + type args struct { + urlStr string + param string + } + + tests := []struct { + name string + args args + want interface{} + }{ + { + name: "success - get query param", + args: args{urlStr: "http://someurl.com?paramOne=oneValue¶mTwo=otherValue", param: "paramOne"}, + want: "oneValue", + }, + { + name: "success - get query param", + args: args{urlStr: "http://someurl.com?paramOne=oneValue¶mTwo=otherValue&oneQuery=value&otherQuery=otherValue", param: "paramTwo"}, + want: "otherValue", + }, + { + name: "success - get query param", + args: args{urlStr: "http://someurl.com?paramOne=oneValue¶mTwo=otherValue&oneQuery=valueOneQuery&otherQuery=otherValue", param: "oneQuery"}, + want: "valueOneQuery", + }, + { + name: "success - get query param", + args: args{urlStr: "http://someurl.com?paramOne=oneValue¶mTwo=otherValue&oneQuery=valueOneQuery&otherQuery=otherQueryValue", param: "otherQuery"}, + want: "otherQueryValue", + }, + { + name: "success - simple parameter", + args: args{ + urlStr: "http://example.com?key=value", + param: "key", + }, + want: "value", + }, + { + name: "success - encoded parameter", + args: args{ + urlStr: "http://example.com?key=value+with+spaces%26special%3Dchars", + param: "key", + }, + want: "value with spaces&special=chars", + }, + { + name: "success - empty parameter value", + args: args{ + urlStr: "http://example.com?key=", + param: "key", + }, + want: "", + }, + { + name: "error - parameter not found", + args: args{ + urlStr: "http://example.com?key=value", + param: "missing", + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got, _ := GetQueryParam(tt.args.urlStr, tt.args.param); got != tt.want { + t.Errorf("GetQueryParam() = %s, want %s", got, tt.want) + } + }) + } +} + +func TestGetQueryParamError(t *testing.T) { + type args struct { + urlStr string + param string + } + testError := struct { + name string + args args + want interface{} + }{ + name: "success - get query param with error", + args: args{urlStr: "http://someurl.com?paramOne=oneValue¶mTwo=otherValue", param: "none"}, + want: "parameter not found", + } + t.Run(testError.name, func(t *testing.T) { + if _, err := GetQueryParam(testError.args.urlStr, testError.args.param); err == nil { + t.Errorf("GetQueryParam() did not return an error") + } + }) + + t.Run(testError.name, func(t *testing.T) { + if _, err := GetQueryParam(testError.args.urlStr, testError.args.param); err.Error() != "parameter not found" { + t.Errorf("GetQueryParam() did not return an error") + } + }) +}