├── .circleci └── config.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── mpath.go └── mpath_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | 3 | orbs: 4 | codecov: codecov/codecov@1.0.4 5 | 6 | jobs: 7 | build: 8 | docker: 9 | - image: circleci/golang:1.12 10 | steps: 11 | - checkout 12 | - run: 13 | name: "Create tmp directory" 14 | command: | 15 | mkdir -p /tmp/artifacts 16 | - run: 17 | name: "Run tests and collect coverage reports" 18 | command: | 19 | go mod vendor 20 | go test ./... -coverprofile=c.out -v 21 | go tool cover -html=c.out -o coverage.html 22 | mv coverage.html /tmp/artifacts 23 | mv c.out /tmp/artifacts 24 | - store_artifacts: 25 | path: /tmp/artifacts 26 | - run: 27 | name: Upload Coverage Results 28 | command: "bash <(curl -s https://codecov.io/bash) \ 29 | -f /tmp/artifacts/* \ 30 | -n ${CIRCLE_BUILD_NUM} \ 31 | -t ${CODECOV_TOKEN} \ 32 | -y .codecov.yml" 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SpaceTab.io 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 | # mpath-go 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/spacetab-io/mpath-go)](https://goreportcard.com/report/github.com/spacetab-io/mpath-go) 4 | [![CircleCI](https://circleci.com/gh/spacetab-io/mpath-go.svg?style=shield)](https://circleci.com/gh/spacetab-io/mpath-go) 5 | [![codecov](https://codecov.io/gh/spacetab-io/mpath-go.svg/graph/badge.svg)](https://codecov.io/gh/spacetab-io/mpath-go) 6 | 7 | Golang realisation of MPTT (or modified preorder tree traversal) in materialized path way. 8 | 9 | ## About 10 | 11 | It provides interfaces which yor database object should implement. 12 | 13 | Your database object should store: 14 | * `path` property as slice of uint64 IDs of materialized path to this object in traversal tree; 15 | * `position` property as integer for determine the order of leafs in tree 16 | 17 | ## Usage 18 | 19 | Implementation example and tests are in [test file](/mpath_test.go). 20 | 21 | ```go 22 | package main 23 | 24 | import ( 25 | "fmt" 26 | 27 | "github.com/spacetab-io/mpath" 28 | ) 29 | 30 | type TestItems []*TestItem 31 | 32 | type TestItem struct { 33 | ID uint64 34 | Path []uint64 35 | Position int 36 | Siblings TestItems 37 | Name string 38 | } 39 | 40 | // Leaf interface implementation for TestItem 41 | // ... 42 | // Leafs interface implementation for TestItems 43 | // ... 44 | 45 | func main() { 46 | flatItemsSlice := getTestItems() 47 | 48 | var parent = TestItem{} 49 | if err := mpath.InitTree(&parent, flatItemsSlice); err != nil { 50 | panic("error tree init") 51 | } 52 | 53 | fmt.Print(parent) 54 | } 55 | 56 | func getTestItems() *TestItems { 57 | return &TestItems{ 58 | {ID: 1, Position: 0, Name: "item 1", Path: []uint64{1}}, 59 | {ID: 2, Position: 0, Name: "item 2", Path: []uint64{1, 2}}, 60 | {ID: 3, Position: 1, Name: "item 3", Path: []uint64{1, 3}}, 61 | {ID: 4, Position: 0, Name: "item 4", Path: []uint64{1, 2, 4}}, 62 | {ID: 5, Position: 1, Name: "item 5", Path: []uint64{1, 2, 5}}, 63 | {ID: 6, Position: 0, Name: "item 6", Path: []uint64{1, 3, 6}}, 64 | } 65 | } 66 | ``` 67 | 68 | ## Tests 69 | 70 | go test ./... -v -race 71 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/spacetab-io/mpath 2 | 3 | go 1.12 4 | 5 | require github.com/stretchr/testify v1.4.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 7 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 11 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 12 | -------------------------------------------------------------------------------- /mpath.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package mpath is golang realisation of MPTT (modified preorder tree traversal) in materialized path way. It includes 3 | interfaces with methods that objects should implement to make an ordered tree from a flat slice of them. 4 | */ 5 | package mpath 6 | 7 | import ( 8 | "fmt" 9 | ) 10 | 11 | //Leafs is Leaf collection interface 12 | type Leafs interface { 13 | 14 | //GetLeafs return Leafs slice 15 | GetLeafs() []Leaf 16 | } 17 | 18 | //Leaf interface for making tree from flat Leafs slice 19 | type Leaf interface { 20 | 21 | //GetID returns an uint64 ID of Leaf object 22 | GetID() uint64 23 | 24 | //SetID sets ID to an empty Leaf object 25 | SetID(id uint64) 26 | 27 | //GetPosition returns position of Leafs branch 28 | GetPosition() int 29 | 30 | //SetPosition sets position of Leaf in its branch 31 | SetPosition(pos int) 32 | 33 | //GetPath returns Leafs path as Leafs IDs slice 34 | GetPath() []uint64 35 | 36 | //GetPathFromIdx returns Leaf path chunk started from passed index of element in path slice 37 | GetPathFromIdx(index *int) []uint64 38 | 39 | //GetLeafByID returns Leaf from Leafs by its ID 40 | GetLeafByID(leafs Leafs, id uint64) Leaf 41 | 42 | //GetLeafOrMakeNew returns Leaf from Leafs by its id or makes new Leaf object with ID and Position property 43 | GetLeafOrMakeNew(leafs Leafs, id uint64, position int) Leaf 44 | 45 | //GetSiblings return Leaf siblings as Leafs 46 | GetSiblings() Leafs 47 | 48 | //AppendSiblings append Leafs siblings to current Leaf 49 | AppendSiblings(interface{}) 50 | 51 | //MakeRoot creates root Leaf to a Leafs tree and return path index of this root Leaf in Leafs path 52 | MakeRoot(leafs Leafs, leaf Leaf) (pathIndex *int) 53 | 54 | //GetRootPathIndex returns index of root Leaf id in Leaf path slice 55 | GetRootPathIndex() *int 56 | } 57 | 58 | //InitTree creates Leaf tree (index) from flat Leafs slice 59 | func InitTree(tree Leaf, leafs Leafs) error { 60 | for _, leaf := range leafs.GetLeafs() { 61 | if err := parsePath(tree, leafs, leaf); err != nil { 62 | return err 63 | } 64 | } 65 | return nil 66 | } 67 | 68 | func parsePath(index Leaf, items Leafs, leaf Leaf) error { 69 | rootPathIdx := index.MakeRoot(items, leaf) 70 | if rootPathIdx == nil { 71 | return fmt.Errorf("no root for path for itemID %d", leaf.GetID()) 72 | } 73 | 74 | itemRootID := leaf.GetPath()[*rootPathIdx] 75 | 76 | // pass child items adding because of wrong root element 77 | if index.GetID() != itemRootID { 78 | return nil 79 | } 80 | 81 | addSibling(index, items, leaf.GetPathFromIdx(rootPathIdx), leaf.GetPosition()) 82 | 83 | return nil 84 | } 85 | 86 | func addSibling(parent Leaf, leafs Leafs, path []uint64, position int) { 87 | if len(path) == 0 { 88 | return 89 | } 90 | 91 | index := getSiblingsIndex(parent, leafs, path[0], position) 92 | 93 | if len(path) > 1 { 94 | addSibling(index, leafs, path[1:], position) 95 | } 96 | } 97 | 98 | func getSiblingsIndex(parent Leaf, leafs Leafs, id uint64, position int) Leaf { 99 | for _, leaf := range parent.GetSiblings().GetLeafs() { 100 | if leaf.GetID() == id { 101 | return leaf 102 | } 103 | } 104 | 105 | parent.AppendSiblings(parent.GetLeafOrMakeNew(leafs, id, position)) 106 | 107 | return getSiblingsIndex(parent, leafs, id, position) 108 | } 109 | -------------------------------------------------------------------------------- /mpath_test.go: -------------------------------------------------------------------------------- 1 | package mpath 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type TestItem struct { 12 | ID uint64 13 | Path []uint64 14 | Position int 15 | Siblings TestItems 16 | Name string 17 | } 18 | 19 | type TestItems []*TestItem 20 | 21 | func (ii TestItems) GetLeafs() []Leaf { 22 | var items []Leaf 23 | for _, i := range ii { 24 | items = append(items, i) 25 | } 26 | 27 | return items 28 | } 29 | 30 | func (ti *TestItem) GetID() uint64 { 31 | return ti.ID 32 | } 33 | 34 | func (ti *TestItem) SetID(id uint64) { 35 | ti.ID = id 36 | } 37 | 38 | func (ti TestItem) GetPath() []uint64 { 39 | return ti.Path 40 | } 41 | 42 | func (ti TestItem) GetPathFromIdx(idx *int) []uint64 { 43 | return ti.Path[*idx+1:] 44 | } 45 | 46 | func (ti TestItem) GetPosition() int { 47 | return ti.Position 48 | } 49 | 50 | func (ti *TestItem) SetPosition(position int) { 51 | ti.Position = position 52 | } 53 | 54 | func (ti *TestItem) GetSiblings() Leafs { 55 | return ti.Siblings 56 | } 57 | 58 | func (ti *TestItem) AppendSiblings(index interface{}) { 59 | idx := index.(*TestItem) 60 | ti.Siblings = append(ti.Siblings, idx) 61 | } 62 | 63 | func (ti *TestItem) GetLeafByID(items Leafs, id uint64) Leaf { 64 | for _, item := range items.GetLeafs() { 65 | if item.GetID() == id { 66 | return item 67 | } 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (ti *TestItem) GetLeafOrMakeNew(items Leafs, id uint64, position int) Leaf { 74 | item := ti.GetLeafByID(items, id) 75 | 76 | if item == nil { 77 | item = &TestItem{ID: id, Position: position} 78 | } 79 | 80 | return item 81 | } 82 | 83 | func (ti *TestItem) MakeRoot(items Leafs, item Leaf) *int { 84 | if ti.GetID() != 0 { 85 | return ti.GetRootPathIndex() 86 | } 87 | for idx, id := range item.GetPath() { 88 | root := ti.GetLeafByID(items, id) 89 | if root != nil { 90 | *ti = *root.(*TestItem) 91 | return &idx 92 | } 93 | } 94 | 95 | return nil 96 | } 97 | 98 | func (ti *TestItem) GetRootPathIndex() *int { 99 | for idx, id := range ti.GetPath() { 100 | if id == ti.GetID() { 101 | return &idx 102 | } 103 | } 104 | 105 | return nil 106 | } 107 | 108 | var ( 109 | itemsFull = &TestItems{ 110 | {ID: 1, Position: 0, Name: "item 1", Path: []uint64{1}}, 111 | {ID: 2, Position: 0, Name: "item 2", Path: []uint64{1, 2}}, 112 | {ID: 3, Position: 1, Name: "item 3", Path: []uint64{1, 3}}, 113 | {ID: 4, Position: 0, Name: "item 4", Path: []uint64{1, 2, 4}}, 114 | {ID: 5, Position: 1, Name: "item 5", Path: []uint64{1, 2, 5}}, 115 | {ID: 6, Position: 0, Name: "item 6", Path: []uint64{1, 3, 6}}, 116 | } 117 | itemsPart = &TestItems{ 118 | {ID: 2, Position: 0, Name: "item 2", Path: []uint64{1, 2}}, 119 | {ID: 4, Position: 0, Name: "item 4", Path: []uint64{1, 2, 4}}, 120 | {ID: 5, Position: 1, Name: "item 5", Path: []uint64{1, 2, 5}}, 121 | {ID: 6, Position: 0, Name: "item 6", Path: []uint64{1, 3, 6}}, 122 | } 123 | itemsError = &TestItems{ 124 | {ID: 7, Position: 0, Name: "item 2", Path: []uint64{1, 2}}, 125 | {ID: 8, Position: 0, Name: "item 4", Path: []uint64{1, 2, 4}}, 126 | {ID: 9, Position: 0, Name: "item 6", Path: []uint64{1, 3, 6}}, 127 | } 128 | testTreeFull = &TestItem{ 129 | Position: 0, 130 | ID: 1, 131 | Name: "item 1", 132 | Path: []uint64{1}, 133 | Siblings: []*TestItem{ 134 | { 135 | Position: 0, 136 | ID: 2, 137 | Name: "item 2", 138 | Path: []uint64{1, 2}, 139 | Siblings: []*TestItem{ 140 | { 141 | Position: 0, 142 | ID: 4, 143 | Name: "item 4", 144 | Path: []uint64{1, 2, 4}, 145 | Siblings: nil, 146 | }, 147 | { 148 | Position: 1, 149 | ID: 5, 150 | Name: "item 5", 151 | Path: []uint64{1, 2, 5}, 152 | Siblings: nil, 153 | }, 154 | }, 155 | }, 156 | { 157 | Position: 1, 158 | ID: 3, 159 | Name: "item 3", 160 | Path: []uint64{1, 3}, 161 | Siblings: []*TestItem{ 162 | { 163 | Position: 0, 164 | ID: 6, 165 | Name: "item 6", 166 | Path: []uint64{1, 3, 6}, 167 | Siblings: nil, 168 | }, 169 | }, 170 | }, 171 | }, 172 | } 173 | testTreePart = &TestItem{ 174 | Position: 0, 175 | ID: 2, 176 | Name: "item 2", 177 | Path: []uint64{1, 2}, 178 | Siblings: []*TestItem{ 179 | { 180 | Position: 0, 181 | ID: 4, 182 | Name: "item 4", 183 | Path: []uint64{1, 2, 4}, 184 | Siblings: nil, 185 | }, 186 | { 187 | Position: 1, 188 | ID: 5, 189 | Name: "item 5", 190 | Path: []uint64{1, 2, 5}, 191 | Siblings: nil, 192 | }, 193 | }, 194 | } 195 | ) 196 | 197 | func TestInitTree(t *testing.T) { 198 | t.Parallel() 199 | t.Run("init tree full", func(t *testing.T) { 200 | var parent = TestItem{} 201 | err := InitTree(&parent, itemsFull) 202 | if !assert.NoError(t, err) { 203 | t.FailNow() 204 | } 205 | assert.Equal(t, testTreeFull, &parent) 206 | }) 207 | t.Run("init tree part", func(t *testing.T) { 208 | var parent = TestItem{} 209 | err := InitTree(&parent, itemsPart) 210 | if !assert.NoError(t, err) { 211 | t.FailNow() 212 | } 213 | assert.Equal(t, testTreePart, &parent) 214 | }) 215 | t.Run("init tree error", func(t *testing.T) { 216 | var index = TestItem{} 217 | err := InitTree(&index, itemsError) 218 | if !assert.Error(t, err) { 219 | t.FailNow() 220 | } 221 | }) 222 | } 223 | 224 | func ExampleInitTree() { 225 | itemsFull = &TestItems{ 226 | {ID: 1, Position: 0, Name: "item 1", Path: []uint64{1}}, 227 | {ID: 2, Position: 0, Name: "item 2", Path: []uint64{1, 2}}, 228 | {ID: 3, Position: 1, Name: "item 3", Path: []uint64{1, 3}}, 229 | {ID: 4, Position: 0, Name: "item 4", Path: []uint64{1, 2, 4}}, 230 | {ID: 5, Position: 1, Name: "item 5", Path: []uint64{1, 2, 5}}, 231 | {ID: 6, Position: 0, Name: "item 6", Path: []uint64{1, 3, 6}}, 232 | } 233 | 234 | var parent = TestItem{} 235 | if err := InitTree(&parent, itemsFull); err != nil { 236 | log.Fatalln(err) 237 | } 238 | 239 | fmt.Println("Tree view:") 240 | showResult(parent, "") 241 | // Output: 242 | // Tree view: 243 | // id: 1, name: item 1, position: 0 244 | // siblings: 245 | // id: 2, name: item 2, position: 0 246 | // siblings: 247 | // id: 4, name: item 4, position: 0 248 | // siblings: 249 | // id: 5, name: item 5, position: 1 250 | // siblings: 251 | // id: 3, name: item 3, position: 1 252 | // siblings: 253 | // id: 6, name: item 6, position: 0 254 | // siblings: 255 | } 256 | 257 | func showResult(parent TestItem, padding string) { 258 | padding += " " 259 | fmt.Printf("%sid: %d, name: %s, position: %d\n", padding, parent.ID, parent.Name, parent.Position) 260 | fmt.Printf("%ssiblings:\n", padding) 261 | for _, ti := range parent.Siblings { 262 | showResult(*ti, padding) 263 | } 264 | } 265 | --------------------------------------------------------------------------------