diff --git a/.gitignore b/.gitignore index 7a8149a..3d8971c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,12 @@ /vendor _* -go.sum \ No newline at end of file +go.sum +go.sum +go.work +vendor/ +_* +benchmark* +.claude* +claude* +CLAUDE* +coverage* diff --git a/errors.go b/errors.go index 441e889..d56dfdf 100644 --- a/errors.go +++ b/errors.go @@ -5,9 +5,7 @@ import ( stdErrors "github.com/bdlm/std/v2/errors" ) -/* -Internal errors -*/ +// Internal errors var ( // InvalidIndex - The specified index does not exist. InvalidIndex stdErrors.Error @@ -26,6 +24,11 @@ var ( // InvalidDataSet - An attempt was made to store a data set that is // with the model type InvalidDataSet stdErrors.Error + + // InvalidSortFlagCombination - The specified sort flag combination is + // invalid for this model type or in conflict with another flag. + // E.g. SortByKey and SortByValue. + InvalidSortFlagCombination stdErrors.Error ) func init() { @@ -33,4 +36,6 @@ func init() { InvalidIndexType = errors.New("an invalid index datatype was used") InvalidMethodContext = errors.New("a method was used in an invalid context") ReadOnlyProperty = errors.New("cannot update a read-only property") + InvalidDataSet = errors.New("invalid data set for model type") + InvalidSortFlagCombination = errors.New("invalid sort flag combination") } diff --git a/examples_test.go b/examples_test.go index 051d63e..907f036 100644 --- a/examples_test.go +++ b/examples_test.go @@ -12,19 +12,19 @@ import ( ) func ExampleNew() { - mdl := model.New(stdModel.ModelTypeHash) + mdl := model.New(stdModel.ModelTypeHash, nil) json.Unmarshal( []byte(`{"key1":"value1","key2":2,"key3":["one","two","three"],"key4":{"k1":"v1","k2":"v2"}}`), &mdl, ) - var key, val interface{} + var key, val any for mdl.Next(&key, &val) { if "key3" == key.(string) || "key4" == key.(string) { - var k2, v2 interface{} + var k2, v2 any var m2 stdModel.Model m2, _ = val.(stdModel.Value).Model() if nil == m2 { - data, hash, index := mdl.Data() + data, hash, index := mdl.GetData() log.Debugf("\n\n\ndata: %v\nhash: %v\nindex: %v\n\n\n", data, hash, index) os.Exit(1) } diff --git a/go.mod b/go.mod index 9e25ec3..68f18c8 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/bdlm/model go 1.26.2 require ( - github.com/bdlm/cast/v2 v2.1.0 + github.com/bdlm/cast/v2 v2.1.4 github.com/bdlm/errors/v2 v2.1.2 github.com/bdlm/log/v2 v2.0.7 github.com/bdlm/std/v2 v2.1.0 diff --git a/interface.marshaler.go b/interface.marshaler.go index 0740a93..1b857a5 100644 --- a/interface.marshaler.go +++ b/interface.marshaler.go @@ -1,22 +1,18 @@ package model -/* -Marshaler is the interface implemented by types that can serialize -themselves into a static byte array. -*/ +// Marshaler is the interface implemented by types that can serialize +// themselves into a static byte array. type Marshaler interface { MarshalModel() ([]byte, error) } -/* -Unmarshaler is the interface implemented by Models that can unmarshal a -serialized description of themselves. The input can be assumed to be a valid -encoding of a Model value. UnmarshalModel must copy the data if it wishes to -retain the data after returning. - -By convention, to approximate the behavior of similar functionality in other -packges, Unmarshalers implement UnmarshalModel([]byte("null")) as a no-op. -*/ +// Unmarshaler is the interface implemented by Models that can unmarshal a +// serialized description of themselves. The input can be assumed to be a valid +// encoding of a Model value. UnmarshalModel must copy the data if it wishes to +// retain the data after returning. +// +// By convention, to approximate the behavior of similar functionality in other +// packages, Unmarshalers implement UnmarshalModel([]byte("null")) as a no-op. type Unmarshaler interface { UnmarshalModel(bytes []byte) error } diff --git a/model.go b/model.go index d52a217..b671d80 100644 --- a/model.go +++ b/model.go @@ -8,9 +8,7 @@ import ( stdModel "github.com/bdlm/std/v2/model" ) -/* -modelType is a data type for defining Model types. -*/ +// modelType is a data type for defining Model types. type modelType int const ( @@ -20,9 +18,7 @@ const ( List ) -/* -Model defines the model data structure. -*/ +// Model defines the model data structure. type Model struct { id any // model identifier locked bool // model read-only flag @@ -35,62 +31,75 @@ type Model struct { pos int // current stdModel.Iterator cursor position } -/* -New returns a new stdModel.Model. -*/ -func New(modelType stdModel.ModelType) *Model { - return &Model{ +// New returns a new stdModel.Model. +func New(modelType stdModel.ModelType, data any) *Model { + model := &Model{ mux: &sync.Mutex{}, typ: modelType, hashIdx: map[string]int{}, idxHash: map[int]string{}, pos: -1, } + if data != nil { + if _, err := model.importData(data); err != nil { + panic(err) + } + } + return model } -/* -Data returns the current data set and indexes. -*/ -func (mdl *Model) Data() ([]any, map[string]int, map[int]string) { - return mdl.data, mdl.hashIdx, mdl.idxHash -} - -/* -Delete removes a value from this model. -*/ +// Delete removes a value from this model. func (mdl *Model) Delete(key any) error { + if mdl.locked { + return errors.WrapE(ReadOnlyProperty, errors.Errorf("model is locked")) + } mdl.mux.Lock() defer mdl.mux.Unlock() if stdModel.ModelTypeList == mdl.GetType() { - k := key.(int) - if k > len(mdl.data) { + k := cast.To[int](key) + if k < 0 || k >= len(mdl.data) { return errors.WrapE(InvalidIndex, errors.Errorf("index '%d' out of range", k)) } - mdl.data = append(mdl.data[:key.(int)-1], mdl.data[key.(int):]...) + mdl.data = append(mdl.data[:k], mdl.data[k+1:]...) return nil } - k := key.(string) + k := cast.To[string](key) if idx, ok := mdl.hashIdx[k]; ok { - mdl.data = append(mdl.data[:idx-1], mdl.data[idx:]...) + mdl.data = append(mdl.data[:idx], mdl.data[idx+1:]...) delete(mdl.hashIdx, k) delete(mdl.idxHash, idx) + for i := idx; i < len(mdl.data); i++ { + shifted := mdl.idxHash[i+1] + mdl.hashIdx[shifted] = i + mdl.idxHash[i] = shifted + delete(mdl.idxHash, i+1) + } return nil } return errors.WrapE(InvalidIndex, errors.Errorf("index '%s' out of range", k)) } -/* -Filter filters elements of the data using a callback function and returns -the result. -*/ -func (mdl *Model) Filter(callback func(stdModel.Value) stdModel.Model) stdModel.Model { - return mdl +// Filter filters elements of the data using a callback function and returns +// the result. +func (mdl *Model) Filter(callback func(stdModel.Value) bool) stdModel.Model { + result := New(mdl.GetType(), nil) + mdl.mux.Lock() + defer mdl.mux.Unlock() + for i := 0; i < len(mdl.data); i++ { + v := toValue(mdl.data[i]) + if callback(v) { + if stdModel.ModelTypeHash == mdl.GetType() { + result.Set(mdl.idxHash[i], v) + } else { + result.Push(v) + } + } + } + return result } -/* -Get returns the specified data value in this model. -*/ +// Get returns the specified data value in this model. func (mdl *Model) Get(key any) (stdModel.Value, error) { if stdModel.ModelTypeHash == mdl.GetType() { var ok bool @@ -115,37 +124,52 @@ func (mdl *Model) Get(key any) (stdModel.Value, error) { case int, int8, int16, int32, int64: mdl.mux.Lock() defer mdl.mux.Unlock() - if key.(int) >= int(len(mdl.data)) { - return nil, errors.WrapE(InvalidIndex, errors.Errorf("invalid index '%d'", key.(int))) + k := cast.To[int](key) + if k < 0 || k >= len(mdl.data) { + return nil, errors.WrapE(InvalidIndex, errors.Errorf("invalid index '%d'", k)) } - ret := mdl.data[key.(int)] - return &Value{ret}, nil + return &Value{mdl.data[k]}, nil default: return nil, errors.WrapE(InvalidIndexType, errors.Errorf("key '%v' must be an integer", key)) } } -/* -GetID returns returns this model's id. -*/ +// GetData returns the current data set and indexes. +func (mdl *Model) GetData() ([]any, map[string]int, map[int]string) { + mdl.mux.Lock() + defer mdl.mux.Unlock() + data := make([]any, len(mdl.data)) + copy(data, mdl.data) + hashIdx := make(map[string]int, len(mdl.hashIdx)) + for k, v := range mdl.hashIdx { + hashIdx[k] = v + } + idxHash := make(map[int]string, len(mdl.idxHash)) + for k, v := range mdl.idxHash { + idxHash[k] = v + } + return data, hashIdx, idxHash +} + +// GetID returns this model's id. func (mdl *Model) GetID() any { return mdl.id } -/* -GetType returns the model type. -*/ +// GetType returns the model type. func (mdl *Model) GetType() stdModel.ModelType { return mdl.typ } -/* -Has tests to see of a specified data element exists in this model. -*/ +// Has tests to see if a specified data element exists in this model. func (mdl *Model) Has(key any) bool { if stdModel.ModelTypeList == mdl.GetType() { - if k, ok := key.(int); ok && k < len(mdl.data) { - return true + switch key.(type) { + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + k := cast.To[int](key) + if k >= 0 && k < len(mdl.data) { + return true + } } } else if kstr, ok := key.(string); ok { if _, ok := mdl.hashIdx[kstr]; ok { @@ -155,69 +179,136 @@ func (mdl *Model) Has(key any) bool { return false } -/* -Lock marks this model as read-only. -*/ +// Lock marks this model as read-only. func (mdl *Model) Lock() { mdl.locked = true } -/* -Map applies a callback to all elements in this model and returns the result. -*/ -func (mdl *Model) Map(callback func(stdModel.Value) stdModel.Model) stdModel.Model { - return nil +// Map applies a callback to all elements in this model and returns the result. +func (mdl *Model) Map(callback func(stdModel.Value) stdModel.Value) stdModel.Model { + result := New(mdl.GetType(), nil) + mdl.mux.Lock() + defer mdl.mux.Unlock() + for i := 0; i < len(mdl.data); i++ { + v := toValue(mdl.data[i]) + mapped := callback(v) + if stdModel.ModelTypeHash == mdl.GetType() { + result.Set(mdl.idxHash[i], mapped) + } else { + result.Push(mapped) + } + } + return result } -/* -Merge merges data from any Model into this Model. -*/ -func (mdl *Model) Merge(model stdModel.Model) error { +// Merge merges data from any Model into this Model. +func (mdl *Model) Merge(incoming stdModel.Model) error { + if mdl.locked { + return errors.WrapE(ReadOnlyProperty, errors.Errorf("model is locked")) + } + + inData, inHashIdx, _ := incoming.GetData() + + switch mdl.GetType() { + case stdModel.ModelTypeHash: + if incoming.GetType() == stdModel.ModelTypeHash { + // hash into hash: incoming wins; merge nested models recursively + for key, inIdx := range inHashIdx { + inVal := inData[inIdx] + mdl.mux.Lock() + myIdx, exists := mdl.hashIdx[key] + var myVal any + if exists { + myVal = mdl.data[myIdx] + } + mdl.mux.Unlock() + if exists { + myModel, myIsModel := asModel(myVal) + inModel, inIsModel := asModel(inVal) + if myIsModel && inIsModel { + if err := myModel.Merge(inModel); err != nil { + return err + } + continue + } + } + if err := mdl.Set(key, inVal); err != nil { + return err + } + } + } else { + // list into hash: string-cast indices become keys + for i, v := range inData { + if err := mdl.Set(cast.To[string](i), v); err != nil { + return err + } + } + } + + case stdModel.ModelTypeList: + if incoming.GetType() == stdModel.ModelTypeList { + // list into list: append + for _, v := range inData { + if err := mdl.Push(v); err != nil { + return err + } + } + } else { + // hash into list: append values in insertion order, ignore keys + for i := 0; i < len(inData); i++ { + if err := mdl.Push(inData[i]); err != nil { + return err + } + } + } + } + return nil } -/* -Push a value to the end of the internal data store. -*/ +// Push a value to the end of the internal data store. func (mdl *Model) Push(value any) error { - if raw, ok := value.(stdModel.Value); ok { - value = raw + if mdl.locked { + return errors.WrapE(ReadOnlyProperty, errors.Errorf("model is locked")) } - // stdModel.ModelTypeList only if stdModel.ModelTypeList != mdl.GetType() { return errors.WrapE(InvalidMethodContext, errors.Errorf("Push() is only valid for stdModel.ModelTypeList model types")) } mdl.mux.Lock() - mdl.data = append(mdl.data, &Value{value}) + mdl.data = append(mdl.data, toValue(value)) mdl.mux.Unlock() return nil } -/* -Reduce iteratively reduces the data to a single value using a callback -function and returns the result. -*/ -func (mdl *Model) Reduce(callback func(stdModel.Value) bool) stdModel.Value { - return nil -} - -/* -Reverse reverses the order of the data store. -*/ -func (mdl *Model) Reverse() { +// Reduce iteratively reduces the data to a single value using a callback +// function and returns the result. +// +// The callback takes two stdModel.Value arguments: carry (the result of the +// previous iteration, or the first element on the first iteration) and cur +// (the current element). The callback returns a stdModel.Value which becomes +// the carry for the next iteration. After all iterations, Reduce returns the +// final carry value. Returns nil for an empty model. +func (mdl *Model) Reduce(callback func(carry, cur stdModel.Value) stdModel.Value) stdModel.Value { + mdl.mux.Lock() + defer mdl.mux.Unlock() + if len(mdl.data) == 0 { + return nil + } + var carry stdModel.Value = toValue(mdl.data[0]) + for i := 1; i < len(mdl.data); i++ { + carry = callback(carry, toValue(mdl.data[i])) + } + return carry } -/* -Set stores a value in the internal data store. All values must be identified -by key. -*/ +// Set stores a value in the internal data store. All values must be identified +// by key. func (mdl *Model) Set(key any, value any) error { - if raw, ok := value.(stdModel.Value); ok { - value = raw + if mdl.locked { + return errors.WrapE(ReadOnlyProperty, errors.Errorf("model is locked")) } - // Hash model if stdModel.ModelTypeHash == mdl.GetType() { // hash keys are always strings @@ -227,10 +318,10 @@ func (mdl *Model) Set(key any, value any) error { if _, ok := mdl.hashIdx[idx]; !ok { mdl.hashIdx[idx] = len(mdl.data) mdl.idxHash[len(mdl.data)] = idx - mdl.data = append(mdl.data, value) + mdl.data = append(mdl.data, toValue(value)) return nil } - mdl.data[mdl.hashIdx[idx]] = value + mdl.data[mdl.hashIdx[idx]] = toValue(value) return nil } @@ -238,37 +329,40 @@ func (mdl *Model) Set(key any, value any) error { switch key.(type) { case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: - k := key.(int) + k := cast.To[int](key) mdl.mux.Lock() defer mdl.mux.Unlock() if k >= len(mdl.data) || k < 0 { return errors.WrapE(InvalidIndex, errors.Errorf("invalid index '%d'", k)) } - mdl.data[k] = value + mdl.data[k] = toValue(value) return nil default: - return errors.WrapE(InvalidIndexType, errors.Errorf("key '%v' is must be an integer", key)) + return errors.WrapE(InvalidIndexType, errors.Errorf("key '%v' must be an integer", key)) } } -/* -SetID sets this Model's identifier property. -*/ -func (mdl *Model) SetID(id any) { +// SetID sets this Model's identifier property. +func (mdl *Model) SetID(id any) error { + if mdl.locked { + return errors.WrapE(ReadOnlyProperty, errors.Errorf("model is locked")) + } mdl.id = id + return nil } -/* -SetData replaces the current data stored in the model with the provided -data. -*/ +// SetData replaces the current data stored in the model with the provided data. func (mdl *Model) SetData(data any) error { + if mdl.locked { + return errors.WrapE(ReadOnlyProperty, errors.Errorf("model is locked")) + } if stdModel.ModelTypeList == mdl.GetType() { d, ok := data.([]any) if !ok { return errors.WrapE(InvalidDataSet, errors.Errorf("invalid data set for list model")) } mdl.data = d + return nil } d, ok := data.(map[string]any) @@ -285,14 +379,32 @@ func (mdl *Model) SetData(data any) error { return nil } -/* -SetType sets the model type. If any data is stored in this model, this -property becomes read-only. -*/ +// SetType sets the model type. If any data is stored in this model, this +// property becomes read-only. func (mdl *Model) SetType(typ stdModel.ModelType) error { + if mdl.locked { + return errors.WrapE(ReadOnlyProperty, errors.Errorf("model is locked")) + } if len(mdl.data) > 0 { return errors.WrapE(ReadOnlyProperty, errors.Errorf("model is not empty, type cannot be modified")) } mdl.typ = typ return nil } + +// toValue wraps v in a *Value, or returns it directly if it is already one. +func toValue(v any) *Value { + if val, ok := v.(*Value); ok && val != nil { + return val + } + return &Value{v} +} + +// asModel extracts a stdModel.Model from v, unwrapping a *Value if necessary. +func asModel(v any) (stdModel.Model, bool) { + if val, ok := v.(*Value); ok { + v = val.data + } + m, ok := v.(stdModel.Model) + return m, ok +} diff --git a/model.importer.go b/model.importer.go index c1464b8..8e219ed 100644 --- a/model.importer.go +++ b/model.importer.go @@ -1,55 +1,80 @@ package model import ( + "github.com/bdlm/errors/v2" stdModel "github.com/bdlm/std/v2/model" stdSorter "github.com/bdlm/std/v2/sorter" ) -func importMap(data map[string]interface{}, node *Model) *Model { +func importMap(data map[string]any, node *Model) (*Model, error) { for k, v := range data { switch typedV := v.(type) { - case map[string]interface{}: - n := New(stdModel.ModelTypeHash) - node.Set(k, importMap(typedV, n)) - case []interface{}: - n := New(stdModel.ModelTypeList) - node.Set(k, importSlice(typedV, n)) - default: - if stdModel.ModelTypeHash == node.GetType() { - node.Set(k, v) + case map[string]any: + n := New(stdModel.ModelTypeHash, nil) + child, err := importMap(typedV, n) + if err != nil { + return nil, errors.Wrap(err, "failed to import nested map at key '%s'", k) + } + if err := node.Set(k, child); err != nil { + return nil, errors.Wrap(err, "failed to set key '%s'", k) + } + case []any: + n := New(stdModel.ModelTypeList, nil) + child, err := importSlice(typedV, n) + if err != nil { + return nil, errors.Wrap(err, "failed to import nested slice at key '%s'", k) } - if stdModel.ModelTypeList == node.GetType() { - node.Push(v) + if err := node.Set(k, child); err != nil { + return nil, errors.Wrap(err, "failed to set key '%s'", k) + } + default: + if err := node.Set(k, v); err != nil { + return nil, errors.Wrap(err, "failed to set key '%s'", k) } } } - node.Sort(stdSorter.SortByKey) - return node + if err := node.Sort(stdSorter.SortByKey); err != nil { + return nil, errors.Wrap(err, "failed to sort by key") + } + return node, nil } -func importSlice(data []interface{}, node *Model) *Model { - for _, v := range data { +func importSlice(data []any, node *Model) (*Model, error) { + for i, v := range data { switch typedV := v.(type) { - case map[string]interface{}: - n := New(stdModel.ModelTypeHash) - node.Push(importMap(typedV, n)) - case []interface{}: - n := New(stdModel.ModelTypeList) - node.Push(importSlice(typedV, n)) + case map[string]any: + n := New(stdModel.ModelTypeHash, nil) + child, err := importMap(typedV, n) + if err != nil { + return nil, errors.Wrap(err, "failed to import nested map at index '%d'", i) + } + if err := node.Push(child); err != nil { + return nil, errors.Wrap(err, "failed to push at index '%d'", i) + } + case []any: + n := New(stdModel.ModelTypeList, nil) + child, err := importSlice(typedV, n) + if err != nil { + return nil, errors.Wrap(err, "failed to import nested slice at index '%d'", i) + } + if err := node.Push(child); err != nil { + return nil, errors.Wrap(err, "failed to push at index '%d'", i) + } default: - node.Push(v) + if err := node.Push(v); err != nil { + return nil, errors.Wrap(err, "failed to push at index '%d'", i) + } } } - node.Sort(stdSorter.SortByKey) - return node + return node, nil } -func (mdl *Model) importData(data interface{}) *Model { +func (mdl *Model) importData(data any) (*Model, error) { switch typedData := data.(type) { - case map[string]interface{}: + case map[string]any: return importMap(typedData, mdl) - case []interface{}: + case []any: return importSlice(typedData, mdl) } - return nil + return nil, nil } diff --git a/model.iterator.go b/model.iterator.go index c7a8cae..b16a12d 100644 --- a/model.iterator.go +++ b/model.iterator.go @@ -1,18 +1,17 @@ package model import ( + "github.com/bdlm/cast/v2" "github.com/bdlm/errors/v2" stdModel "github.com/bdlm/std/v2/model" ) -/* -Cur implements stdModel.Iterator. - -Cur reads the key and value at the current cursor postion into pK and pV -respectively. Cur will return false if no iteration has begun, including -following calls to Reset. -*/ -func (mdl *Model) Cur(pK, pV *interface{}) bool { +// Cur implements stdModel.Iterator. +// +// Cur reads the key and value at the current cursor position into pK and pV +// respectively. Cur will return false if no iteration has begun, including +// following calls to Reset. +func (mdl *Model) Cur(pK, pV *any) bool { if mdl.pos < 0 || mdl.pos >= len(mdl.data) { return false } @@ -30,16 +29,14 @@ func (mdl *Model) Cur(pK, pV *interface{}) bool { return true } -/* -Next implements stdModel.Iterator. - -Next moves the cursor forward one position before reading the key and value -at the cursor position into pK and pV respectively. If data is available at -that position and was written to pK and pV then Next returns true, else -false to signify the end of the data and resets the cursor postion to the -beginning of the data set (-1). -*/ -func (mdl *Model) Next(pK, pV *interface{}) bool { +// Next implements stdModel.Iterator. +// +// Next moves the cursor forward one position before reading the key and value +// at the cursor position into pK and pV respectively. If data is available at +// that position and was written to pK and pV then Next returns true, else +// false to signify the end of the data and resets the cursor position to the +// beginning of the data set (-1). +func (mdl *Model) Next(pK, pV *any) bool { mdl.mux.Lock() mdl.pos++ @@ -64,15 +61,13 @@ func (mdl *Model) Next(pK, pV *interface{}) bool { return true } -/* -Prev implements stdModel.Iterator. - -Prev moves the cursor backward one position before reading the key and value -at the cursor position into pK and pV respectively. If data is available at -that position and was written to pK and pV then Prev returns true, else -false to signify the beginning of the data. -*/ -func (mdl *Model) Prev(pK, pV *interface{}) bool { +// Prev implements stdModel.Iterator. +// +// Prev moves the cursor backward one position before reading the key and value +// at the cursor position into pK and pV respectively. If data is available at +// that position and was written to pK and pV then Prev returns true, else +// false to signify the beginning of the data. +func (mdl *Model) Prev(pK, pV *any) bool { mdl.mux.Lock() mdl.pos-- @@ -96,37 +91,39 @@ func (mdl *Model) Prev(pK, pV *interface{}) bool { return true } -/* -Reset implements stdModel.Iterator. - -Reset sets the iterator cursor position. -*/ +// Reset implements stdModel.Iterator. +// +// Reset sets the iterator cursor to the position before the first element. func (mdl *Model) Reset() { mdl.pos = -1 } -/* -Seek implements stdModel.Iterator. - -Seek sets the iterator cursor position. -*/ -func (mdl *Model) Seek(pos interface{}) error { +// Seek implements stdModel.Iterator. +// +// Seek sets the iterator cursor to the specified position so that Cur returns +// the element at that position. +func (mdl *Model) Seek(pos any) error { // List model if stdModel.ModelTypeList == mdl.GetType() { - idx := pos.(int) + idx, err := cast.ToE[int](pos) + if err != nil { + return errors.WrapE(InvalidIndexType, errors.Errorf("position '%v' must be an integer", pos)) + } + if idx < 0 { + return errors.WrapE(InvalidIndex, errors.Errorf("invalid index '%d'", idx)) + } if idx >= len(mdl.data) { return errors.WrapE(InvalidIndex, errors.Errorf("the specified position '%d' is beyond the end of the data", idx)) - } else if idx < 0 { - return errors.WrapE(InvalidIndex, errors.Errorf("invalid index '%d'", idx)) } - mdl.pos = idx - 1 + mdl.pos = idx return nil } // Hash model hashKey := pos.(string) if idx, ok := mdl.hashIdx[hashKey]; ok { - mdl.pos = idx - 1 + mdl.pos = idx + return nil } return errors.WrapE(InvalidIndex, errors.Errorf("the specified position '%s' does not exist", hashKey)) } diff --git a/model.marshaler.go b/model.marshaler.go index aa7c5a3..011f685 100644 --- a/model.marshaler.go +++ b/model.marshaler.go @@ -7,45 +7,42 @@ import ( stdModel "github.com/bdlm/std/v2/model" ) -/* -MarshalJSON implements json.Marshaler. -*/ +// MarshalJSON implements json.Marshaler. func (mdl *Model) MarshalJSON() ([]byte, error) { + mdl.mux.Lock() + defer mdl.mux.Unlock() if stdModel.ModelTypeList == mdl.GetType() { return json.Marshal(mdl.data) } - d := map[string]interface{}{} + d := map[string]any{} for k, v := range mdl.data { d[mdl.idxHash[k]] = v } return json.Marshal(d) } -/* -MarshalModel implements Marshaler. -*/ +// MarshalModel implements Marshaler. func (mdl *Model) MarshalModel() ([]byte, error) { return mdl.MarshalJSON() } -/* -UnmarshalJSON implements json.Unmarshaler. -*/ +// UnmarshalJSON implements json.Unmarshaler. func (mdl *Model) UnmarshalJSON(jsn []byte) error { - var data interface{} + var data any - err := json.Unmarshal(jsn, &data) - if nil != err { + if err := json.Unmarshal(jsn, &data); err != nil { return errors.Wrap(err, "unmarshaling failed") } - mdl.importData(data) - + if _, err := mdl.importData(data); err != nil { + return errors.Wrap(err, "import failed") + } return nil } -/* -UnmarshalModel implements Marshaler. -*/ -func (mdl *Model) UnmarshalModel() ([]byte, error) { - return mdl.MarshalJSON() +// UnmarshalModel implements Unmarshaler. +func (mdl *Model) UnmarshalModel(bytes []byte) error { + if string(bytes) == "null" { + return nil + } + return mdl.UnmarshalJSON(bytes) } diff --git a/model.sorter.go b/model.sorter.go index ec2b06a..6237d74 100644 --- a/model.sorter.go +++ b/model.sorter.go @@ -1,38 +1,180 @@ package model import ( + "reflect" "sort" + "github.com/bdlm/cast/v2" + "github.com/bdlm/errors/v2" stdModel "github.com/bdlm/std/v2/model" stdSorter "github.com/bdlm/std/v2/sorter" ) -/* -Sort sorts the model data. -*/ +// Len returns the number of items stored in this model. +func (mdl *Model) Len() int { + return len(mdl.data) +} + +// Reverse reverses the order of the data store. +func (mdl *Model) Reverse() error { + if mdl.locked { + return errors.WrapE(ReadOnlyProperty, errors.Errorf("model is locked")) + } + mdl.mux.Lock() + defer mdl.mux.Unlock() + + n := len(mdl.data) + for i, j := 0, n-1; i < j; i, j = i+1, j-1 { + mdl.data[i], mdl.data[j] = mdl.data[j], mdl.data[i] + } + if stdModel.ModelTypeHash == mdl.GetType() { + newHashIdx := make(map[string]int, n) + newIdxHash := make(map[int]string, n) + for oldIdx, key := range mdl.idxHash { + newIdx := n - 1 - oldIdx + newHashIdx[key] = newIdx + newIdxHash[newIdx] = key + } + mdl.hashIdx = newHashIdx + mdl.idxHash = newIdxHash + } + return nil +} + +// Sort sorts the model data by the specified flag. func (mdl *Model) Sort(flag stdSorter.SortFlag) error { - data := []interface{}{} - hashIdx := map[string]int{} - idxHash := map[int]string{} - switch flag { - case stdSorter.SortByKey: + if mdl.locked { + return errors.WrapE(ReadOnlyProperty, errors.Errorf("model is locked")) + } + + if flag&stdSorter.SortByKey != 0 { if stdModel.ModelTypeHash == mdl.GetType() { - order := []string{} - for _, v := range mdl.idxHash { - order = append(order, v) + keys := make([]string, 0, len(mdl.idxHash)) + for _, k := range mdl.idxHash { + keys = append(keys, k) } - sort.Strings(order) - for _, v := range order { - hashIdx[v] = len(data) - idxHash[len(data)] = v - data = append(data, mdl.data[mdl.hashIdx[v]]) + sort.Strings(keys) + mdl.data, mdl.hashIdx, mdl.idxHash = rebuildHashIndexes(keys, mdl) + } + // list: integer keys are always positional, SortByKey is a no-op + } + + if flag&stdSorter.SortByValue != 0 { + compare := stratifiedLess + if flag&stdSorter.SortAsString != 0 { + compare = stringLess + } + + if stdModel.ModelTypeHash == mdl.GetType() { + keys := make([]string, 0, len(mdl.idxHash)) + for _, k := range mdl.idxHash { + keys = append(keys, k) } - mdl.data = data - mdl.hashIdx = hashIdx - mdl.idxHash = idxHash + sort.SliceStable(keys, func(i, j int) bool { + return compare(mdl.data[mdl.hashIdx[keys[i]]], mdl.data[mdl.hashIdx[keys[j]]]) + }) + mdl.data, mdl.hashIdx, mdl.idxHash = rebuildHashIndexes(keys, mdl) } + if stdModel.ModelTypeList == mdl.GetType() { + sort.SliceStable(mdl.data, func(i, j int) bool { + return compare(mdl.data[i], mdl.data[j]) + }) + } + } + + if flag&stdSorter.SortReverse != 0 { + if err := mdl.Reverse(); err != nil { + return errors.Wrap(err, "failed to reverse data") } } + return nil } + +// modelLen returns the number of elements in a Model, or 0 for other types. +func modelLen(v any) int { + if m, ok := v.(stdModel.Model); ok { + d, _, _ := m.GetData() + return len(d) + } + return 0 +} + +// rebuildHashIndexes rebuilds hashIdx and idxHash from an ordered key slice +// after the data slice has been repopulated in that order. +func rebuildHashIndexes(keys []string, src *Model) ([]any, map[string]int, map[int]string) { + data := make([]any, 0, len(keys)) + hashIdx := make(map[string]int, len(keys)) + idxHash := make(map[int]string, len(keys)) + for _, k := range keys { + hashIdx[k] = len(data) + idxHash[len(data)] = k + data = append(data, src.data[src.hashIdx[k]]) + } + return data, hashIdx, idxHash +} + +// stratifiedLess compares two values using type-stratified ordering: +// Model/*Model < nil < bool < numeric < string < other. Within each bucket, +// values are compared naturally (bool: false