Skip to content

Commit

Permalink
Path vars
Browse files Browse the repository at this point in the history
Path vars are now flexible and accept other types apart from strings
Plus added host, headers and query params for building full URLs
  • Loading branch information
marrow16 committed Nov 11, 2022
1 parent 8680e2b commit 0477d98
Show file tree
Hide file tree
Showing 14 changed files with 1,023 additions and 40 deletions.
81 changes: 81 additions & 0 deletions headers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package urit

import (
"errors"
)

type HeadersOption interface {
GetHeaders() (map[string]string, error)
}

type Headers interface {
HeadersOption
Set(key string, value interface{}) Headers
Get(key string) (interface{}, bool)
Has(key string) bool
Del(key string) Headers
Clone() Headers
}

func NewHeaders(namesAndValues ...interface{}) (Headers, error) {
if len(namesAndValues)%2 != 0 {
return nil, errors.New("must be a value for each name")
}
result := &headers{
entries: map[string]interface{}{},
}
for i := 0; i < len(namesAndValues)-1; i += 2 {
if k, ok := namesAndValues[i].(string); ok {
result.entries[k] = namesAndValues[i+1]
} else {
return nil, errors.New("name must be a string")
}
}
return result, nil
}

type headers struct {
entries map[string]interface{}
}

func (h *headers) GetHeaders() (map[string]string, error) {
result := map[string]string{}
for k, v := range h.entries {
if str, err := getValue(v); err == nil {
result[k] = str
} else {
return result, err
}
}
return result, nil
}

func (h *headers) Set(key string, value interface{}) Headers {
h.entries[key] = value
return h
}

func (h *headers) Get(key string) (interface{}, bool) {
v, ok := h.entries[key]
return v, ok
}

func (h *headers) Has(key string) bool {
_, ok := h.entries[key]
return ok
}

func (h *headers) Del(key string) Headers {
delete(h.entries, key)
return h
}

func (h *headers) Clone() Headers {
result := &headers{
entries: map[string]interface{}{},
}
for k, v := range h.entries {
result.entries[k] = v
}
return result
}
105 changes: 105 additions & 0 deletions headers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package urit

import (
"github.com/stretchr/testify/require"
"testing"
)

func TestNewHeaders(t *testing.T) {
h, err := NewHeaders()
require.NoError(t, err)
require.NotNil(t, h)
rh, ok := h.(*headers)
require.True(t, ok)
require.Equal(t, 0, len(rh.entries))

h, err = NewHeaders("foo", 1.23)
require.NoError(t, err)
require.NotNil(t, h)
rh, ok = h.(*headers)
require.True(t, ok)
require.Equal(t, 1, len(rh.entries))
require.Equal(t, 1.23, rh.entries["foo"])
}

func TestNewHeadersErrors(t *testing.T) {
_, err := NewHeaders("foo") // must be an even number!
require.Error(t, err)
require.Equal(t, `must be a value for each name`, err.Error())

_, err = NewHeaders(true, false) // first must be a string!
require.Error(t, err)
require.Equal(t, `name must be a string`, err.Error())
}

func TestHeaders_GetHeaders(t *testing.T) {
h, err := NewHeaders()
require.NoError(t, err)
hds, err := h.GetHeaders()
require.NoError(t, err)
require.Equal(t, 0, len(hds))

h, err = NewHeaders("foo", 1.23)
require.NoError(t, err)
hds, err = h.GetHeaders()
require.NoError(t, err)
require.Equal(t, 1, len(hds))
require.Equal(t, "1.23", hds["foo"])

h, err = NewHeaders("foo", nil)
require.NoError(t, err)
_, err = h.GetHeaders()
require.Error(t, err)
require.Equal(t, `unknown value type`, err.Error())

h, err = NewHeaders("foo", func() {
// this does not yield a string
})
require.NoError(t, err)
_, err = h.GetHeaders()
require.Error(t, err)
require.Equal(t, `unknown value type`, err.Error())
}

func TestHeaders_GetSet(t *testing.T) {
h, err := NewHeaders("foo", 1)
require.NoError(t, err)
v, ok := h.Get("foo")
require.True(t, ok)
require.Equal(t, 1, v)
h.Set("foo", 2)
v, ok = h.Get("foo")
require.True(t, ok)
require.Equal(t, 2, v)
_, ok = h.Get("bar")
require.False(t, ok)
}

func TestHeaders_HasDel(t *testing.T) {
h, err := NewHeaders("foo", 1)
require.NoError(t, err)
require.True(t, h.Has("foo"))
h.Del("foo")
require.False(t, h.Has("foo"))
require.False(t, h.Has("bar"))
}

func TestHeaders_Clone(t *testing.T) {
h1, err := NewHeaders("foo", 1)
require.NoError(t, err)
require.True(t, h1.Has("foo"))
require.False(t, h1.Has("bar"))
h2 := h1.Clone()
require.True(t, h2.Has("foo"))
require.False(t, h2.Has("bar"))
h2.Del("foo")
require.True(t, h1.Has("foo"))
require.False(t, h1.Has("bar"))
require.False(t, h2.Has("foo"))
require.False(t, h2.Has("bar"))
h2.Set("bar", 2)
require.True(t, h1.Has("foo"))
require.False(t, h1.Has("bar"))
require.False(t, h2.Has("foo"))
require.True(t, h2.Has("bar"))
}
23 changes: 23 additions & 0 deletions host.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package urit

type HostOption interface {
GetAddress() string
}

type Host interface {
HostOption
}

func NewHost(address string) Host {
return &host{
address: address,
}
}

type host struct {
address string
}

func (h *host) GetAddress() string {
return h.address
}
15 changes: 15 additions & 0 deletions host_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package urit

import (
"github.com/stretchr/testify/require"
"testing"
)

func TestNewHost(t *testing.T) {
h := NewHost(`www.example.com`)
require.NotNil(t, h)
rh, ok := h.(*host)
require.True(t, ok)
require.Equal(t, `www.example.com`, rh.address)
require.Equal(t, `www.example.com`, h.GetAddress())
}
15 changes: 15 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ type VarMatchOption interface {

var (
_CaseInsensitiveFixed = &caseInsensitiveFixed{}
_PathRegexCheck = &pathRegexChecker{}
)
var (
CaseInsensitiveFixed = _CaseInsensitiveFixed // is a FixedMatchOption that can be used with templates to allow case-insensitive fixed path parts
PathRegexCheck = _PathRegexCheck // is a VarMatchOption that can be used with Template.PathFrom or Template.RequestFrom to check that vars passed in match regexes for the path part
)

type fixedMatchOptions []FixedMatchOption
Expand Down Expand Up @@ -64,3 +66,16 @@ type caseInsensitiveFixed struct{}
func (o *caseInsensitiveFixed) Match(value string, expected string, pathPos int, vars PathVars) bool {
return value == expected || strings.EqualFold(value, expected)
}

type pathRegexChecker struct{}

func (o *pathRegexChecker) Applicable(value string, position int, name string, rx *regexp.Regexp, rxs string, pathPos int, vars PathVars) bool {
return rx != nil
}

func (o *pathRegexChecker) Match(value string, position int, name string, rx *regexp.Regexp, rxs string, pathPos int, vars PathVars) (string, bool) {
if rx != nil && !rx.MatchString(value) {
return value, false
}
return value, true
}
41 changes: 34 additions & 7 deletions path_part.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package urit

import (
"errors"
"fmt"
"regexp"
"strconv"
Expand Down Expand Up @@ -150,27 +151,53 @@ func (pt *pathPart) pathFrom(tracker *positionsTracker) (string, error) {

type positionsTracker struct {
vars PathVars
position int
varPosition int
pathPosition int
namedPositions map[string]int
varMatches varMatchOptions
}

func (tr *positionsTracker) getVar(pt *pathPart) (string, error) {
if tr.vars.VarsType() == Positions {
if str, ok := tr.vars.GetPositional(tr.position); ok {
tr.position++
useVars := tr.vars
if useVars == nil {
useVars = Positional()
}
var err error
if useVars.VarsType() == Positions {
if str, ok := useVars.GetPositional(tr.varPosition); ok {
tr.varPosition++
return str, nil
}
return "", fmt.Errorf("no var for position %d", tr.position+1)
return "", fmt.Errorf("no var for varPosition %d", tr.varPosition+1)
} else {
np := tr.namedPositions[pt.name]
if str, ok := tr.vars.GetNamed(pt.name, np); ok {
if str, ok := useVars.GetNamed(pt.name, np); ok {
str, err = tr.checkVar(str, pt, tr.varPosition, tr.pathPosition)
if err != nil {
return "", err
}
tr.namedPositions[pt.name] = np + 1
tr.varPosition++
return str, nil
} else if np == 0 {
return "", fmt.Errorf("no var for '%s'", pt.name)
}
return "", fmt.Errorf("no var for '%s' (position %d)", pt.name, np+1)
return "", fmt.Errorf("no var for '%s' (varPosition %d)", pt.name, np+1)
}
}

func (tr *positionsTracker) checkVar(s string, pt *pathPart, pos int, pathPos int) (result string, err error) {
result = s
for _, ck := range tr.varMatches {
if ck.Applicable(result, pos, pt.name, pt.regexp, pt.orgRegexp, pathPos, tr.vars) {
if altS, ok := ck.Match(result, pos, pt.name, pt.regexp, pt.orgRegexp, pathPos, tr.vars); ok {
result = altS
} else {
err = errors.New("no match path var")
}
}
}
return
}

func addRegexHeadAndTail(rx string) string {
Expand Down
Loading

0 comments on commit 0477d98

Please sign in to comment.