Last active
July 1, 2024 18:59
-
-
Save DeadlySurgeon/7e062a5bd027a9d58a071ce213ff5828 to your computer and use it in GitHub Desktop.
Regex based unmarshaler
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"encoding" | |
"fmt" | |
"reflect" | |
"regexp" | |
"strconv" | |
) | |
var tagIndex = map[reflect.Type]map[string]int{} | |
func unmarshal[T any](r *regexp.Regexp, text string) (T, error) { | |
t := reflect.TypeOf(*new(T)) | |
v := reflect.New(t) | |
if _, ok := tagIndex[t]; !ok { | |
tagIndex[t] = map[string]int{} | |
} | |
matches := r.FindStringSubmatch(text) | |
for i, name := range r.SubexpNames() { | |
index, ok := tagIndex[t][name] | |
if !ok { | |
index = -1 | |
for i := 0; i < t.NumField(); i++ { | |
if t.Field(i).Tag.Get("match") == name { | |
tagIndex[t][name] = i | |
index = i | |
break | |
} | |
} | |
if index == -1 { | |
continue | |
} | |
} | |
f := v.Elem().Field(tagIndex[t][name]) | |
v, err := caster(f.Type(), matches[i]) | |
if err != nil { | |
return *new(T), err | |
} | |
f.Set(v) | |
} | |
return v.Elem().Interface().(T), nil | |
} | |
func caster(t reflect.Type, s string) (reflect.Value, error) { | |
switch t.Kind() { | |
case reflect.String: | |
return reflect.ValueOf(s), nil | |
case reflect.Int: | |
i, err := strconv.Atoi(s) | |
if err != nil { | |
return reflect.Value{}, err | |
} | |
return reflect.ValueOf(i), nil | |
case reflect.Struct: | |
v := reflect.New(t) | |
decoder, ok := v.Interface().(encoding.TextUnmarshaler) | |
if !ok { | |
return reflect.Value{}, fmt.Errorf("unable to decode %v", t) | |
} | |
if err := decoder.UnmarshalText([]byte(s)); err != nil { | |
return reflect.Value{}, err | |
} | |
return v.Elem(), nil | |
default: | |
return reflect.Value{}, fmt.Errorf("unsupported type: %v", t.Kind()) | |
} | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"regexp" | |
"testing" | |
"time" | |
"github.com/stretchr/testify/assert" | |
) | |
func TestUnmarshal(t *testing.T) { | |
var ( | |
userExample = regexp.MustCompile(`username=(?P<username>.*)\sage=(?P<age>\d*)\semail=(?P<email>.*)`) | |
) | |
type example struct { | |
Username string `match:"username"` | |
Age int `match:"age"` | |
Email string `match:"email"` | |
} | |
for name, test := range map[string]struct { | |
Input string | |
Regex *regexp.Regexp | |
Expect example | |
}{ | |
"basic 1": { | |
Input: `username=bob age=21 [email protected]`, | |
Regex: userExample, | |
Expect: example{ | |
Username: "bob", | |
Age: 21, | |
Email: "[email protected]", | |
}, | |
}, | |
"basic 2": { | |
Input: `username=sally age=22 [email protected]`, | |
Regex: userExample, | |
Expect: example{ | |
Username: "sally", | |
Age: 22, | |
Email: "[email protected]", | |
}, | |
}, | |
} { | |
t.Run(name, func(t *testing.T) { | |
e, err := unmarshal[example](userExample, test.Input) | |
if err != nil { | |
t.Fatal(err) | |
} | |
assert.Equal(t, test.Expect.Username, e.Username) | |
assert.Equal(t, test.Expect.Age, e.Age) | |
assert.Equal(t, test.Expect.Email, e.Email) | |
}) | |
} | |
} | |
func TestComplexUnmarshal(t *testing.T) { | |
var ( | |
timeExample = regexp.MustCompile(`time=(?P<time>.*)`) | |
) | |
type example struct { | |
Time time.Time `match:"time"` | |
} | |
for name, test := range map[string]struct { | |
Input string | |
Regex *regexp.Regexp | |
Expect example | |
}{ | |
"basic 1": { | |
Input: `time=2024-04-01T00:00:00Z`, | |
Regex: timeExample, | |
Expect: example{ | |
Time: time.Date(2024, time.April, 1, 0, 0, 0, 0, time.UTC), | |
}, | |
}, | |
} { | |
t.Run(name, func(t *testing.T) { | |
e, err := unmarshal[example](timeExample, test.Input) | |
if err != nil { | |
t.Fatal(err) | |
} | |
assert.Equal(t, test.Expect.Time, e.Time) | |
}) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Some improvements that can be made:
UnmarshalText
interface