├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── callbacks.go ├── export_test.go ├── go.mod ├── go.sum ├── plugin.go ├── plugin_test.go └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | env: 3 | - GO111MODULE=on 4 | go: 5 | - 1.11.x 6 | before_install: 7 | - go get github.com/mattn/goveralls 8 | script: 9 | - go test -v ./... 10 | - $GOPATH/bin/goveralls -service=travis-ci 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Viorel Craescu & Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GORM Nested Set Tree [![Go Report Card](https://goreportcard.com/badge/github.com/vcraescu/gorm-nested)](https://goreportcard.com/report/github.com/vcraescu/gorm-nested) [![Build Status](https://travis-ci.com/vcraescu/gorm-nested.svg?branch=master)](https://travis-ci.com/vcraescu/gorm-nested) [![Coverage Status](https://coveralls.io/repos/github/vcraescu/gorm-nested/badge.svg?branch=master)](https://coveralls.io/github/vcraescu/gorm-nested?branch=master) 2 | 3 | #### Do not use in production. This is an experiment!!! 4 | 5 | 6 | #### Install 7 | 8 | `go get github.com/vcraescu/gorm-nested` 9 | 10 | 11 | #### How to use 12 | 13 | ```go 14 | type Taxon struct { 15 | ID uint `gorm:"primary_key"` 16 | Name string 17 | ParentID uint 18 | Parent *Taxon `gorm:"association_autoupdate:false"` 19 | TreeLeft int `gorm-nested:"left"` 20 | TreeRight int `gorm-nested:"right"` 21 | TreeLevel int `gorm-nested:"level"` 22 | } 23 | 24 | func (t Taxon) GetParentID() interface{} { 25 | return t.ParentID 26 | } 27 | 28 | func (t Taxon) GetParent() nested.Interface { 29 | return t.Parent 30 | } 31 | 32 | func main() { 33 | db, err := gorm.Open("sqlite3", "my_dbname") 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | db.AutoMigrate(&Taxon{}) 39 | 40 | _, err = nested.Register(db) 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /callbacks.go: -------------------------------------------------------------------------------- 1 | package nested 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jinzhu/gorm" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | func (p *Plugin) createCallback(scope *gorm.Scope) { 11 | p.initColumnNames(scope.Value) 12 | 13 | value := doubleToSingleIndirect(scope.Value) 14 | if !isTreeNode(value) { 15 | return 16 | } 17 | 18 | node := value.(Interface) 19 | defer refreshNode(node, scope) 20 | 21 | if isRoot(node) { 22 | p.updateInsertRootNode(node, scope) 23 | 24 | return 25 | } 26 | 27 | err := p.updateTreeAfterInsertChildNode(node, scope) 28 | if err != nil { 29 | panic(err) 30 | } 31 | } 32 | 33 | func (p *Plugin) updateCallback(scope *gorm.Scope) { 34 | p.initColumnNames(scope.Value) 35 | 36 | value := doubleToSingleIndirect(scope.Value) 37 | if isUpdateIgnored(scope) || !isTreeNode(value) { 38 | return 39 | } 40 | 41 | node := value.(Interface) 42 | defer refreshNode(node, scope) 43 | 44 | width := nodeWidth(node) 45 | db := scope.DB().Set(settingIgnoreUpdate, true) 46 | 47 | parent := node.GetParent() 48 | if isRoot(node) { 49 | max := newNodePtrFromValue(node) 50 | db.Order(p.expr(":tree_right desc")).First(max) 51 | 52 | treeOffset := (getTreeRight(max) - width) + 1 - getTreeLeft(node) 53 | if treeOffset == 0 { 54 | return 55 | } 56 | 57 | levelOffset := 0 - getTreeLevel(node) 58 | db. 59 | Table(scope.TableName()). 60 | Where(p.expr(":tree_left >= ? and :tree_right <= ?"), getTreeLeft(node), getTreeRight(node)). 61 | Updates(map[string]interface{}{ 62 | p.treeLeftName: gorm.Expr(p.expr("-1 * (:tree_left + ?)"), treeOffset), 63 | p.treeRightName: gorm.Expr(p.expr("-1 * (:tree_right + ?)"), treeOffset), 64 | p.treeLevelName: gorm.Expr(p.expr(":tree_level + ?"), levelOffset), 65 | }) 66 | 67 | p.shiftTreeFromRightOf(scope, node, width) 68 | 69 | db. 70 | Table(scope.TableName()). 71 | Where(p.expr(":tree_right < 0"), getTreeLeft(node), getTreeRight(node)). 72 | Updates(map[string]interface{}{ 73 | p.treeLeftName: gorm.Expr(p.expr("-1 * :tree_left")), 74 | p.treeRightName: gorm.Expr(p.expr("-1 * tree_right")), 75 | }) 76 | 77 | return 78 | } 79 | 80 | // update current node subtreee and remove it 81 | treeOffset := getTreeRight(parent) - getTreeLeft(node) 82 | levelOffset := getTreeLevel(parent) + 1 - getTreeLevel(node) 83 | 84 | db. 85 | Table(scope.TableName()). 86 | Where(p.expr(":tree_left >= ? and :tree_right <= ?"), getTreeLeft(node), getTreeRight(node)). 87 | Updates(map[string]interface{}{ 88 | p.treeLeftName: gorm.Expr(p.expr("0 - (:tree_left + ?)"), treeOffset), 89 | p.treeRightName: gorm.Expr(p.expr("0 - (:tree_right + ?)"), treeOffset), 90 | p.treeLevelName: gorm.Expr(p.expr(":tree_level + ?"), levelOffset), 91 | }) 92 | 93 | // shift nodes from the right of moving node to the left 94 | p.shiftTreeFromRightOf(scope, node, width) 95 | 96 | // reload parent because it might be update after the query from above 97 | db.First(parent) 98 | 99 | // shift nodes from the right of parent node to the right 100 | p.shiftTreeFromRightOf(scope, parent, -1*width) 101 | 102 | updateCurrentNode(parent, map[string]interface{}{ 103 | p.treeRightName: getTreeRight(parent) + width, 104 | }, scope) 105 | 106 | // put back current tree 107 | db. 108 | Table(scope.TableName()). 109 | Where(p.expr(":tree_right < 0")). 110 | Updates(map[string]interface{}{ 111 | p.treeLeftName: gorm.Expr(p.expr("-1 * :tree_left")), 112 | p.treeRightName: gorm.Expr(p.expr("-1 * :tree_right")), 113 | }) 114 | } 115 | 116 | func (p *Plugin) deleteCallback(scope *gorm.Scope) { 117 | p.initColumnNames(scope.Value) 118 | 119 | value := doubleToSingleIndirect(scope.Value) 120 | if isDeletionIgnored(scope) || !isTreeNode(scope.Value) { 121 | return 122 | } 123 | 124 | node := value.(Interface) 125 | 126 | defer refreshNode(node, scope) 127 | 128 | p.deleteTree(node, scope) 129 | width := nodeWidth(node) 130 | p.shiftTreeFromRightOf(scope, node, width) 131 | } 132 | 133 | func (p *Plugin) shiftTreeFromRightOf(scope *gorm.Scope, node Interface, offset int) { 134 | db := scope.DB().Set(settingIgnoreUpdate, true) 135 | treeRight := getTreeRight(node) 136 | db. 137 | Table(scope.TableName()). 138 | Where(p.expr(":tree_right > ?"), treeRight). 139 | Update(p.treeRightName, gorm.Expr(p.expr(":tree_right - ?"), offset)) 140 | db. 141 | Table(scope.TableName()). 142 | Where(p.expr(":tree_left > ?"), treeRight). 143 | Update(p.treeLeftName, gorm.Expr(p.expr(":tree_left - ?"), offset)) 144 | } 145 | 146 | func findParent(node Interface, scope *gorm.Scope) (Interface, bool) { 147 | db := scope.NewDB() 148 | parent := newNodePtrFromValue(node) 149 | where := fmt.Sprintf("%s = ?", scope.PrimaryKey()) 150 | db.First(parent, where, node.GetParentID()) 151 | 152 | return parent, !isZeroValue(node.GetParentID()) 153 | } 154 | 155 | func (p *Plugin) deleteTree(node Interface, scope *gorm.Scope) { 156 | db := scope.DB().Set(settingIgnoreDelete, true) 157 | db.Delete( 158 | newNodePtrFromValue(scope.Value), 159 | p.expr(":tree_left > ? AND :tree_left < ?"), 160 | getTreeLeft(node), 161 | getTreeRight(node), 162 | ) 163 | } 164 | 165 | func nodeWidth(node Interface) int { 166 | return getTreeRight(node) - getTreeLeft(node) + 1 167 | } 168 | 169 | func isRoot(node Interface) bool { 170 | return isZeroValue(node.GetParentID()) 171 | } 172 | 173 | func (p *Plugin) updateInsertRootNode(node Interface, scope *gorm.Scope) { 174 | db := scope.DB().Set(settingIgnoreUpdate, true) 175 | max := newNodePtrFromValue(node) 176 | db.Order(p.expr(":tree_right desc")).First(max) 177 | treeRight := getTreeRight(max) 178 | updateCurrentNode(node, map[string]interface{}{ 179 | p.treeLeftName: treeRight + 1, 180 | p.treeRightName: treeRight + 2, 181 | }, scope) 182 | } 183 | 184 | func (p *Plugin) updateTreeAfterInsertChildNode(node Interface, scope *gorm.Scope) error { 185 | parent, ok := findParent(node, scope) 186 | if !ok { 187 | panic(fmt.Errorf("parent not found: %s", node.GetParentID())) 188 | } 189 | 190 | db := scope.DB().Set(settingIgnoreUpdate, true) 191 | treeRight := getTreeRight(parent) 192 | db. 193 | Table(scope.TableName()). 194 | Where(p.expr(":tree_right >= ?"), treeRight). 195 | Update(p.treeRightName, gorm.Expr(p.expr(":tree_right + 2"))) 196 | db. 197 | Table(scope.TableName()). 198 | Where(p.expr(":tree_left >= ?"), treeRight). 199 | Update(p.treeLeftName, gorm.Expr(p.expr(":tree_left + 2"))) 200 | 201 | updateCurrentNode(node, map[string]interface{}{ 202 | p.treeLeftName: treeRight, 203 | p.treeRightName: treeRight + 1, 204 | p.treeLevelName: getTreeLevel(parent) + 1, 205 | }, scope) 206 | 207 | return nil 208 | } 209 | 210 | func (p *Plugin) expr(expr string) string { 211 | expr = strings.Replace(expr, ":tree_left", p.treeLeftName, -1) 212 | expr = strings.Replace(expr, ":tree_right", p.treeRightName, -1) 213 | expr = strings.Replace(expr, ":tree_level", p.treeLevelName, -1) 214 | 215 | return expr 216 | } 217 | 218 | func (p *Plugin) initColumnNames(node interface{}) { 219 | var dbf *gorm.Field 220 | 221 | if p.treeRightName != "" || p.treeLeftName != "" || p.treeLevelName != "" { 222 | return 223 | } 224 | 225 | if f, ok := getFieldByTagValue(node, "left"); ok { 226 | if dbf, ok = p.db.NewScope(node).FieldByName(f.Name); ok { 227 | p.treeLeftName = dbf.DBName 228 | } 229 | } 230 | 231 | if f, ok := getFieldByTagValue(node, "right"); ok { 232 | if dbf, ok = p.db.NewScope(node).FieldByName(f.Name); ok { 233 | p.treeRightName = dbf.DBName 234 | } 235 | } 236 | 237 | if f, ok := getFieldByTagValue(node, "level"); ok { 238 | if dbf, ok = p.db.NewScope(node).FieldByName(f.Name); ok { 239 | p.treeLevelName = dbf.DBName 240 | } 241 | } 242 | } 243 | 244 | func updateCurrentNode(node Interface, updates map[string]interface{}, scope *gorm.Scope) { 245 | scope = scope.New(node) 246 | db := scope.DB().Set(settingIgnoreUpdate, true) 247 | db. 248 | Table(scope.TableName()). 249 | Where(fmt.Sprintf("%s = ?", scope.PrimaryKey()), scope.PrimaryKeyValue()). 250 | Updates(updates) 251 | } 252 | 253 | func refreshNode(node Interface, scope *gorm.Scope) { 254 | parent := node.GetParent() 255 | for !isNilInterface(parent) { 256 | scope.DB().First(parent) 257 | parent = parent.GetParent() 258 | } 259 | 260 | scope.DB().First(node) 261 | } 262 | 263 | func getFieldByTagValue(node interface{}, tagValue string) (*reflect.StructField, bool) { 264 | v := reflect.Indirect(reflect.Indirect(reflect.ValueOf(node))).Interface() 265 | t := reflect.TypeOf(v) 266 | for i := 0; i < t.NumField(); i++ { 267 | f := t.Field(i) 268 | tv, ok := f.Tag.Lookup(tagName) 269 | if !ok || tv != tagValue { 270 | continue 271 | } 272 | 273 | return &f, true 274 | } 275 | 276 | return nil, false 277 | } 278 | 279 | func getTreeLeft(node Interface) int { 280 | f, ok := getFieldByTagValue(node, "left") 281 | if !ok { 282 | return 0 283 | } 284 | 285 | v := reflect.Indirect(reflect.ValueOf(node)) 286 | return int(v.FieldByName(f.Name).Int()) 287 | } 288 | 289 | func getTreeRight(node Interface) int { 290 | f, ok := getFieldByTagValue(node, "right") 291 | if !ok { 292 | return 0 293 | } 294 | 295 | v := reflect.Indirect(reflect.ValueOf(node)) 296 | return int(v.FieldByName(f.Name).Int()) 297 | } 298 | 299 | func getTreeLevel(node Interface) int { 300 | f, ok := getFieldByTagValue(node, "level") 301 | if !ok { 302 | return 0 303 | } 304 | 305 | v := reflect.Indirect(reflect.ValueOf(node)) 306 | return int(v.FieldByName(f.Name).Int()) 307 | } 308 | 309 | func isTreeNode(v interface{}) bool { 310 | node, ok := v.(Interface) 311 | if !ok { 312 | return false 313 | } 314 | 315 | return isValidNode(node) 316 | } 317 | 318 | func isValidNode(node Interface) bool { 319 | _, ok := getFieldByTagValue(node, "left") 320 | if !ok { 321 | return false 322 | } 323 | 324 | _, ok = getFieldByTagValue(node, "right") 325 | if !ok { 326 | return false 327 | } 328 | 329 | _, ok = getFieldByTagValue(node, "level") 330 | 331 | return ok 332 | } 333 | 334 | func isUpdateIgnored(scope *gorm.Scope) bool { 335 | v, ok := scope.Get(settingIgnoreUpdate) 336 | if !ok { 337 | return false 338 | } 339 | 340 | vv, _ := v.(bool) 341 | 342 | return vv 343 | } 344 | 345 | func isDeletionIgnored(scope *gorm.Scope) bool { 346 | v, ok := scope.Get(settingIgnoreDelete) 347 | if !ok { 348 | return false 349 | } 350 | 351 | vv, _ := v.(bool) 352 | 353 | return vv 354 | } 355 | -------------------------------------------------------------------------------- /export_test.go: -------------------------------------------------------------------------------- 1 | package nested 2 | 3 | var ( 4 | GetTreeLeft = getTreeLeft 5 | GetTreeRight = getTreeRight 6 | GetTreeLevel = getTreeLevel 7 | ) 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vcraescu/gorm-nested 2 | 3 | require ( 4 | github.com/davecgh/go-spew v1.1.1 5 | github.com/jinzhu/gorm v1.9.2 6 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect 7 | github.com/mattn/go-sqlite3 v1.10.0 // indirect 8 | github.com/stretchr/objx v0.1.1 // indirect 9 | github.com/stretchr/testify v1.3.0 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/jinzhu/gorm v1.9.2 h1:lCvgEaqe/HVE+tjAR2mt4HbbHAZsQOv3XAZiEZV37iw= 5 | github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= 6 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k= 7 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 8 | github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= 9 | github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 14 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 15 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 16 | -------------------------------------------------------------------------------- /plugin.go: -------------------------------------------------------------------------------- 1 | package nested 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | const ( 8 | tagName = "gorm-nested" 9 | callbackNameCreate = "gorm-nested:create" 10 | callbackNameUpdate = "gorm-nested:update" 11 | callbackNameDelete = "gorm-nested:delete" 12 | settingIgnoreUpdate = "gorm-nested:ignore_update" 13 | settingIgnoreDelete = "gorm-nested:ignore_delete" 14 | ) 15 | 16 | // Plugin gorm nested set plugin 17 | type Plugin struct { 18 | db *gorm.DB 19 | treeLeftName string 20 | treeRightName string 21 | treeLevelName string 22 | } 23 | 24 | // Register registers nested set plugin 25 | func Register(db *gorm.DB) (Plugin, error) { 26 | p := Plugin{db: db} 27 | 28 | p.enableCallbacks() 29 | 30 | return p, nil 31 | } 32 | 33 | func (p *Plugin) enableCallbacks() { 34 | callback := p.db.Callback() 35 | callback.Create().After("gorm:after_create").Register(callbackNameCreate, p.createCallback) 36 | callback.Update().After("gorm:after_update").Register(callbackNameUpdate, p.updateCallback) 37 | callback.Delete().After("gorm:after_delete").Register(callbackNameDelete, p.deleteCallback) 38 | } 39 | 40 | // Interface must be implemented by the gorm model 41 | type Interface interface { 42 | GetParentID() interface{} 43 | GetParent() Interface 44 | } 45 | -------------------------------------------------------------------------------- /plugin_test.go: -------------------------------------------------------------------------------- 1 | package nested_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jinzhu/gorm" 6 | _ "github.com/jinzhu/gorm/dialects/sqlite" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/suite" 9 | "github.com/vcraescu/gorm-nested" 10 | "math/rand" 11 | "os" 12 | "testing" 13 | ) 14 | 15 | var dbName = fmt.Sprintf("test_%d.db", rand.Int()) 16 | 17 | type PluginTestSuite struct { 18 | suite.Suite 19 | db *gorm.DB 20 | } 21 | 22 | type Taxon struct { 23 | ID uint `gorm:"primary_key"` 24 | Name string 25 | ParentID uint 26 | Parent *Taxon `gorm:"association_autoupdate:false"` 27 | TreeLeft int `gorm-nested:"left"` 28 | TreeRight int `gorm-nested:"right"` 29 | TreeLevel int `gorm-nested:"level"` 30 | } 31 | 32 | func (t Taxon) GetParentID() interface{} { 33 | return t.ParentID 34 | } 35 | 36 | func (t Taxon) GetParent() nested.Interface { 37 | return t.Parent 38 | } 39 | 40 | func (suite *PluginTestSuite) SetupTest() { 41 | db, err := gorm.Open("sqlite3", dbName) 42 | if err != nil { 43 | panic(fmt.Errorf("setup test: %s", err)) 44 | } 45 | 46 | suite.db = db 47 | suite.db.AutoMigrate(&Taxon{}) 48 | 49 | _, err = nested.Register(suite.db) 50 | if err != nil { 51 | panic(err) 52 | } 53 | } 54 | 55 | func (suite *PluginTestSuite) TearDownTest() { 56 | if err := suite.db.Close(); err != nil { 57 | panic(fmt.Errorf("tear down test: %s", err)) 58 | } 59 | 60 | if err := os.Remove(dbName); err != nil { 61 | panic(fmt.Errorf("tear down test: %s", err)) 62 | } 63 | } 64 | 65 | func (suite *PluginTestSuite) TestAddRoot() { 66 | root1 := Taxon{ 67 | Name: "Root1", 68 | } 69 | suite.db.Create(&root1) 70 | 71 | root2 := Taxon{ 72 | Name: "Root2", 73 | } 74 | suite.db.Create(&root2) 75 | 76 | root3 := Taxon{ 77 | Name: "Root3", 78 | } 79 | suite.db.Create(&root3) 80 | 81 | var taxons []Taxon 82 | suite.db.Find(&taxons) 83 | 84 | assert.Len(suite.T(), taxons, 3) 85 | assert.Equal(suite.T(), 1, taxons[0].TreeLeft) 86 | assert.Equal(suite.T(), 2, taxons[0].TreeRight) 87 | 88 | assert.Equal(suite.T(), 3, taxons[1].TreeLeft) 89 | assert.Equal(suite.T(), 4, taxons[1].TreeRight) 90 | 91 | assert.Equal(suite.T(), 5, taxons[2].TreeLeft) 92 | assert.Equal(suite.T(), 6, taxons[2].TreeRight) 93 | } 94 | 95 | func (suite *PluginTestSuite) TestInsertEntireTree() { 96 | node := Taxon{ 97 | Name: "Tube", 98 | Parent: &Taxon{ 99 | Name: "Television", 100 | Parent: &Taxon{ 101 | Name: "Electronics", 102 | }, 103 | }, 104 | } 105 | suite.db.Create(&node) 106 | 107 | var taxons []Taxon 108 | suite.db.Find(&taxons) 109 | 110 | assert.Len(suite.T(), taxons, 3) 111 | assert.Equal(suite.T(), 1, taxons[0].TreeLeft) 112 | assert.Equal(suite.T(), 6, taxons[0].TreeRight) 113 | 114 | assert.Equal(suite.T(), 2, taxons[1].TreeLeft) 115 | assert.Equal(suite.T(), 5, taxons[1].TreeRight) 116 | 117 | assert.Equal(suite.T(), 3, taxons[2].TreeLeft) 118 | assert.Equal(suite.T(), 4, taxons[2].TreeRight) 119 | } 120 | 121 | func (suite *PluginTestSuite) TestInsertNodeByNode() { 122 | television := Taxon{ 123 | Name: "Television", 124 | Parent: &Taxon{ 125 | Name: "Electronics", 126 | }, 127 | } 128 | suite.db.Create(&television) 129 | 130 | tube := Taxon{ 131 | Name: "Tube", 132 | Parent: &television, 133 | } 134 | suite.db.Create(&tube) 135 | 136 | var taxons []Taxon 137 | suite.db.Find(&taxons) 138 | 139 | assert.Len(suite.T(), taxons, 3) 140 | assert.Equal(suite.T(), 1, taxons[0].TreeLeft) 141 | assert.Equal(suite.T(), 6, taxons[0].TreeRight) 142 | assert.Equal(suite.T(), 0, taxons[0].TreeLevel) 143 | 144 | assert.Equal(suite.T(), 2, taxons[1].TreeLeft) 145 | assert.Equal(suite.T(), 5, taxons[1].TreeRight) 146 | assert.Equal(suite.T(), 1, taxons[1].TreeLevel) 147 | 148 | assert.Equal(suite.T(), 3, taxons[2].TreeLeft) 149 | assert.Equal(suite.T(), 4, taxons[2].TreeRight) 150 | assert.Equal(suite.T(), 2, taxons[2].TreeLevel) 151 | } 152 | 153 | func (suite *PluginTestSuite) TestDeleteNode() { 154 | suite.createTree() 155 | 156 | television := Taxon{} 157 | suite.db.First(&television, "name = 'Television'") 158 | suite.db.Delete(&television) 159 | 160 | var taxons []Taxon 161 | suite.db.Find(&taxons) 162 | 163 | assert.Len(suite.T(), taxons, 7) 164 | var count int 165 | for _, taxon := range taxons { 166 | switch taxon.Name { 167 | case "Electronics": 168 | assert.Equal(suite.T(), 1, taxon.TreeLeft) 169 | assert.Equal(suite.T(), 14, taxon.TreeRight) 170 | count++ 171 | break 172 | case "Game Consoles": 173 | assert.Equal(suite.T(), 2, taxon.TreeLeft) 174 | assert.Equal(suite.T(), 3, taxon.TreeRight) 175 | count++ 176 | break 177 | case "Portable Electronics": 178 | assert.Equal(suite.T(), 4, taxon.TreeLeft) 179 | assert.Equal(suite.T(), 13, taxon.TreeRight) 180 | count++ 181 | break 182 | case "MP3": 183 | assert.Equal(suite.T(), 5, taxon.TreeLeft) 184 | assert.Equal(suite.T(), 8, taxon.TreeRight) 185 | count++ 186 | break 187 | case "Flash": 188 | assert.Equal(suite.T(), 6, taxon.TreeLeft) 189 | assert.Equal(suite.T(), 7, taxon.TreeRight) 190 | count++ 191 | break 192 | case "CD Player": 193 | assert.Equal(suite.T(), 9, taxon.TreeLeft) 194 | assert.Equal(suite.T(), 10, taxon.TreeRight) 195 | count++ 196 | break 197 | case "Radio": 198 | assert.Equal(suite.T(), 11, taxon.TreeLeft) 199 | assert.Equal(suite.T(), 12, taxon.TreeRight) 200 | count++ 201 | break 202 | } 203 | } 204 | 205 | var portableElectronics Taxon 206 | suite.db.First(&portableElectronics, "name = 'Portable Electronics'") 207 | suite.db.Delete(&portableElectronics) 208 | 209 | taxons = []Taxon{} 210 | suite.db.Find(&taxons) 211 | 212 | assert.Len(suite.T(), taxons, 2) 213 | count = 0 214 | for _, taxon := range taxons { 215 | switch taxon.Name { 216 | case "Electronics": 217 | assert.Equal(suite.T(), 1, taxon.TreeLeft) 218 | assert.Equal(suite.T(), 4, taxon.TreeRight) 219 | count++ 220 | break 221 | case "Game Consoles": 222 | assert.Equal(suite.T(), 2, taxon.TreeLeft) 223 | assert.Equal(suite.T(), 3, taxon.TreeRight) 224 | count++ 225 | break 226 | } 227 | } 228 | 229 | var gameConsoles Taxon 230 | suite.db.First(&gameConsoles, "name = 'Game Consoles'") 231 | suite.db.Delete(&gameConsoles) 232 | taxons = []Taxon{} 233 | suite.db.Find(&taxons) 234 | 235 | assert.Equal(suite.T(), 1, taxons[0].TreeLeft) 236 | assert.Equal(suite.T(), 2, taxons[0].TreeRight) 237 | } 238 | 239 | func (suite *PluginTestSuite) TestMoveNodeToLeft() { 240 | suite.createTree() 241 | 242 | var portableElectronics Taxon 243 | var lcd Taxon 244 | 245 | assert.False(suite.T(), suite.db.First(&portableElectronics, "name = 'Portable Electronics'").RecordNotFound()) 246 | assert.False(suite.T(), suite.db.First(&lcd, "name = 'LCD'").RecordNotFound()) 247 | 248 | portableElectronics.Parent = &lcd 249 | portableElectronics.ParentID = lcd.ID 250 | 251 | suite.db.Save(&portableElectronics) 252 | 253 | var taxons []Taxon 254 | suite.db.Find(&taxons) 255 | 256 | assert.Len(suite.T(), taxons, 11) 257 | 258 | var count int 259 | for _, taxon := range taxons { 260 | switch taxon.Name { 261 | case "Electronics": 262 | assert.Equal(suite.T(), 1, taxon.TreeLeft) 263 | assert.Equal(suite.T(), 22, taxon.TreeRight) 264 | assert.Equal(suite.T(), 0, taxon.TreeLevel) 265 | count++ 266 | break 267 | case "Television": 268 | assert.Equal(suite.T(), 2, taxon.TreeLeft) 269 | assert.Equal(suite.T(), 19, taxon.TreeRight) 270 | assert.Equal(suite.T(), 1, taxon.TreeLevel) 271 | count++ 272 | break 273 | case "Game Consoles": 274 | assert.Equal(suite.T(), 20, taxon.TreeLeft) 275 | assert.Equal(suite.T(), 21, taxon.TreeRight) 276 | assert.Equal(suite.T(), 1, taxon.TreeLevel) 277 | count++ 278 | break 279 | case "Tube": 280 | assert.Equal(suite.T(), 3, taxon.TreeLeft) 281 | assert.Equal(suite.T(), 4, taxon.TreeRight) 282 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 283 | count++ 284 | break 285 | case "LCD": 286 | assert.Equal(suite.T(), 5, taxon.TreeLeft) 287 | assert.Equal(suite.T(), 16, taxon.TreeRight) 288 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 289 | count++ 290 | break 291 | case "Plasma": 292 | assert.Equal(suite.T(), 17, taxon.TreeLeft) 293 | assert.Equal(suite.T(), 18, taxon.TreeRight) 294 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 295 | count++ 296 | break 297 | case "Portable Electronics": 298 | assert.Equal(suite.T(), 6, taxon.TreeLeft) 299 | assert.Equal(suite.T(), 15, taxon.TreeRight) 300 | assert.Equal(suite.T(), 3, taxon.TreeLevel) 301 | count++ 302 | break 303 | case "MP3": 304 | assert.Equal(suite.T(), 7, taxon.TreeLeft) 305 | assert.Equal(suite.T(), 10, taxon.TreeRight) 306 | assert.Equal(suite.T(), 4, taxon.TreeLevel) 307 | count++ 308 | break 309 | case "Flash": 310 | assert.Equal(suite.T(), 8, taxon.TreeLeft) 311 | assert.Equal(suite.T(), 9, taxon.TreeRight) 312 | assert.Equal(suite.T(), 5, taxon.TreeLevel) 313 | count++ 314 | break 315 | case "CD Player": 316 | assert.Equal(suite.T(), 11, taxon.TreeLeft) 317 | assert.Equal(suite.T(), 12, taxon.TreeRight) 318 | assert.Equal(suite.T(), 4, taxon.TreeLevel) 319 | count++ 320 | break 321 | case "Radio": 322 | assert.Equal(suite.T(), 13, taxon.TreeLeft) 323 | assert.Equal(suite.T(), 14, taxon.TreeRight) 324 | assert.Equal(suite.T(), 4, taxon.TreeLevel) 325 | count++ 326 | break 327 | } 328 | } 329 | 330 | assert.Equal(suite.T(), len(taxons), count) 331 | } 332 | 333 | func (suite *PluginTestSuite) TestMoveNodeToRight() { 334 | suite.createTree() 335 | 336 | var mp3 Taxon 337 | var lcd Taxon 338 | 339 | assert.False(suite.T(), suite.db.First(&mp3, "name = 'MP3'").RecordNotFound()) 340 | assert.False(suite.T(), suite.db.First(&lcd, "name = 'LCD'").RecordNotFound()) 341 | 342 | lcd.Parent = &mp3 343 | lcd.ParentID = mp3.ID 344 | 345 | suite.db.Save(&lcd) 346 | 347 | var taxons []Taxon 348 | suite.db.Find(&taxons) 349 | 350 | assert.Len(suite.T(), taxons, 11) 351 | 352 | var count int 353 | for _, taxon := range taxons { 354 | switch taxon.Name { 355 | case "Electronics": 356 | assert.Equal(suite.T(), 1, taxon.TreeLeft) 357 | assert.Equal(suite.T(), 22, taxon.TreeRight) 358 | assert.Equal(suite.T(), 0, taxon.TreeLevel) 359 | count++ 360 | break 361 | case "Television": 362 | assert.Equal(suite.T(), 2, taxon.TreeLeft) 363 | assert.Equal(suite.T(), 7, taxon.TreeRight) 364 | assert.Equal(suite.T(), 1, taxon.TreeLevel) 365 | count++ 366 | break 367 | case "Game Consoles": 368 | assert.Equal(suite.T(), 8, taxon.TreeLeft) 369 | assert.Equal(suite.T(), 9, taxon.TreeRight) 370 | assert.Equal(suite.T(), 1, taxon.TreeLevel) 371 | count++ 372 | break 373 | case "Tube": 374 | assert.Equal(suite.T(), 3, taxon.TreeLeft) 375 | assert.Equal(suite.T(), 4, taxon.TreeRight) 376 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 377 | count++ 378 | break 379 | case "Plasma": 380 | assert.Equal(suite.T(), 5, taxon.TreeLeft) 381 | assert.Equal(suite.T(), 6, taxon.TreeRight) 382 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 383 | count++ 384 | break 385 | case "Portable Electronics": 386 | assert.Equal(suite.T(), 10, taxon.TreeLeft) 387 | assert.Equal(suite.T(), 21, taxon.TreeRight) 388 | assert.Equal(suite.T(), 1, taxon.TreeLevel) 389 | count++ 390 | break 391 | case "MP3": 392 | assert.Equal(suite.T(), 11, taxon.TreeLeft) 393 | assert.Equal(suite.T(), 16, taxon.TreeRight) 394 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 395 | count++ 396 | break 397 | case "Flash": 398 | assert.Equal(suite.T(), 12, taxon.TreeLeft) 399 | assert.Equal(suite.T(), 13, taxon.TreeRight) 400 | assert.Equal(suite.T(), 3, taxon.TreeLevel) 401 | count++ 402 | break 403 | case "LCD": 404 | assert.Equal(suite.T(), 16, taxon.TreeLeft) 405 | assert.Equal(suite.T(), 17, taxon.TreeRight) 406 | assert.Equal(suite.T(), 3, taxon.TreeLevel) 407 | count++ 408 | break 409 | case "CD Player": 410 | assert.Equal(suite.T(), 17, taxon.TreeLeft) 411 | assert.Equal(suite.T(), 18, taxon.TreeRight) 412 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 413 | count++ 414 | break 415 | case "Radio": 416 | assert.Equal(suite.T(), 19, taxon.TreeLeft) 417 | assert.Equal(suite.T(), 20, taxon.TreeRight) 418 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 419 | count++ 420 | break 421 | } 422 | } 423 | 424 | assert.Equal(suite.T(), len(taxons), count) 425 | } 426 | 427 | func (suite *PluginTestSuite) TestChildNodeBecomesRoot() { 428 | suite.createTree() 429 | 430 | var mp3 Taxon 431 | 432 | assert.False(suite.T(), suite.db.First(&mp3, "name = 'MP3'").RecordNotFound()) 433 | 434 | mp3.Parent = nil 435 | mp3.ParentID = 0 436 | 437 | suite.db.Save(&mp3) 438 | 439 | var taxons []Taxon 440 | suite.db.Find(&taxons) 441 | 442 | assert.Len(suite.T(), taxons, 11) 443 | 444 | var count int 445 | for _, taxon := range taxons { 446 | switch taxon.Name { 447 | case "Electronics": 448 | assert.Equal(suite.T(), 1, taxon.TreeLeft) 449 | assert.Equal(suite.T(), 18, taxon.TreeRight) 450 | assert.Equal(suite.T(), 0, taxon.TreeLevel) 451 | count++ 452 | break 453 | case "Television": 454 | assert.Equal(suite.T(), 2, taxon.TreeLeft) 455 | assert.Equal(suite.T(), 9, taxon.TreeRight) 456 | assert.Equal(suite.T(), 1, taxon.TreeLevel) 457 | count++ 458 | break 459 | case "Game Consoles": 460 | assert.Equal(suite.T(), 10, taxon.TreeLeft) 461 | assert.Equal(suite.T(), 11, taxon.TreeRight) 462 | assert.Equal(suite.T(), 1, taxon.TreeLevel) 463 | count++ 464 | break 465 | case "Tube": 466 | assert.Equal(suite.T(), 3, taxon.TreeLeft) 467 | assert.Equal(suite.T(), 4, taxon.TreeRight) 468 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 469 | count++ 470 | break 471 | case "LCD": 472 | assert.Equal(suite.T(), 5, taxon.TreeLeft) 473 | assert.Equal(suite.T(), 6, taxon.TreeRight) 474 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 475 | count++ 476 | break 477 | case "Plasma": 478 | assert.Equal(suite.T(), 7, taxon.TreeLeft) 479 | assert.Equal(suite.T(), 8, taxon.TreeRight) 480 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 481 | count++ 482 | break 483 | case "Portable Electronics": 484 | assert.Equal(suite.T(), 12, taxon.TreeLeft) 485 | assert.Equal(suite.T(), 17, taxon.TreeRight) 486 | assert.Equal(suite.T(), 1, taxon.TreeLevel) 487 | count++ 488 | break 489 | case "CD Player": 490 | assert.Equal(suite.T(), 13, taxon.TreeLeft) 491 | assert.Equal(suite.T(), 14, taxon.TreeRight) 492 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 493 | count++ 494 | break 495 | case "Radio": 496 | assert.Equal(suite.T(), 15, taxon.TreeLeft) 497 | assert.Equal(suite.T(), 16, taxon.TreeRight) 498 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 499 | count++ 500 | break 501 | case "MP3": 502 | assert.Equal(suite.T(), 19, taxon.TreeLeft) 503 | assert.Equal(suite.T(), 22, taxon.TreeRight) 504 | assert.Equal(suite.T(), 0, taxon.TreeLevel) 505 | count++ 506 | break 507 | case "Flash": 508 | assert.Equal(suite.T(), 20, taxon.TreeLeft) 509 | assert.Equal(suite.T(), 21, taxon.TreeRight) 510 | assert.Equal(suite.T(), 1, taxon.TreeLevel) 511 | count++ 512 | break 513 | } 514 | } 515 | 516 | assert.Equal(suite.T(), len(taxons), count) 517 | } 518 | 519 | func (suite *PluginTestSuite) TestAutoUpdateParentAssociation() { 520 | electronics := Taxon{ 521 | Name: "Electronics", 522 | } 523 | 524 | television := Taxon{ 525 | Name: "Television", 526 | Parent: &electronics, 527 | } 528 | gameConsoles := Taxon{ 529 | Name: "Game Consoles", 530 | Parent: &electronics, 531 | } 532 | portableElectronics := Taxon{ 533 | Name: "Portable Electronics", 534 | Parent: &electronics, 535 | } 536 | 537 | tube := Taxon{ 538 | Name: "Tube", 539 | Parent: &television, 540 | } 541 | lcd := Taxon{ 542 | Name: "LCD", 543 | Parent: &television, 544 | } 545 | plasma := Taxon{ 546 | Name: "Plasma", 547 | Parent: &television, 548 | } 549 | 550 | mp3 := Taxon{ 551 | Name: "MP3", 552 | Parent: &portableElectronics, 553 | } 554 | 555 | cdPlayer := Taxon{ 556 | Name: "CD Player", 557 | Parent: &portableElectronics, 558 | } 559 | 560 | radio := Taxon{ 561 | Name: "Radio", 562 | Parent: &portableElectronics, 563 | } 564 | 565 | flash := Taxon{ 566 | Name: "Flash", 567 | Parent: &mp3, 568 | } 569 | 570 | suite.db.Save(&television) 571 | suite.db.Save(&gameConsoles) 572 | suite.db.Save(&tube) 573 | suite.db.Save(&lcd) 574 | suite.db.Save(&plasma) 575 | suite.db.Save(&flash) 576 | suite.db.Save(&cdPlayer) 577 | suite.db.Save(&radio) 578 | 579 | assert.Equal(suite.T(), 1, electronics.TreeLeft) 580 | assert.Equal(suite.T(), 22, electronics.TreeRight) 581 | assert.Equal(suite.T(), 0, electronics.TreeLevel) 582 | 583 | assert.Equal(suite.T(), 2, television.TreeLeft) 584 | assert.Equal(suite.T(), 9, television.TreeRight) 585 | assert.Equal(suite.T(), 1, television.TreeLevel) 586 | 587 | assert.Equal(suite.T(), 3, tube.TreeLeft) 588 | assert.Equal(suite.T(), 4, tube.TreeRight) 589 | assert.Equal(suite.T(), 2, tube.TreeLevel) 590 | 591 | assert.Equal(suite.T(), 5, lcd.TreeLeft) 592 | assert.Equal(suite.T(), 6, lcd.TreeRight) 593 | assert.Equal(suite.T(), 2, lcd.TreeLevel) 594 | 595 | assert.Equal(suite.T(), 7, plasma.TreeLeft) 596 | assert.Equal(suite.T(), 8, plasma.TreeRight) 597 | assert.Equal(suite.T(), 2, plasma.TreeLevel) 598 | 599 | //assert.Equal(suite.T(), 10, gameConsoles.TreeLeft) 600 | //assert.Equal(suite.T(), 11, gameConsoles.TreeRight) 601 | //assert.Equal(suite.T(), 1, gameConsoles.TreeLevel) 602 | 603 | assert.Equal(suite.T(), 12, portableElectronics.TreeLeft) 604 | assert.Equal(suite.T(), 21, portableElectronics.TreeRight) 605 | assert.Equal(suite.T(), 1, portableElectronics.TreeLevel) 606 | 607 | assert.Equal(suite.T(), 13, mp3.TreeLeft) 608 | assert.Equal(suite.T(), 16, mp3.TreeRight) 609 | assert.Equal(suite.T(), 2, mp3.TreeLevel) 610 | 611 | assert.Equal(suite.T(), 17, cdPlayer.TreeLeft) 612 | assert.Equal(suite.T(), 18, cdPlayer.TreeRight) 613 | assert.Equal(suite.T(), 2, cdPlayer.TreeLevel) 614 | 615 | assert.Equal(suite.T(), 19, radio.TreeLeft) 616 | assert.Equal(suite.T(), 20, radio.TreeRight) 617 | assert.Equal(suite.T(), 2, radio.TreeLevel) 618 | 619 | assert.Equal(suite.T(), 14, flash.TreeLeft) 620 | assert.Equal(suite.T(), 15, flash.TreeRight) 621 | assert.Equal(suite.T(), 3, flash.TreeLevel) 622 | } 623 | 624 | func (suite *PluginTestSuite) createTree() { 625 | electronics := Taxon{ 626 | Name: "Electronics", 627 | } 628 | 629 | television := Taxon{ 630 | Name: "Television", 631 | Parent: &electronics, 632 | } 633 | gameConsoles := Taxon{ 634 | Name: "Game Consoles", 635 | Parent: &electronics, 636 | } 637 | portableElectronics := Taxon{ 638 | Name: "Portable Electronics", 639 | Parent: &electronics, 640 | } 641 | 642 | tube := Taxon{ 643 | Name: "Tube", 644 | Parent: &television, 645 | } 646 | lcd := Taxon{ 647 | Name: "LCD", 648 | Parent: &television, 649 | } 650 | plasma := Taxon{ 651 | Name: "Plasma", 652 | Parent: &television, 653 | } 654 | 655 | mp3 := Taxon{ 656 | Name: "MP3", 657 | Parent: &portableElectronics, 658 | } 659 | 660 | cdPlayer := Taxon{ 661 | Name: "CD Player", 662 | Parent: &portableElectronics, 663 | } 664 | 665 | radio := Taxon{ 666 | Name: "Radio", 667 | Parent: &portableElectronics, 668 | } 669 | 670 | flash := Taxon{ 671 | Name: "Flash", 672 | Parent: &mp3, 673 | } 674 | 675 | suite.db.Save(&television) 676 | suite.db.Save(&gameConsoles) 677 | suite.db.Save(&tube) 678 | suite.db.Save(&lcd) 679 | suite.db.Save(&plasma) 680 | suite.db.Save(&flash) 681 | suite.db.Save(&cdPlayer) 682 | suite.db.Save(&radio) 683 | 684 | var taxons []Taxon 685 | suite.db.Find(&taxons) 686 | 687 | assert.Len(suite.T(), taxons, 11) 688 | var count int 689 | for _, taxon := range taxons { 690 | switch taxon.Name { 691 | case "Electronics": 692 | assert.Equal(suite.T(), 1, taxon.TreeLeft) 693 | assert.Equal(suite.T(), 22, taxon.TreeRight) 694 | assert.Equal(suite.T(), 0, taxon.TreeLevel) 695 | count++ 696 | break 697 | case "Television": 698 | assert.Equal(suite.T(), 2, taxon.TreeLeft) 699 | assert.Equal(suite.T(), 9, taxon.TreeRight) 700 | assert.Equal(suite.T(), 1, taxon.TreeLevel) 701 | count++ 702 | break 703 | case "Tube": 704 | assert.Equal(suite.T(), 3, taxon.TreeLeft) 705 | assert.Equal(suite.T(), 4, taxon.TreeRight) 706 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 707 | count++ 708 | case "LCD": 709 | assert.Equal(suite.T(), 5, taxon.TreeLeft) 710 | assert.Equal(suite.T(), 6, taxon.TreeRight) 711 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 712 | count++ 713 | break 714 | case "Plasma": 715 | assert.Equal(suite.T(), 7, taxon.TreeLeft) 716 | assert.Equal(suite.T(), 8, taxon.TreeRight) 717 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 718 | count++ 719 | break 720 | case "Game Consoles": 721 | assert.Equal(suite.T(), 10, taxon.TreeLeft) 722 | assert.Equal(suite.T(), 11, taxon.TreeRight) 723 | assert.Equal(suite.T(), 1, taxon.TreeLevel) 724 | count++ 725 | break 726 | case "Portable Electronics": 727 | assert.Equal(suite.T(), 12, taxon.TreeLeft) 728 | assert.Equal(suite.T(), 21, taxon.TreeRight) 729 | assert.Equal(suite.T(), 1, taxon.TreeLevel) 730 | count++ 731 | break 732 | case "MP3": 733 | assert.Equal(suite.T(), 13, taxon.TreeLeft) 734 | assert.Equal(suite.T(), 16, taxon.TreeRight) 735 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 736 | count++ 737 | break 738 | case "CD Player": 739 | assert.Equal(suite.T(), 17, taxon.TreeLeft) 740 | assert.Equal(suite.T(), 18, taxon.TreeRight) 741 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 742 | count++ 743 | break 744 | case "Radio": 745 | assert.Equal(suite.T(), 19, taxon.TreeLeft) 746 | assert.Equal(suite.T(), 20, taxon.TreeRight) 747 | assert.Equal(suite.T(), 2, taxon.TreeLevel) 748 | count++ 749 | break 750 | case "Flash": 751 | assert.Equal(suite.T(), 14, taxon.TreeLeft) 752 | assert.Equal(suite.T(), 15, taxon.TreeRight) 753 | assert.Equal(suite.T(), 3, taxon.TreeLevel) 754 | count++ 755 | break 756 | } 757 | } 758 | 759 | assert.Equal(suite.T(), len(taxons), count) 760 | } 761 | 762 | func (suite *PluginTestSuite) TestGetTreeLeft() { 763 | t := &Taxon{ 764 | TreeLeft: 41, 765 | } 766 | 767 | assert.Equal(suite.T(), 41, nested.GetTreeLeft(t)) 768 | } 769 | 770 | func (suite *PluginTestSuite) TestGetTreeRight() { 771 | t := &Taxon{ 772 | TreeRight: 41, 773 | } 774 | 775 | assert.Equal(suite.T(), 41, nested.GetTreeRight(t)) 776 | } 777 | 778 | func (suite *PluginTestSuite) TestGetTreeLevel() { 779 | t := &Taxon{ 780 | TreeLevel: 41, 781 | } 782 | 783 | assert.Equal(suite.T(), 41, nested.GetTreeLevel(t)) 784 | } 785 | 786 | func TestPluginTestSuite(t *testing.T) { 787 | suite.Run(t, new(PluginTestSuite)) 788 | } 789 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package nested 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func isZeroValue(v interface{}) bool { 8 | return reflect.ValueOf(v).Interface() == reflect.Zero(reflect.TypeOf(v)).Interface() 9 | } 10 | 11 | func newNodePtrFromValue(value interface{}) Interface { 12 | v := reflect.ValueOf(value) 13 | return reflect.New(reflect.TypeOf(reflect.Indirect(v).Interface())).Interface().(Interface) 14 | } 15 | 16 | func doubleToSingleIndirect(v interface{}) interface{} { 17 | iv := reflect.Indirect(reflect.ValueOf(v)) 18 | if iv.Kind() == reflect.Ptr { 19 | return iv.Interface() 20 | } 21 | 22 | return v 23 | } 24 | 25 | func isNilInterface(i interface{}) bool { 26 | if i == nil { 27 | return true 28 | } 29 | 30 | return reflect.ValueOf(i).IsNil() 31 | } 32 | --------------------------------------------------------------------------------