Skip to content
Snippets Groups Projects
url.go 18.6 KiB
Newer Older
AlexStocks's avatar
AlexStocks committed
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
fangyincheng's avatar
fangyincheng committed

vito.he's avatar
vito.he committed
package common
fangyincheng's avatar
fangyincheng committed

import (
	"bytes"
	"encoding/base64"
fangyincheng's avatar
fangyincheng committed
	"fmt"
zonghaishang's avatar
zonghaishang committed
	"math"
fangyincheng's avatar
fangyincheng committed
	"net"
	"net/url"
	"strconv"
	"strings"
fangyincheng's avatar
fangyincheng committed
)

import (
邹毅贤's avatar
邹毅贤 committed
	gxset "github.com/dubbogo/gost/container/set"
	"github.com/jinzhu/copier"
fangyincheng's avatar
fangyincheng committed
	perrors "github.com/pkg/errors"
xujianhai666's avatar
xujianhai666 committed
	"github.com/satori/go.uuid"
fangyincheng's avatar
fangyincheng committed
)
vito.he's avatar
vito.he committed
import (
	"github.com/apache/dubbo-go/common/constant"
flycash's avatar
flycash committed
	"github.com/apache/dubbo-go/common/logger"
vito.he's avatar
vito.he committed
)
flycash's avatar
flycash committed
// ///////////////////////////////
flycash's avatar
flycash committed
// ///////////////////////////////
邹毅贤's avatar
邹毅贤 committed
// role constant
	// CONSUMER is consumer role
	// CONFIGURATOR is configurator role
	// ROUTER is router role
	// PROVIDER is provider role
	PROTOCOL = "protocol"
	// DubboNodes Dubbo service node
	DubboNodes = [...]string{"consumers", "configurators", "routers", "providers"}
邹毅贤's avatar
邹毅贤 committed
	// DubboRole Dubbo service role
邹毅贤's avatar
邹毅贤 committed
	DubboRole = [...]string{"consumer", "", "routers", "provider"}
type RoleType int

func (t RoleType) String() string {
	return DubboNodes[t]
}

// Role returns role by @RoleType
func (t RoleType) Role() string {
	return DubboRole[t]
vito.he's avatar
vito.he committed
type baseUrl struct {
zengfanwei's avatar
zengfanwei committed
	Protocol string
	Location string // ip+port
	Ip       string
	Port     string
	//url.Values is not safe map, add to avoid concurrent map read and map write error
	paramsLock   sync.RWMutex
vito.he's avatar
vito.he committed
	params       url.Values
vito.he's avatar
vito.he committed
	PrimitiveURL string
flycash's avatar
flycash committed
// URL is not thread-safe.
// we fail to define this struct to be immutable object.
// but, those method which will update the URL, including SetParam, SetParams
// are only allowed to be invoked in creating URL instance
// Please keep in mind that this struct is immutable after it has been created and initialized.
type URL struct {
vito.he's avatar
vito.he committed
	baseUrl
	Path     string // like  /com.ikurento.dubbo.UserProvider3
	Username string
	Password string
flycash's avatar
flycash committed
	// special for registry
	SubURL *URL
// Option accepts url
// Option will define a function of handling URL
type option func(*URL)

// WithUsername sets username for url
func WithUsername(username string) option {
	return func(url *URL) {
		url.Username = username
	}
}

// WithPassword sets password for url
func WithPassword(pwd string) option {
	return func(url *URL) {
		url.Password = pwd
	}
}
// WithMethods sets methods for url
func WithMethods(methods []string) option {
	return func(url *URL) {
		url.Methods = methods
	}
}

// WithParams sets params for url
func WithParams(params url.Values) option {
	return func(url *URL) {
vito.he's avatar
vito.he committed
		url.params = params
// WithParamsValue sets params field for url
func WithParamsValue(key, val string) option {
	return func(url *URL) {
vito.he's avatar
vito.he committed
		url.SetParam(key, val)
// WithProtocol sets protocol for url
vito.he's avatar
vito.he committed
func WithProtocol(proto string) option {
	return func(url *URL) {
		url.Protocol = proto
	}
}
vito.he's avatar
vito.he committed
func WithIp(ip string) option {
	return func(url *URL) {
		url.Ip = ip
	}
}

vito.he's avatar
vito.he committed
func WithPort(port string) option {
	return func(url *URL) {
		url.Port = port
	}
}

func WithPath(path string) option {
	return func(url *URL) {
		url.Path = "/" + strings.TrimPrefix(path, "/")
// WithLocation sets location for url
func WithLocation(location string) option {
	return func(url *URL) {
		url.Location = location
	}
}
xujianhai666's avatar
xujianhai666 committed
func WithToken(token string) option {
	return func(url *URL) {
		if len(token) > 0 {
			value := token
			if strings.ToLower(token) == "true" || strings.ToLower(token) == "default" {
flycash's avatar
flycash committed
				u, err := uuid.NewV4()
				if err != nil {
					logger.Errorf("could not generator UUID: %v", err)
flycash's avatar
flycash committed
					return
flycash's avatar
flycash committed
				}
				value = u.String()
xujianhai666's avatar
xujianhai666 committed
			}
			url.SetParam(constant.TOKEN_KEY, value)
		}
	}
}

// NewURLWithOptions will create a new url with options
func NewURLWithOptions(opts ...option) *URL {
	url := &URL{}
	for _, opt := range opts {
		opt(url)
	}
fangyincheng's avatar
fangyincheng committed
	url.Location = url.Ip + ":" + url.Port
// NewURL will create a new url
// the urlString should not be empty
func NewURL(urlString string, opts ...option) (URL, error) {
fangyincheng's avatar
fangyincheng committed
	var (
		err          error
		rawUrlString string
		serviceUrl   *url.URL
		s            = URL{baseUrl: baseUrl{}}
	// new a null instance
	if urlString == "" {
		return s, nil
	}

fangyincheng's avatar
fangyincheng committed
	rawUrlString, err = url.QueryUnescape(urlString)
	if err != nil {
fangyincheng's avatar
fangyincheng committed
		return s, perrors.Errorf("url.QueryUnescape(%s),  error{%v}", urlString, err)
flycash's avatar
flycash committed
	// rawUrlString = "//" + rawUrlString
	if !strings.Contains(rawUrlString, "//") {
		t := URL{baseUrl: baseUrl{}}
		for _, opt := range opts {
			opt(&t)
		}
		rawUrlString = t.Protocol + "://" + rawUrlString
	}
fangyincheng's avatar
fangyincheng committed
	serviceUrl, err = url.Parse(rawUrlString)
	if err != nil {
fangyincheng's avatar
fangyincheng committed
		return s, perrors.Errorf("url.Parse(url string{%s}),  error{%v}", rawUrlString, err)
vito.he's avatar
vito.he committed
	s.params, err = url.ParseQuery(serviceUrl.RawQuery)
fangyincheng's avatar
fangyincheng committed
	if err != nil {
fangyincheng's avatar
fangyincheng committed
		return s, perrors.Errorf("url.ParseQuery(raw url string{%s}),  error{%v}", serviceUrl.RawQuery, err)
vito.he's avatar
vito.he committed
	s.PrimitiveURL = urlString
	s.Protocol = serviceUrl.Scheme
	s.Username = serviceUrl.User.Username()
	s.Password, _ = serviceUrl.User.Password()
vito.he's avatar
vito.he committed
	s.Location = serviceUrl.Host
	s.Path = serviceUrl.Path
	if strings.Contains(s.Location, ":") {
		s.Ip, s.Port, err = net.SplitHostPort(s.Location)
fangyincheng's avatar
fangyincheng committed
		if err != nil {
aliiohs's avatar
aliiohs committed
			return s, perrors.Errorf("net.SplitHostPort(url.Host{%s}), error{%v}", s.Location, err)
fangyincheng's avatar
fangyincheng committed
	return s, nil
}

// URLEqual judge @url and @c is equal or not.
func (c URL) URLEqual(url URL) bool {
fangyincheng's avatar
fangyincheng committed
	c.Ip = ""
	c.Port = ""
vito.he's avatar
vito.he committed
	url.Ip = ""
	url.Port = ""
	cGroup := c.GetParam(constant.GROUP_KEY, "")
	urlGroup := url.GetParam(constant.GROUP_KEY, "")
	cKey := c.Key()
	urlKey := url.Key()

	if cGroup == constant.ANY_VALUE {
		cKey = strings.Replace(cKey, "group=*", "group="+urlGroup, 1)
	} else if urlGroup == constant.ANY_VALUE {
		urlKey = strings.Replace(urlKey, "group=*", "group="+cGroup, 1)
	}
Xargin's avatar
Xargin committed

	// 1. protocol, username, password, ip, port, service name, group, version should be equal
	if cKey != urlKey {
vito.he's avatar
vito.he committed
		return false
	}
Xargin's avatar
Xargin committed

	// 2. if url contains enabled key, should be true, or *
	if url.GetParam(constant.ENABLED_KEY, "true") != "true" && url.GetParam(constant.ENABLED_KEY, "") != constant.ANY_VALUE {
		return false
	}
Xargin's avatar
Xargin committed

flycash's avatar
flycash committed
	// TODO :may need add interface key any value condition
Xargin's avatar
Xargin committed
	return isMatchCategory(url.GetParam(constant.CATEGORY_KEY, constant.DEFAULT_CATEGORY), c.GetParam(constant.CATEGORY_KEY, constant.DEFAULT_CATEGORY))
vito.he's avatar
vito.he committed
}
xg.gao's avatar
xg.gao committed

func isMatchCategory(category1 string, category2 string) bool {
	if len(category2) == 0 {
		return category1 == constant.DEFAULT_CATEGORY
	} else if strings.Contains(category2, constant.ANY_VALUE) {
		return true
	} else if strings.Contains(category2, constant.REMOVE_VALUE_PREFIX) {
		return !strings.Contains(category2, constant.REMOVE_VALUE_PREFIX+category1)
	} else {
vito.he's avatar
vito.he committed
		return strings.Contains(category2, category1)
xg.gao's avatar
xg.gao committed

func (c URL) String() string {
watermelo's avatar
watermelo committed
	var buf strings.Builder
	if len(c.Username) == 0 && len(c.Password) == 0 {
watermelo's avatar
watermelo committed
		buf.WriteString(fmt.Sprintf(
			"%s://%s:%s%s?",
watermelo's avatar
watermelo committed
			c.Protocol, c.Ip, c.Port, c.Path))
	} else {
watermelo's avatar
watermelo committed
		buf.WriteString(fmt.Sprintf(
			"%s://%s:%s@%s:%s%s?",
watermelo's avatar
watermelo committed
			c.Protocol, c.Username, c.Password, c.Ip, c.Port, c.Path))
watermelo's avatar
watermelo committed
	buf.WriteString(c.params.Encode())
	return buf.String()
vito.he's avatar
vito.he committed
func (c URL) Key() string {
	buildString := fmt.Sprintf(
		"%s://%s:%s@%s:%s/?interface=%s&group=%s&version=%s",
fangyincheng's avatar
fangyincheng committed
		c.Protocol, c.Username, c.Password, c.Ip, c.Port, c.Service(), c.GetParam(constant.GROUP_KEY, ""), c.GetParam(constant.VERSION_KEY, ""))
vito.he's avatar
vito.he committed
	return buildString
// ServiceKey gets a unique key of a service.
func (c URL) ServiceKey() string {
cvictory's avatar
cvictory committed
	return ServiceKey(c.GetParam(constant.INTERFACE_KEY, strings.TrimPrefix(c.Path, "/")),
		c.GetParam(constant.GROUP_KEY, ""), c.GetParam(constant.VERSION_KEY, ""))
}

func ServiceKey(intf string, group string, version string) string {
	if intf == "" {
		return ""
	}
	buf := &bytes.Buffer{}
	if group != "" {
		buf.WriteString(group)
		buf.WriteString("/")
	}

	buf.WriteString(intf)

fangyincheng's avatar
fangyincheng committed
	if version != "" && version != "0.0.0" {
		buf.WriteString(":")
		buf.WriteString(version)
	}

	return buf.String()
vito.he's avatar
vito.he committed
}

Ooo0oO0o0oO's avatar
Ooo0oO0o0oO committed
// ColonSeparatedKey
// The format is "{interface}:[version]:[group]"
func (c *URL) ColonSeparatedKey() string {
	intf := c.GetParam(constant.INTERFACE_KEY, strings.TrimPrefix(c.Path, "/"))
	if intf == "" {
		return ""
	}
watermelo's avatar
watermelo committed
	var buf strings.Builder
	buf.WriteString(intf)
	buf.WriteString(":")
	version := c.GetParam(constant.VERSION_KEY, "")
	if version != "" && version != "0.0.0" {
		buf.WriteString(version)
	}
	group := c.GetParam(constant.GROUP_KEY, "")
	buf.WriteString(":")
	if group != "" {
		buf.WriteString(group)
	}
	return buf.String()
}

// EncodedServiceKey encode the service key
func (c *URL) EncodedServiceKey() string {
	serviceKey := c.ServiceKey()
	return strings.Replace(serviceKey, "/", "*", 1)
}

func (c URL) Service() string {
	service := c.GetParam(constant.INTERFACE_KEY, strings.TrimPrefix(c.Path, "/"))
	if service != "" {
		return service
	} else if c.SubURL != nil {
		service = c.SubURL.GetParam(constant.INTERFACE_KEY, strings.TrimPrefix(c.Path, "/"))
flycash's avatar
flycash committed
		if service != "" { // if url.path is "" then return suburl's path, special for registry url
			return service
		}
	}
	return ""
flycash's avatar
flycash committed
// AddParam will add the key-value pair
func (c *URL) AddParam(key string, value string) {
zengfanwei's avatar
zengfanwei committed
	c.paramsLock.Lock()
	defer c.paramsLock.Unlock()
vito.he's avatar
vito.he committed
	c.params.Add(key, value)
}

// AddParamAvoidNil will add key-value pair
func (c *URL) AddParamAvoidNil(key string, value string) {
	c.paramsLock.Lock()
	defer c.paramsLock.Unlock()
	if c.params == nil {
		c.params = url.Values{}
	}

	c.params.Add(key, value)
flycash's avatar
flycash committed
// SetParam will put the key-value pair into url
// usually it should only be invoked when you want to initialized an url
vito.he's avatar
vito.he committed
func (c *URL) SetParam(key string, value string) {
zengfanwei's avatar
zengfanwei committed
	c.paramsLock.Lock()
	defer c.paramsLock.Unlock()
vito.he's avatar
vito.he committed
	c.params.Set(key, value)
flycash's avatar
flycash committed
// RangeParams will iterate the params
vito.he's avatar
vito.he committed
func (c *URL) RangeParams(f func(key, value string) bool) {
zengfanwei's avatar
zengfanwei committed
	c.paramsLock.RLock()
	defer c.paramsLock.RUnlock()
	for k, v := range c.params {
		if !f(k, v[0]) {
			break
func (c URL) GetParam(s string, d string) string {
	c.paramsLock.RLock()
	defer c.paramsLock.RUnlock()
	r := c.params.Get(s)
	if len(r) == 0 {
vito.he's avatar
vito.he committed

vito.he's avatar
vito.he committed
func (c URL) GetParams() url.Values {
	return c.params
}

// GetParamAndDecoded gets values and decode
aliiohs's avatar
aliiohs committed
func (c URL) GetParamAndDecoded(key string) (string, error) {
	ruleDec, err := base64.URLEncoding.DecodeString(c.GetParam(key, ""))
	value := string(ruleDec)
	return value, err
}
aliiohs's avatar
aliiohs committed
func (c URL) GetRawParam(key string) string {
aliiohs's avatar
aliiohs committed
	switch key {
	case PROTOCOL:
aliiohs's avatar
aliiohs committed
	case "username":
aliiohs's avatar
aliiohs committed
	case "host":
		return strings.Split(c.Location, ":")[0]
aliiohs's avatar
aliiohs committed
	case "password":
aliiohs's avatar
aliiohs committed
	case "port":
aliiohs's avatar
aliiohs committed
	case "path":
aliiohs's avatar
aliiohs committed
	default:
vito.he's avatar
vito.he committed
		return c.GetParam(key, "")
// GetParamBool judge whether @key exists or not
func (c URL) GetParamBool(key string, d bool) bool {
	r, err := strconv.ParseBool(c.GetParam(key, ""))
	if err != nil {
aliiohs's avatar
aliiohs committed
		return d
	}
	return r
}
// GetParamInt gets int64 value by @key
func (c URL) GetParamInt(key string, d int64) int64 {
	r, err := strconv.ParseInt(c.GetParam(key, ""), 10, 64)
	if r == 0 || err != nil {
	return r
}

// GetParamInt32 gets int32 value by @key
func (c URL) GetParamInt32(key string, d int32) int32 {
	r, err := strconv.ParseInt(c.GetParam(key, ""), 10, 32)
	if r == 0 || err != nil {
		return d
	}
	return int32(r)
}

// GetParamByInt0 gets int value by @key
func (c URL) GetParamByInt0(key string, d int) int {
	r, err := strconv.ParseInt(c.GetParam(key, ""), 10, 0)
	if r == 0 || err != nil {
		return d
	}
	return int(r)
// GetMethodParamInt gets int method param
func (c URL) GetMethodParamInt(method string, key string, d int64) int64 {
	r, err := strconv.ParseInt(c.GetParam("methods."+method+"."+key, ""), 10, 64)
	if r == 0 || err != nil {
		return d
	}
	return r
}

// GetMethodParamInt0 gets int method param
func (c URL) GetMethodParamInt0(method string, key string, d int) int {
	r, err := strconv.ParseInt(c.GetParam("methods."+method+"."+key, ""), 10, 0)
	if r == 0 || err != nil {
// GetMethodParamInt64 gets int64 method param
zonghaishang's avatar
zonghaishang committed
func (c URL) GetMethodParamInt64(method string, key string, d int64) int64 {
	r := c.GetMethodParamInt(method, key, math.MinInt64)
	if r == math.MinInt64 {
		return c.GetParamInt(key, d)
	}
	return r
}

// GetMethodParam gets method param
func (c URL) GetMethodParam(method string, key string, d string) string {
	r := c.GetParam("methods."+method+"."+key, "")
	if r == "" {
// GetMethodParamBool judge whether @method param exists or not
Ooo0oO0o0oO's avatar
Ooo0oO0o0oO committed
func (c URL) GetMethodParamBool(method string, key string, d bool) bool {
	r := c.GetParamBool("methods."+method+"."+key, d)
	return r
}

flycash's avatar
flycash committed
// SetParams will put all key-value pair into url.
// 1. if there already has same key, the value will be override
// 2. it's not thread safe
// 3. think twice when you want to invoke this method
vito.he's avatar
vito.he committed
func (c *URL) SetParams(m url.Values) {
	for k := range m {
		c.SetParam(k, m.Get(k))
aliiohs's avatar
aliiohs committed
// ToMap transfer URL to Map
func (c URL) ToMap() map[string]string {
	paramsMap := make(map[string]string)
vito.he's avatar
vito.he committed
	c.RangeParams(func(key, value string) bool {
		paramsMap[key] = value
		return true
	})

aliiohs's avatar
aliiohs committed
	if c.Protocol != "" {
		paramsMap[PROTOCOL] = c.Protocol
aliiohs's avatar
aliiohs committed
	}
	if c.Username != "" {
		paramsMap["username"] = c.Username
	}
	if c.Password != "" {
		paramsMap["password"] = c.Password
	}
	if c.Location != "" {
		paramsMap["host"] = strings.Split(c.Location, ":")[0]
		var port string
		if strings.Contains(c.Location, ":") {
			port = strings.Split(c.Location, ":")[1]
		} else {
			port = "0"
		}
		paramsMap["port"] = port
aliiohs's avatar
aliiohs committed
	}
	if c.Protocol != "" {
		paramsMap[PROTOCOL] = c.Protocol
aliiohs's avatar
aliiohs committed
	}
	if c.Path != "" {
		paramsMap["path"] = c.Path
	}
aliiohs's avatar
aliiohs committed
	if len(paramsMap) == 0 {
		return nil
	}
aliiohs's avatar
aliiohs committed
	return paramsMap
}

// configuration  > reference config >service config
//  in this function we should merge the reference local url config into the service url from registry.
flycash's avatar
flycash committed
// TODO configuration merge, in the future , the configuration center's config should merge too.

// MergeUrl will merge those two url
// the result is based on serviceUrl, and the key which si only contained in referenceUrl
// will be added into result.
// for example, if serviceUrl contains params (a1->v1, b1->v2) and referenceUrl contains params(a2->v3, b1 -> v4)
// the params of result will be (a1->v1, b1->v2, a2->v3).
// You should notice that the value of b1 is v2, not v4.
// due to URL is not thread-safe, so this method is not thread-safe
vito.he's avatar
vito.he committed
func MergeUrl(serviceUrl *URL, referenceUrl *URL) *URL {
	mergedUrl := serviceUrl.Clone()
flycash's avatar
flycash committed
	// iterator the referenceUrl if serviceUrl not have the key ,merge in
vito.he's avatar
vito.he committed
	referenceUrl.RangeParams(func(key, value string) bool {
		if v := mergedUrl.GetParam(key, ""); len(v) == 0 {
			mergedUrl.SetParam(key, value)
vito.he's avatar
vito.he committed
		return true
flycash's avatar
flycash committed
	// loadBalance,cluster,retries strategy config
	methodConfigMergeFcn := mergeNormalParam(mergedUrl, referenceUrl, []string{constant.LOADBALANCE_KEY, constant.CLUSTER_KEY, constant.RETRIES_KEY, constant.TIMEOUT_KEY})
flycash's avatar
flycash committed
	// remote timestamp
vito.he's avatar
vito.he committed
	if v := serviceUrl.GetParam(constant.TIMESTAMP_KEY, ""); len(v) > 0 {
		mergedUrl.SetParam(constant.REMOTE_TIMESTAMP_KEY, v)
		mergedUrl.SetParam(constant.TIMESTAMP_KEY, referenceUrl.GetParam(constant.TIMESTAMP_KEY, ""))
flycash's avatar
flycash committed
	// finally execute methodConfigMergeFcn
	for _, method := range referenceUrl.Methods {
		for _, fcn := range methodConfigMergeFcn {
			fcn("methods." + method)
		}
	}

	return mergedUrl
}
flycash's avatar
flycash committed
// Clone will copy the url
func (c *URL) Clone() *URL {
	newUrl := &URL{}
	copier.Copy(newUrl, c)
vito.he's avatar
vito.he committed
	newUrl.params = url.Values{}
	c.RangeParams(func(key, value string) bool {
		newUrl.SetParam(key, value)
		return true
	})
vito.he's avatar
vito.he committed

flycash's avatar
flycash committed
func (c *URL) CloneExceptParams(excludeParams *gxset.HashSet) *URL {
	newUrl := &URL{}
	copier.Copy(newUrl, c)
	newUrl.params = url.Values{}
	c.RangeParams(func(key, value string) bool {
		if !excludeParams.Contains(key) {
			newUrl.SetParam(key, value)
		}
		return true
	})
	return newUrl
}

// Copy url based on the reserved parameter's keys.
xg.gao's avatar
xg.gao committed
func (c *URL) CloneWithParams(reserveParams []string) *URL {
	params := url.Values{}
	for _, reserveParam := range reserveParams {
		v := c.GetParam(reserveParam, "")
xg.gao's avatar
xg.gao committed
		if len(v) != 0 {
			params.Set(reserveParam, v)
		}
	}

	return NewURLWithOptions(
		WithProtocol(c.Protocol),
		WithUsername(c.Username),
		WithPassword(c.Password),
		WithIp(c.Ip),
		WithPort(c.Port),
		WithPath(c.Path),
xg.gao's avatar
xg.gao committed
		WithMethods(c.Methods),
Ian Luo's avatar
Ian Luo committed
// IsEquals compares if two URLs equals with each other. Excludes are all parameter keys which should ignored.
func IsEquals(left URL, right URL, excludes ...string) bool {
	if left.Ip != right.Ip || left.Port != right.Port {
		return false
	}

	leftMap := left.ToMap()
	rightMap := right.ToMap()
	for _, exclude := range excludes {
		delete(leftMap, exclude)
		delete(rightMap, exclude)
	}

	if len(leftMap) != len(rightMap) {
		return false
	}

	for lk, lv := range leftMap {
		if rv, ok := rightMap[lk]; !ok {
			return false
		} else if lv != rv {
			return false
		}
	}

	return true
}

vito.he's avatar
vito.he committed
func mergeNormalParam(mergedUrl *URL, referenceUrl *URL, paramKeys []string) []func(method string) {
	methodConfigMergeFcn := make([]func(method string), 0, len(paramKeys))
vito.he's avatar
vito.he committed
	for _, paramKey := range paramKeys {
vito.he's avatar
vito.he committed
		if v := referenceUrl.GetParam(paramKey, ""); len(v) > 0 {
			mergedUrl.SetParam(paramKey, v)
vito.he's avatar
vito.he committed
		}
		methodConfigMergeFcn = append(methodConfigMergeFcn, func(method string) {
vito.he's avatar
vito.he committed
			if v := referenceUrl.GetParam(method+"."+paramKey, ""); len(v) > 0 {
				mergedUrl.SetParam(method+"."+paramKey, v)
vito.he's avatar
vito.he committed
			}
		})
	}
vito.he's avatar
vito.he committed
	return methodConfigMergeFcn
vito.he's avatar
vito.he committed
}
flycash's avatar
flycash committed
// URLSlice will be used to sort URL instance
// Instances will be order by URL.String()
flycash's avatar
flycash committed
type URLSlice []URL

flycash's avatar
flycash committed
// nolint
flycash's avatar
flycash committed
func (s URLSlice) Len() int {
	return len(s)
}

flycash's avatar
flycash committed
// nolint
flycash's avatar
flycash committed
func (s URLSlice) Less(i, j int) bool {
	return s[i].String() < s[j].String()
}

flycash's avatar
flycash committed
// nolint
flycash's avatar
flycash committed
func (s URLSlice) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}