Skip to content
This repository has been archived by the owner on May 28, 2023. It is now read-only.

Commit

Permalink
Update to use goschtalt v0.17.0 syntax.
Browse files Browse the repository at this point in the history
  • Loading branch information
schmidtw committed May 2, 2023
1 parent 87f377e commit 4aff491
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 44 deletions.
33 changes: 18 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,39 @@

An optional set of [naming conventions](https://en.wikipedia.org/wiki/Naming_convention_(programming)) mapping options for goschtalt.

Fairly often we want to use structures for configuration that we can't easily alter.
Fortunately goschtalt provides an easy way to inject arbitrary mappers, however
that code is fairly boiler plate and clunky to repeat. Hence the value of this
Fairly often we want to use structures for configuration that can't easily be
altered. Fortunately goschtalt provides an easy way to inject arbitrary mappers,
however that code is fairly boiler plate and clunky to repeat. Hence the value of this
library. With the [janos/casbab](https://github.com/janos/casbab) and a bit of
glue code, it's now easy to automatically convert from config files that may be
`snake_case` into go structures that follow the general conventions.
glue code, it's now easy to automatically convert to/from config files that may
be `snake_case` into go structures that follow the general conventions.

To set configuration from go structs (AddValue()) or read configuration values
from a configuration that is `snake_case` just use the option in place of
`goschtalt.KeymapFn()`.
`goschtalt.KeymapMapper()`.

```go
casemapper.ConfigStoredAs("two_words")
casemapper.ConfigIs("two_words")
```

With more complex structures there will likely be exceptions needed. For example,
if the input data has the field `HTTP-Header` but the normal format expects that
value as `Http-Header` no problem; there is an optional `map[string]string` parameter
that maps the **configuration field** to the **structure field**.
With more complex structures there will likely be adjustments will be needed.
Adjustments are in the form of a map where keys are the golang struct field name
and values are the configuration key name.

For example:

```go
casemapper.ConfigStoredAs("Two_Words"),
goschtalt.Keymap(
casemapper.ConfigStoredAs("two_words",
map[string]string{
"Configuration-Field": "StructureField",
"HTTP-Header": "HTTPHeader",
"CfgField": "configuration_field",
"CNAMEs": "cnames",
},
)
```

will use the normal snake case mapping except for `CfgField` and `CNAMEs` fields,
which are mapped to `configuration_field` and `cnames` respectively.

References
----------
- https://en.wikipedia.org/wiki/Naming_convention_(programming)
Expand Down
68 changes: 59 additions & 9 deletions casemapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
package casemapper

import (
"errors"
"fmt"
"sort"
"strings"
Expand All @@ -34,6 +35,10 @@ const (
TrainCase = "Two-Words"
)

var (
ErrDuplicate = errors.New("duplicate adjustment")
)

var fmtToFunc = map[string]func(string) string{
"twowords": allLower,
"TWOWORDS": allUpper,
Expand All @@ -49,7 +54,20 @@ var fmtToFunc = map[string]func(string) string{
"Two-Words": casbab.CamelKebab,
}

// ConfigStoredAs provides a strict field/key mapper that converts the config
type casemapper struct {
toCase func(string) string
adjustments map[string]string
}

func (c casemapper) Map(in string) string {
out, found := c.adjustments[in]
if !found {
out = c.toCase(in)
}
return out
}

// ConfigIs provides a strict field/key mapper that converts the config
// values from the specified nomenclature into the go structure name.
//
// Since the names of the different formatting styles are not standardized, only
Expand All @@ -68,18 +86,33 @@ var fmtToFunc = map[string]func(string) string{
// - Two-Words
// - two-Words
//
// This option provides a goschtalt.KeymapFunc based option that will convert
// every input string, effectively ending the chain 100% of the time.
// Generally, this option should be specified prior to any goschtalt.Keymap
// options that handle customization.
func ConfigStoredAs(format string) goschtalt.Option {
// This option provides a goschtalt.KeymapMapper based option that will convert
// every input string, ending the chain 100% of the time.
//
// To make adjustments pass in a map (or many) with keys being the golang
// structure field names and values being the configuration name.
//
// The map keys and values must be unique and the inverse mapping must also be
// unique or an error is returned.
func ConfigIs(format string, structToConfig ...map[string]string) goschtalt.Option {
sToC, cToS, err := merge(structToConfig)
if err != nil {
return goschtalt.WithError(err)
}

if toCase, found := fmtToFunc[format]; found {
return goschtalt.Options(
goschtalt.DefaultUnmarshalOptions(
goschtalt.KeymapFunc(toCase),
goschtalt.KeymapMapper(&casemapper{
toCase: toCase,
adjustments: cToS,
}),
),
goschtalt.DefaultValueOptions(
goschtalt.KeymapFunc(toCase),
goschtalt.KeymapMapper(&casemapper{
toCase: toCase,
adjustments: sToC,
}),
),
)
}
Expand All @@ -92,14 +125,31 @@ func ConfigStoredAs(format string) goschtalt.Option {
sort.Strings(keys)

return goschtalt.WithError(
fmt.Errorf("%w, '%s' unknown format by casemapper.ConfigStoredAs(). Known formats: %s",
fmt.Errorf("%w, '%s' unknown format by casemapper.ConfigIs(). Known formats: %s",
goschtalt.ErrInvalidInput,
format,
strings.Join(keys, ", "),
),
)
}

func merge(in []map[string]string) (map[string]string, map[string]string, error) {
sToC := make(map[string]string, len(in))
cToS := make(map[string]string, len(in))
for i := range in {
for k, v := range in[i] {
_, a := sToC[k]
_, b := cToS[v]
if a || b {
return nil, nil, fmt.Errorf("%w, '%s' is duplicated.", ErrDuplicate, k)
}
sToC[k] = v
cToS[v] = k
}
}
return sToC, cToS, nil
}

func allLower(s string) string {
return strings.Join(strings.Split(casbab.Lower(s), " "), "")
}
Expand Down
106 changes: 106 additions & 0 deletions casemapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,112 @@ func TestMapping(t *testing.T) {
}
}

func TestInvalidMapping(t *testing.T) {
assert := assert.New(t)

gs, err := goschtalt.New(goschtalt.AutoCompile(),
ConfigStoredAs("two_words",
map[string]string{
"Foo": "foo",
},
map[string]string{
"Foo": "foo",
},
))
assert.Nil(gs)
assert.Error(err)
}

func TestMapMerging(t *testing.T) {
tests := []struct {
description string
in []map[string]string
sToC map[string]string
cToS map[string]string
expectErr error
}{
{
description: "single item",
in: []map[string]string{
map[string]string{
"a": "b",
},
},
sToC: map[string]string{
"a": "b",
},
cToS: map[string]string{
"b": "a",
},
}, {
description: "multiple in the array",
in: []map[string]string{
map[string]string{
"A": "a",
},
map[string]string{
"B": "b",
},
},
sToC: map[string]string{
"A": "a",
"B": "b",
},
cToS: map[string]string{
"a": "A",
"b": "B",
},
}, {
description: "invalid, duplicated config name",
in: []map[string]string{
map[string]string{
"A": "a",
},
map[string]string{
"B": "b",
},
map[string]string{
"C": "b",
},
},
expectErr: ErrDuplicate,
}, {
description: "invalid, duplicated field name",
in: []map[string]string{
map[string]string{
"A": "a",
},
map[string]string{
"B": "b",
},
map[string]string{
"B": "c",
},
},
expectErr: ErrDuplicate,
},
}

for _, tc := range tests {
t.Run(tc.description, func(t *testing.T) {
assert := assert.New(t)

a, b, e := merge(tc.in)

if tc.expectErr == nil {
assert.Equal(a, tc.sToC)
assert.Equal(b, tc.cToS)
assert.NoError(e)
return
}

assert.Nil(a)
assert.Nil(b)
assert.ErrorIs(e, tc.expectErr)
})
}
}

func TestUnknown(t *testing.T) {
expected := `Known formats: TWO-WORDS, TWOWORDS, TWO_WORDS, Two-Words, TwoWords, Two_Words, two-Words, two-words, twoWords, two_Words, two_words, twowords`

Expand Down
47 changes: 27 additions & 20 deletions example_configstoredas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,29 @@ import (

"github.com/goschtalt/casemapper"
"github.com/goschtalt/goschtalt"
"github.com/goschtalt/goschtalt/pkg/debug"
)

func ExampleConfigStoredAs() {
func ExampleConfigIs() {
c := debug.Collect{}
gs, err := goschtalt.New(
goschtalt.AutoCompile(),
goschtalt.DefaultUnmarshalOptions(goschtalt.KeymapReport(&c)),
goschtalt.DefaultValueOptions(goschtalt.KeymapReport(&c)),

casemapper.ConfigStoredAs("two_words"),
casemapper.ConfigStoredAs("two_words",
// Keys are the struct field names and values are the configuration
// names.
map[string]string{
"Header": "http_header",
"Sally": "frog",
},
),

// Normally you'll be including data from a file or something like that.
// Here we want to use the built in options to avoid including additional
// dependencies.
goschtalt.AddValue("incoming", "",
goschtalt.AddValue("incoming", goschtalt.Root,
&struct {
Name string `goschtalt:"FirstName"` // Rename via tags...
Header string
Expand All @@ -29,15 +40,6 @@ func ExampleConfigStoredAs() {
Header: "Content-Type: text/plain",
Sally: "Likes go",
},

// or rename via mapping, which is useful if you can't change
// the target structure, but want the configuration to be a
// specific key.
goschtalt.Keymap(
map[string]string{
"header": "HTTPHeader", // Convert Http back to HTTP
},
),
),
)
if err != nil {
Expand All @@ -50,22 +52,27 @@ func ExampleConfigStoredAs() {
Frog string
}

cfg, err := goschtalt.Unmarshal[Config](gs, "",
// You can also remap when you unmarshal if that's simpler
goschtalt.Keymap(
map[string]string{
"frog": "sally",
},
),
)
cfg, err := goschtalt.Unmarshal[Config](gs, goschtalt.Root)
if err != nil {
panic(err)
}

fmt.Printf("Mappings:\n")
fmt.Print(c.String())

fmt.Printf("\nConfig:\n")
fmt.Printf("Config.HTTPHeader: '%s'\n", cfg.HTTPHeader)
fmt.Printf("Config.FirstName: '%s'\n", cfg.FirstName)
fmt.Printf("Config.Frog: '%s'\n", cfg.Frog)
// Output:
// Mappings:
// 'FirstName' --> 'first_name'
// 'Frog' --> 'frog'
// 'HTTPHeader' --> 'http_header'
// 'Header' --> 'http_header'
// 'Sally' --> 'frog'
//
// Config:
// Config.HTTPHeader: 'Content-Type: text/plain'
// Config.FirstName: 'Gopher'
// Config.Frog: 'Likes go'
Expand Down

0 comments on commit 4aff491

Please sign in to comment.