diff --git a/models/meta.go b/models/meta.go index 2b75fa9..7f8d08d 100644 --- a/models/meta.go +++ b/models/meta.go @@ -3,9 +3,11 @@ package models import ( "bytes" "encoding/json" + "errors" "fmt" "regexp" "strconv" + "strings" "time" ) @@ -16,7 +18,7 @@ type Meta struct { func (m Meta) MarshalText() ([]byte, error) { if regexp.MustCompile(`\s`).MatchString(m.Key) { - return []byte{}, fmt.Errorf("whitespace is now allowed in key: %s", m.Key) + return []byte{}, fmt.Errorf("whitespace is not allowed in key: %s", m.Key) } buff := &bytes.Buffer{} buff.WriteRune('@') @@ -51,3 +53,51 @@ func (m Meta) MarshalText() ([]byte, error) { return buff.Bytes(), nil } + +func (m *Meta) UnmarshalText(in []byte) error { + if len(in) == 0 { + return errors.New("Unable to Unmarshal empty string") + } + re := regexp.MustCompile("(?s)^@([^ ]+) (.*)( @end)?$") + match := re.FindSubmatch(in) + if len(match) == 0 { + return fmt.Errorf("Failed to match %s", in) + } + m.Key = string(match[1]) + return m.processMeta(match[2]) +} + +func (m *Meta) processMeta(in []byte) error { + if len(in) == 0 { + return errors.New("No value found") + } + s := strings.TrimSpace(string(in)) + if len(s) == 0 { + return errors.New("No value found") + } + yesno := regexp.MustCompile("^(y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)$") + yes := regexp.MustCompile("^(y|Y|yes|Yes|YES|true|True|TRUE|on|On|ON)$") + null := regexp.MustCompile("^(~|null|Null|NULL|none|None|NONE|nil|Nil|NIL)$") + var j json.RawMessage + if null.MatchString(s) { + m.Value = nil + } else if yesno.MatchString(s) { + if yes.MatchString(s) { + m.Value = true + } else { + m.Value = false + } + } else if i, err := strconv.Atoi(s); err == nil { + m.Value = i + } else if f, err := strconv.ParseFloat(s, 64); err == nil { + m.Value = f + } else if t, err := time.Parse(time.RFC3339, s); err == nil { + m.Value = t + } else if err := json.Unmarshal(in, &j); err == nil { + m.Value = j + } else { + m.Value = s + } + + return nil +} diff --git a/models/meta_test.go b/models/meta_test.go index e9147ed..1fa19ea 100644 --- a/models/meta_test.go +++ b/models/meta_test.go @@ -12,43 +12,87 @@ import ( // Type assertions var _ encoding.TextMarshaler = Meta{} +var _ encoding.TextUnmarshaler = new(Meta) + +var skipMarshalTest = errors.New("skip marshal") func TestMeta(t *testing.T) { when := time.Now() tests := []struct { - name string - key string - value any - out string - err error + name string + key string + value any + out string + err error + newVal any }{ - {"int", "num", 42, "@num 42", nil}, - {"float", "num", 42.13, "@num 42.13", nil}, - {"string", "word", "hello", "@word hello", nil}, - {"json number", "num", json.Number("42.13"), "@num 42.13", nil}, - {"true", "b", true, "@b true", nil}, - {"false", "b", false, "@b false", nil}, - {"nil", "n", nil, "", nil}, - {"time", "when", when, "@when " + when.Format(time.RFC3339), nil}, - {"rune", "char", '@', "@char @", nil}, - {"bytes", "byteme", []byte("yo"), "@byteme yo", nil}, - {"byte", "byteme", byte(67), "@byteme C", nil}, - {"json-obj", "obj", json.RawMessage(`{"foo":"bar","baz":"quux"}`), `@obj {"foo":"bar","baz":"quux"}`, nil}, - {"json-arr", "arr", json.RawMessage(`["foo",42,"bar", null,"quux", true]`), `@arr ["foo",42,"bar", null,"quux", true]`, nil}, - {"chan", "nope", make(chan bool), "", errors.New("Unknown type chan bool")}, - {"whitespace", "no space", "hi", "", errors.New("whitespace is now allowed in key: no space")}, + {"int", "num", 42, "@num 42", nil, 42}, + {"float", "num", 42.13, "@num 42.13", nil, 42.13}, + {"string", "word", "hello", "@word hello", nil, "hello"}, + {"json number", "num", json.Number("42.13"), "@num 42.13", nil, 42.13}, + {"true", "b", true, "@b true", nil, true}, + {"false", "b", false, "@b false", nil, false}, + {"nil", "n", nil, "", nil, errors.New("Unable to Unmarshal empty string")}, + {"time", "when", when, "@when " + when.Format(time.RFC3339), nil, when}, + {"rune", "char", '@', "@char @", nil, "@"}, + {"bytes", "byteme", []byte("yo"), "@byteme yo", nil, "yo"}, + {"byte", "byteme", byte(67), "@byteme C", nil, "C"}, + {"json-obj", "obj", json.RawMessage(`{"foo":"bar","baz":"quux"}`), `@obj {"foo":"bar","baz":"quux"}`, nil, json.RawMessage(`{"foo":"bar","baz":"quux"}`)}, + {"json-arr", "arr", json.RawMessage(`["foo",42,"bar", null,"quux", true]`), `@arr ["foo",42,"bar", null,"quux", true]`, nil, json.RawMessage(`["foo",42,"bar", null,"quux", true]`)}, + {"chan", "nope", make(chan bool), "", errors.New("Unknown type chan bool"), ""}, + {"whitespace-key", "no space", "hi", "", errors.New("whitespace is not allowed in key: no space"), ""}, + {"empty-mar", "nope", skipMarshalTest, "", nil, errors.New("Unable to Unmarshal empty string")}, + {"no-key-mar", "nope", skipMarshalTest, "nope", nil, errors.New("Failed to match nope")}, + {"no-value-mar", "nope", skipMarshalTest, "@nope ", nil, errors.New("No value found")}, + {"space-value-mar", "nope", skipMarshalTest, "@nope ", nil, errors.New("No value found")}, + {"space-value-mar", "nope", skipMarshalTest, "@nope \n ", nil, errors.New("No value found")}, + {"null-value-mar", "nope", skipMarshalTest, "@nope null", nil, nil}, + {"tilda-value-mar", "nope", skipMarshalTest, "@nope ~", nil, nil}, + {"none-value-mar", "nope", skipMarshalTest, "@nope none", nil, nil}, + {"nil-value-mar", "nope", skipMarshalTest, "@nope nil", nil, nil}, + {"yes-value-mar", "nope", skipMarshalTest, "@nope yes", nil, true}, + {"on-value-mar", "nope", skipMarshalTest, "@nope on", nil, true}, + {"no-value-mar", "nope", skipMarshalTest, "@nope no", nil, false}, + {"off-value-mar", "nope", skipMarshalTest, "@nope off", nil, false}, } for _, tt := range tests { - t.Run(tt.name, getMetaTestRunner(tt.key, tt.value, tt.out, tt.err)) + t.Run(tt.name, getMetaTestRunner(tt.key, tt.value, tt.out, tt.err, tt.newVal)) } } -func getMetaTestRunner(key string, value any, out string, err error) func(*testing.T) { +func getMetaTestRunner(key string, value any, out string, err error, newVal any) func(*testing.T) { return func(t *testing.T) { st := Meta{key, value} - o, e := st.MarshalText() - assert.Equal(t, out, string(o)) - assert.Equal(t, err, e) + n := &Meta{} + var e error + + if valE, ok := value.(error); !ok || !errors.Is(valE, skipMarshalTest) { + var o []byte + o, e = st.MarshalText() + assert.Equal(t, out, string(o)) + assert.Equal(t, err, e) + if e != nil { + return + } + + e = n.UnmarshalText(o) + } else { + e = n.UnmarshalText([]byte(out)) + } + if newE, ok := newVal.(error); ok { + assert.Equal(t, newE, e) + } else { + assert.Equal(t, key, n.Key) + if ti, ok := newVal.(time.Time); ok { + valT, ok := n.Value.(time.Time) + if assert.True(t, ok) { + assert.WithinRange(t, valT, ti.Add(-time.Second), ti.Add(time.Second)) + } + + } else { + assert.Equal(t, newVal, n.Value) + } + } } }