Skip to content

Commit

Permalink
Static file serving (#15)
Browse files Browse the repository at this point in the history
* Created configuration structure

* Covered decoding by tests

* Fixed configuration decoding issues

* Created static directory mapping

* Fixed cloning issue

* Added tests for static serving

* Added static dirs pringing

* Changed handler structure

* Added static middleware draft

* Fixed index file serving

* Added base tests for static middleware

* Fixed mapping tests

* Fixed middelware tesrs

* Added tests fro normalise helper

* Added tests for uncors handler

* Added tests for index file

* Added tests for mocks

* Enabled skipped tests

* Added fir for prefix cutting

* Fixed error handling

* Added tests for mocks handler registration

* Fixed sonar issues

* Updated readme
  • Loading branch information
Evgeny Abramovich committed May 13, 2023
1 parent 201b14c commit 4b7a562
Show file tree
Hide file tree
Showing 43 changed files with 1,860 additions and 668 deletions.
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ linters:
- nolintlint
- tagliatelle
- maintidx
- ireturn
linters-settings:
varnamelen:
ignore-names:
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
- Wildcard URL request mapping
- Simple request/response mocking
- HTTP/HTTPS proxy support
- *Static file serving ([coming soon...](./ROADMAP.md))*
- Static file serving
- *Response caching ([coming soon...](./ROADMAP.md))*

Other new features you can find in [UNCORS roadmap](https://github.com/evg4b/uncors/blob/main/ROADMAP.md)
Expand Down Expand Up @@ -155,7 +155,12 @@ Uncors supports a YAML file configuration with the following options:
# Base configuration
http-port: 8080 # Local HTTP listened port.
mappings:
http://localhost:3000: https://githib.com
- http://localhost: https://githib.com
- from: http://other.domain.com
to: https//example.com
statics:
/path: ./public
/another-path: ~/another-static-dir
debug: false # Show debug output.
proxy: localhost:8080

Expand Down
7 changes: 4 additions & 3 deletions internal/configuration/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,19 @@ func LoadConfiguration(viperInstance *viper.Viper, args []string) (*UncorsConfig
Mocks: []Mock{},
}

configPath := viperInstance.GetString("config")
if len(configPath) > 0 {
if configPath := viperInstance.GetString("config"); len(configPath) > 0 {
viperInstance.SetConfigFile(configPath)
if err := viperInstance.ReadInConfig(); err != nil {
return nil, fmt.Errorf("filed to read config file '%s': %w", configPath, err)
}
}

configOption := viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
hooks.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
hooks.StringToTimeDurationHookFunc(),
URLMappingHookFunc(),
))

if err := viperInstance.Unmarshal(configuration, configOption); err != nil {
return nil, fmt.Errorf("filed parsing configuration: %w", err)
}
Expand Down
23 changes: 23 additions & 0 deletions internal/configuration/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"errors"
"strings"

"github.com/mitchellh/mapstructure"

"github.com/samber/lo"

"github.com/evg4b/uncors/internal/log"
Expand Down Expand Up @@ -46,3 +48,24 @@ func readURLMapping(config *viper.Viper, configuration *UncorsConfig) error {

return nil
}

func decodeConfig[T any](data any, mapping *T, decodeFuncs ...mapstructure.DecodeHookFunc) error {
hook := mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToSliceHookFunc(","),
mapstructure.ComposeDecodeHookFunc(decodeFuncs...),
)
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Result: mapping,
DecodeHook: hook,
ErrorUnused: true,
IgnoreUntaggedFields: true,
})

if err != nil {
return err //nolint:wrapcheck
}

err = decoder.Decode(data)

return err //nolint:wrapcheck
}
2 changes: 1 addition & 1 deletion internal/configuration/hooks/time_decode_hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/mitchellh/mapstructure"
)

func StringToTimeDurationHookFunc() mapstructure.DecodeHookFunc { //nolint: ireturn
func StringToTimeDurationHookFunc() mapstructure.DecodeHookFunc {
return func(f reflect.Type, t reflect.Type, data any) (any, error) {
if f.Kind() != reflect.String {
return data, nil
Expand Down
61 changes: 61 additions & 0 deletions internal/configuration/static_mapping.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package configuration

import (
"reflect"

"github.com/mitchellh/mapstructure"
)

type StaticDirMappings = []StaticDirMapping

type StaticDirMapping struct {
Path string `mapstructure:"path"`
Dir string `mapstructure:"dir"`
Index string `mapstructure:"index"`
}

func (s StaticDirMapping) Clone() StaticDirMapping {
return StaticDirMapping{
Path: s.Path,
Dir: s.Dir,
Index: s.Index,
}
}

var staticDirMappingsType = reflect.TypeOf(StaticDirMappings{})

func StaticDirMappingHookFunc() mapstructure.DecodeHookFunc { //nolint: ireturn
return func(f reflect.Type, t reflect.Type, rawData any) (any, error) {
if t != staticDirMappingsType || f.Kind() != reflect.Map {
return rawData, nil
}

mappingsDefs, ok := rawData.(map[string]any)
if !ok {
return rawData, nil
}

var mappings StaticDirMappings
for path, mappingDef := range mappingsDefs {
if def, ok := mappingDef.(string); ok {
mappings = append(mappings, StaticDirMapping{
Path: path,
Dir: def,
})

continue
}

mapping := StaticDirMapping{}
err := decodeConfig(mappingDef, &mapping)
if err != nil {
return nil, err
}

mapping.Path = path
mappings = append(mappings, mapping)
}

return mappings, nil
}
}
140 changes: 140 additions & 0 deletions internal/configuration/static_mapping_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package configuration_test

import (
"testing"

"github.com/evg4b/uncors/internal/configuration"
"github.com/evg4b/uncors/testing/testutils"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
)

const (
anotherStaticDir = "/another-static-dir"
anotherPath = "/another-path"
path = "/path"
staticDir = "/static-dir"
)

func TestStaticDirMappingHookFunc(t *testing.T) {
const configFile = "config.yaml"
type testType struct {
Statics configuration.StaticDirMappings `mapstructure:"statics"`
}

tests := []struct {
name string
config string
expected configuration.StaticDirMappings
}{
{
name: "decode plan mapping",
config: `
statics:
/path: /static-dir
/another-path: /another-static-dir
`,
expected: configuration.StaticDirMappings{
{Path: anotherPath, Dir: anotherStaticDir},
{Path: path, Dir: staticDir},
},
},
{
name: "decode object mappings",
config: `
statics:
/path: { dir: /static-dir }
/another-path: { dir: /another-static-dir }
`,
expected: configuration.StaticDirMappings{
{Path: path, Dir: staticDir},
{Path: anotherPath, Dir: anotherStaticDir},
},
},
{
name: "decode object mappings with index",
config: `
statics:
/path: { dir: /static-dir, index: index.html }
/another-path: { dir: /another-static-dir, index: default.html }
`,
expected: configuration.StaticDirMappings{
{Path: path, Dir: staticDir, Index: "index.html"},
{Path: anotherPath, Dir: anotherStaticDir, Index: "default.html"},
},
},
{
name: "decode mixed mappings with index",
config: `
statics:
/path: { dir: /static-dir, index: index.html }
/another-path: /another-static-dir
`,
expected: configuration.StaticDirMappings{
{Path: path, Dir: staticDir, Index: "index.html"},
{Path: anotherPath, Dir: anotherStaticDir},
},
},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
viperInstance := viper.GetViper()
viperInstance.SetFs(testutils.FsFromMap(t, map[string]string{
configFile: testCase.config,
}))
viperInstance.SetConfigFile(configFile)
err := viperInstance.ReadInConfig()
testutils.CheckNoError(t, err)

actual := testType{}

err = viperInstance.Unmarshal(&actual, viper.DecodeHook(
configuration.StaticDirMappingHookFunc(),
))
testutils.CheckNoError(t, err)

assert.ElementsMatch(t, actual.Statics, testCase.expected)
})
}
}

func TestStaticDirMappingClone(t *testing.T) {
tests := []struct {
name string
expected configuration.StaticDirMapping
}{
{
name: "empty structure",
expected: configuration.StaticDirMapping{},
},
{
name: "structure with 1 field",
expected: configuration.StaticDirMapping{
Dir: "dir",
},
},
{
name: "structure with 2 field",
expected: configuration.StaticDirMapping{
Dir: "dir",
Path: "/some-path",
},
},
{
name: "structure with all field",
expected: configuration.StaticDirMapping{
Dir: "dir",
Path: "/one-more-path",
Index: "index.html",
},
},
}
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)
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,28 @@ import (
)

type URLMapping struct {
From string `mapstructure:"from"`
To string `mapstructure:"to"`
From string `mapstructure:"from"`
To string `mapstructure:"to"`
Statics StaticDirMappings `mapstructure:"statics"`
}

func (u URLMapping) Clone() URLMapping {
return URLMapping{
From: u.From,
To: u.To,
Statics: lo.If(u.Statics == nil, StaticDirMappings(nil)).
ElseF(func() StaticDirMappings {
return lo.Map(u.Statics, func(item StaticDirMapping, index int) StaticDirMapping {
return item.Clone()
})
}),
}
}

var urlMappingType = reflect.TypeOf(URLMapping{})
var urlMappingFields = getTagValues(urlMappingType, "mapstructure")

func URLMappingHookFunc() mapstructure.DecodeHookFunc { //nolint: ireturn
func URLMappingHookFunc() mapstructure.DecodeHookFunc {
return func(f reflect.Type, t reflect.Type, rawData any) (any, error) {
if t != urlMappingType || f.Kind() != reflect.Map {
return rawData, nil
Expand All @@ -31,9 +45,9 @@ func URLMappingHookFunc() mapstructure.DecodeHookFunc { //nolint: ireturn
}

mapping := URLMapping{}
err := mapstructure.Decode(data, &mapping)
err := decodeConfig(data, &mapping, StaticDirMappingHookFunc())

return mapping, err //nolint:wrapcheck
return mapping, err
}

return rawData, nil
Expand Down
Loading

0 comments on commit 4b7a562

Please sign in to comment.