Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

URL Rewriting #41

Merged
merged 15 commits into from
Dec 26, 2024
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ uncors
!uncors/
tools/fakedata/scheme.json
tools/fakedata/docs.md
.vscode
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
- [HTTP/HTTPS proxy support](https://github.com/evg4b/uncors/wiki/2.-Configuration#proxy-configuration)
- [Static file serving](https://github.com/evg4b/uncors/wiki/4.-Static-file-serving)
- [Response caching](https://github.com/evg4b/uncors/wiki/5.-Response-caching)
- [Request rewriting](https://github.com/evg4b/uncors/wiki/6.-Request-rewriting)

Other new features you can find in [roadmap](https://github.com/evg4b/uncors/blob/main/ROADMAP.md).

Expand All @@ -79,7 +80,7 @@ scoop install evg4b/uncors
#### [NPM](https://npmjs.com) (Cross-platform)

```bash
# Run as a independent CLI tool
# Run as a independent CLI tool
npx -y uncors ...
# Or add as dependency in your package
npm install uncors --save-dev
Expand Down
22 changes: 12 additions & 10 deletions internal/config/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,22 @@ import (
)

type Mapping struct {
From string `mapstructure:"from"`
To string `mapstructure:"to"`
Statics StaticDirectories `mapstructure:"statics"`
Mocks Mocks `mapstructure:"mocks"`
Cache CacheGlobs `mapstructure:"cache"`
From string `mapstructure:"from"`
To string `mapstructure:"to"`
Statics StaticDirectories `mapstructure:"statics"`
Mocks Mocks `mapstructure:"mocks"`
Cache CacheGlobs `mapstructure:"cache"`
Rewrites RewriteOptions `mapstructure:"rewrites"`
}

func (m *Mapping) Clone() Mapping {
return Mapping{
From: m.From,
To: m.To,
Statics: m.Statics.Clone(),
Mocks: m.Mocks.Clone(),
Cache: m.Cache.Clone(),
From: m.From,
To: m.To,
Statics: m.Statics.Clone(),
Mocks: m.Mocks.Clone(),
Cache: m.Cache.Clone(),
Rewrites: m.Rewrites.Clone(),
}
}

Expand Down
30 changes: 30 additions & 0 deletions internal/config/rewrite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package config

type RewritingOption struct {
From string `mapstructure:"from"`
To string `mapstructure:"to"`
Host string `mapstructure:"host"`
}

func (r RewritingOption) Clone() RewritingOption {
return RewritingOption{
From: r.From,
To: r.To,
Host: r.Host,
}
}

type RewriteOptions []RewritingOption

func (r RewriteOptions) Clone() RewriteOptions {
if r == nil {
return nil
}

clone := make(RewriteOptions, len(r))
for i, rewrite := range r {
clone[i] = rewrite.Clone()
}

return clone
}
82 changes: 82 additions & 0 deletions internal/config/rewrite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package config_test

import (
"testing"

"github.com/evg4b/uncors/internal/config"
"github.com/stretchr/testify/assert"
)

func TestRewritingOptionClone(t *testing.T) {
tests := []struct {
name string
expected config.RewritingOption
}{
{
name: "empty structure",
expected: config.RewritingOption{},
},
{
name: "structure with 1 field",
expected: config.RewritingOption{
From: "from",
},
},
{
name: "structure with 2 fields",
expected: config.RewritingOption{
From: "from",
To: "to",
},
},
{
name: "structure with all fields",
expected: config.RewritingOption{
From: "from",
To: "to",
Host: "host",
},
},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
actual := testCase.expected.Clone()

assert.NotSame(t, &testCase.expected, &actual)
assert.Equal(t, testCase.expected, actual)
})
}
}

func TestRewriteOptionsClone(t *testing.T) {
tests := []struct {
name string
expected config.RewriteOptions
}{
{
name: "empty slice",
expected: config.RewriteOptions{},
},
{
name: "slice with one element",
expected: config.RewriteOptions{
{From: "from1", To: "to1", Host: "host1"},
},
},
{
name: "slice with multiple elements",
expected: config.RewriteOptions{
{From: "from1", To: "to1", Host: "host1"},
{From: "from2", To: "to2", Host: "host2"},
},
},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
actual := testCase.expected.Clone()

assert.NotSame(t, &testCase.expected, &actual)
assert.Equal(t, testCase.expected, actual)
})
}
}
11 changes: 6 additions & 5 deletions internal/config/validators/base/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import (
)

type PathValidator struct {
Field string
Value string
Field string
Value string
Relative bool
}

func (p *PathValidator) IsValid(errors *validate.Errors) {
Expand All @@ -20,13 +21,13 @@ func (p *PathValidator) IsValid(errors *validate.Errors) {
return
}

if !strings.HasPrefix(p.Value, "/") {
errors.Add(p.Field, fmt.Sprintf("%s must start with /", p.Field))
if !p.Relative && !strings.HasPrefix(p.Value, "/") {
errors.Add(p.Field, fmt.Sprintf("%s must be absolute and start with /", p.Field))

return
}

uri, err := urlx.Parse("localhost" + p.Value)
uri, err := urlx.Parse("//localhost/" + strings.TrimPrefix(p.Value, "/"))
if err != nil {
errors.Add(p.Field, fmt.Sprintf("%s is not valid path", p.Field))
}
Expand Down
117 changes: 62 additions & 55 deletions internal/config/validators/base/path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,66 +13,73 @@ import (
func TestPathValidator(t *testing.T) {
const field = "field"

t.Run("should not register errors for", func(t *testing.T) {
tests := []struct {
name string
value string
}{
{
name: "valid path",
value: "/",
},
}
t.Run("absolute path", func(t *testing.T) {
t.Run("should not register errors for", func(t *testing.T) {
tests := []struct {
name string
value string
}{
{
name: "root path",
value: "/",
},
{
name: "valid path",
value: "/api/info",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
errors := validate.Validate(&base.PathValidator{
Field: field,
Value: test.value,
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
errors := validate.Validate(&base.PathValidator{
Field: field,
Value: test.value,
})

assert.False(t, errors.HasAny())
})
}
})

assert.False(t, errors.HasAny())
})
}
})
t.Run("should register errors for invalid hosts", func(t *testing.T) {
t.Skip("This test is not implemented yet.")
tests := []struct {
name string
value string
error string
}{
{
name: "empty path",
value: "",
error: "field must not be empty",
},
{
name: "path without /",
value: "api/info",
error: "field must be absolute and start with /",
},
{
name: "path with query",
value: "/api/info?demo=1",
error: "field must not contain query",
},
{
name: "path with host",
value: "demo.com/api/info?demo=1",
error: "field must be absolute and start with /",
},
}

t.Run("should register errors for invalid hosts", func(t *testing.T) {
tests := []struct {
name string
value string
error string
}{
{
name: "empty path",
value: "",
error: "field must not be empty",
},
{
name: "path without /",
value: "api/info",
error: "field must start with /",
},
{
name: "path with query",
value: "/api/info?demo=1",
error: "field must not contain query",
},
{
name: "path with host",
value: "demo.com/api/info?demo=1",
error: "field must start with /",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
errors := validate.Validate(&base.PathValidator{
Field: field,
Value: test.value,
})

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
errors := validate.Validate(&base.PathValidator{
Field: field,
Value: test.value,
require.EqualError(t, errors, test.error)
})

require.EqualError(t, errors, test.error)
})
}
}
})
})
}
7 changes: 7 additions & 0 deletions internal/config/validators/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,11 @@ func (m *MappingValidator) IsValid(errors *validate.Errors) {
Value: cache,
}))
}

for i, rewrite := range m.Value.Rewrites {
errors.Append(validate.Validate(&RewritingOptionValidator{
Field: joinPath(m.Field, "rewrite", index(i)),
Value: rewrite,
}))
}
}
36 changes: 36 additions & 0 deletions internal/config/validators/rewrite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package validators

import (
"github.com/evg4b/uncors/internal/config"
"github.com/evg4b/uncors/internal/config/validators/base"
"github.com/gobuffalo/validate"
)

type RewritingOptionValidator struct {
Field string
Value config.RewritingOption
}

func (m *RewritingOptionValidator) IsValid(errors *validate.Errors) {
errors.Append(validate.Validate(
&base.PathValidator{
Field: joinPath(m.Field, "from"),
Value: m.Value.From,
Relative: true,
},
&base.PathValidator{
Field: joinPath(m.Field, "to"),
Value: m.Value.To,
Relative: true,
},
))

if len(m.Value.Host) > 0 {
errors.Append(validate.Validate(
&base.HostValidator{
Field: joinPath(m.Field, "host"),
Value: m.Value.Host,
},
))
}
}
Loading
Loading