From ad14083d2207452da61d26cb6ad6b8945bd7e0fd Mon Sep 17 00:00:00 2001 From: aacebo Date: Mon, 7 Oct 2024 23:03:27 -0400 Subject: [PATCH] ensure serialized schemas have ordered keys --- any.go | 27 +++++++++++++++++++++------ any_test.go | 4 ++-- array_test.go | 4 ++-- float_test.go | 4 ++-- int_test.go | 4 ++-- object_test.go | 4 ++-- ordered_map/map.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ string_test.go | 4 ++-- 8 files changed, 77 insertions(+), 18 deletions(-) create mode 100644 ordered_map/map.go diff --git a/any.go b/any.go index 182b923..7f41b88 100644 --- a/any.go +++ b/any.go @@ -5,6 +5,9 @@ import ( "errors" "fmt" "reflect" + "slices" + + "github.com/aacebo/owl/ordered_map" ) type AnySchema struct { @@ -29,12 +32,24 @@ func (self AnySchema) Type() string { } func (self *AnySchema) Rule(key string, value any, rule RuleFn) *AnySchema { - self.rules = append(self.rules, Rule{ - Key: key, - Value: value, - Resolve: rule, + i := slices.IndexFunc(self.rules, func(rule Rule) bool { + return rule.Key == key }) + if i > -1 { + self.rules[i] = Rule{ + Key: key, + Value: value, + Resolve: rule, + } + } else { + self.rules = append(self.rules, Rule{ + Key: key, + Value: value, + Resolve: rule, + }) + } + return self } @@ -70,10 +85,10 @@ func (self *AnySchema) Enum(values ...any) *AnySchema { } func (self AnySchema) MarshalJSON() ([]byte, error) { - data := map[string]any{} + data := ordered_map.Map[string, any]{} for _, rule := range self.rules { - data[rule.Key] = rule.Value + data.Set(rule.Key, rule.Value) } return json.Marshal(data) diff --git a/any_test.go b/any_test.go index 85fc197..524ced2 100644 --- a/any_test.go +++ b/any_test.go @@ -71,10 +71,10 @@ func Test_Any(t *testing.T) { t.Error(err) } - if string(b) != `{"enum":[1,true,"hi"],"required":true,"type":"any"}` { + if string(b) != `{"type":"any","enum":[1,true,"hi"],"required":true}` { t.Errorf( "expected `%s`, received `%s`", - `{"enum":[1,true,"hi"],"required":true,"type":"any"}`, + `{"type":"any","enum":[1,true,"hi"],"required":true}`, string(b), ) } diff --git a/array_test.go b/array_test.go index 94f80ee..e29e53e 100644 --- a/array_test.go +++ b/array_test.go @@ -155,10 +155,10 @@ func Test_Array(t *testing.T) { t.Error(err) } - if string(b) != `{"items":{"type":"string"},"required":true,"type":"array[string]"}` { + if string(b) != `{"type":"array[string]","items":{"type":"string"},"required":true}` { t.Errorf( "expected `%s`, received `%s`", - `{"items":{"type":"string"},"required":true,"type":"array[string]"}`, + `{"type":"array[string]","items":{"type":"string"},"required":true}`, string(b), ) } diff --git a/float_test.go b/float_test.go index cdcd19e..02b8146 100644 --- a/float_test.go +++ b/float_test.go @@ -149,10 +149,10 @@ func Test_Float(t *testing.T) { t.Error(err) } - if string(b) != `{"max":5,"min":1,"type":"float"}` { + if string(b) != `{"type":"float","min":1,"max":5}` { t.Errorf( "expected `%s`, received `%s`", - `{"max":5,"min":1,"type":"float"}`, + `{"type":"float","min":1,"max":5}`, string(b), ) } diff --git a/int_test.go b/int_test.go index d397a50..1405ed3 100644 --- a/int_test.go +++ b/int_test.go @@ -123,10 +123,10 @@ func Test_Int(t *testing.T) { t.Error(err) } - if string(b) != `{"max":5,"min":1,"type":"int"}` { + if string(b) != `{"type":"int","min":1,"max":5}` { t.Errorf( "expected `%s`, received `%s`", - `{"max":5,"min":1,"type":"int"}`, + `{"type":"int","min":1,"max":5}`, string(b), ) } diff --git a/object_test.go b/object_test.go index d20b411..60f8ab6 100644 --- a/object_test.go +++ b/object_test.go @@ -180,10 +180,10 @@ func Test_Object(t *testing.T) { t.Error(err) } - if string(b) != `{"fields":{"password":{"max":20,"min":5,"required":true,"type":"string"},"staySignedIn":{"type":"bool"},"username":{"regex":"^[0-9a-zA-Z_-]+$","required":true,"type":"string"}},"type":"object"}` { + if string(b) != `{"type":"object","fields":{"password":{"type":"string","min":5,"max":20,"required":true},"staySignedIn":{"type":"bool"},"username":{"type":"string","regex":"^[0-9a-zA-Z_-]+$","required":true}}}` { t.Errorf( "expected `%s`, received `%s`", - `{"fields":{"password":{"max":20,"min":5,"required":true,"type":"string"},"staySignedIn":{"type":"bool"},"username":{"regex":"^[0-9a-zA-Z_-]+$","required":true,"type":"string"}},"type":"object"}`, + `{"type":"object","fields":{"password":{"type":"string","min":5,"max":20,"required":true},"staySignedIn":{"type":"bool"},"username":{"type":"string","regex":"^[0-9a-zA-Z_-]+$","required":true}}}`, string(b), ) } diff --git a/ordered_map/map.go b/ordered_map/map.go new file mode 100644 index 0000000..312dee1 --- /dev/null +++ b/ordered_map/map.go @@ -0,0 +1,44 @@ +package ordered_map + +import ( + "bytes" + "encoding/json" + "fmt" +) + +type Item[K comparable, V any] struct { + Key K + Value V +} + +type Map[K comparable, V any] []Item[K, V] + +func (self *Map[K, V]) Set(key K, value V) { + *self = append(*self, Item[K, V]{ + Key: key, + Value: value, + }) +} + +func (self Map[K, V]) MarshalJSON() ([]byte, error) { + buf := &bytes.Buffer{} + buf.Write([]byte{'{'}) + + for i, item := range self { + b, err := json.Marshal(item.Value) + + if err != nil { + return nil, err + } + + buf.WriteString(fmt.Sprintf("%q:", fmt.Sprintf("%v", item.Key))) + buf.Write(b) + + if i < len(self)-1 { + buf.Write([]byte{','}) + } + } + + buf.Write([]byte{'}'}) + return buf.Bytes(), nil +} diff --git a/string_test.go b/string_test.go index d0f37be..805621c 100644 --- a/string_test.go +++ b/string_test.go @@ -228,10 +228,10 @@ func Test_String(t *testing.T) { t.Error(err) } - if string(b) != `{"email":true,"max":5,"min":1,"type":"string"}` { + if string(b) != `{"type":"string","min":1,"max":5,"email":true}` { t.Errorf( "expected `%s`, received `%s`", - `{"email":true,"max":5,"min":1,"type":"string"}`, + `{"type":"string","min":1,"max":5,"email":true}`, string(b), ) }