Skip to content

Instantly share code, notes, and snippets.

@DeadlySurgeon
Last active July 1, 2024 18:59
Show Gist options
  • Save DeadlySurgeon/7e062a5bd027a9d58a071ce213ff5828 to your computer and use it in GitHub Desktop.
Save DeadlySurgeon/7e062a5bd027a9d58a071ce213ff5828 to your computer and use it in GitHub Desktop.
Regex based unmarshaler
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())
}
}
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=bob@bob.com`,
Regex: userExample,
Expect: example{
Username: "bob",
Age: 21,
Email: "bob@bob.com",
},
},
"basic 2": {
Input: `username=sally age=22 email=sally@example.com`,
Regex: userExample,
Expect: example{
Username: "sally",
Age: 22,
Email: "sally@example.com",
},
},
} {
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)
})
}
}
@DeadlySurgeon
Copy link
Author

Some improvements that can be made:

  • Support more types
    • Support UnmarshalText interface
  • Default to field name matching (similiar to how json works)
  • Tag Index might be unsafe. Since we're only adding to it, chances are it's fine but still, should add some safety

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment