diff --git a/.github/docker-compose.yaml b/.github/docker-compose.yaml new file mode 100644 index 0000000..3583ccc --- /dev/null +++ b/.github/docker-compose.yaml @@ -0,0 +1,29 @@ +# Copyright (c) 2019 MindStand Technologies, Inc +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +version: "3.6" +services: + neo: + image: neo4j:enterprise + environment: + - NEO4J_ACCEPT_LICENSE_AGREEMENT=yes + - NEO4J_AUTH=neo4j/password + ports: + - "7474:7474" + - "7687:7687" \ No newline at end of file diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 18bb488..a69e051 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -1,3 +1,22 @@ +# Copyright (c) 2019 MindStand Technologies, Inc +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + name: Go on: [push] jobs: @@ -6,16 +25,13 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - name: Set up Go 1.13 uses: actions/setup-go@v1 with: go-version: 1.13 id: go - - name: Check out code into the Go module directory uses: actions/checkout@v1 - - name: Get dependencies run: | go get -v -t -d ./... @@ -23,8 +39,19 @@ jobs: curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh dep ensure fi - - name: Test - run: go test ./... - - name: Build run: go build -v . + - name: Run Unit Tests + run: go test ./... -short + - name: Start Neo4j Docker + run: | + docker-compose -f .github/docker-compose.yaml up -d + - name: Wait for neo4j to be ready + run: | + sleep 5 + - name: Run Integration Test + run: go test ./... -run Integration + - name: Stop Neo4j Docker + run: | + docker-compose -f .github/docker-compose.yaml down + diff --git a/README.md b/README.md index 7c13889..b248993 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/mindstand/gogm)](https://goreportcard.com/report/github.com/mindstand/gogm) [![Actions Status](https://github.com/mindstand/gogm/workflows/Go/badge.svg)](https://github.com/mindstand/gogm/actions) +[![](https://godoc.org/github.com/nathany/looper?status.svg)](http://godoc.org/github.com/mindstand/gogm) # GoGM Golang Object Graph Mapper ``` @@ -15,6 +16,7 @@ go get -u github.com/mindstand/gogm - Support for HA Clusters using `bolt+routing` through [MindStand's fork](https://github.com/mindstand/golang-neo4j-bolt-driver) of [@johnnadratowski's golang bolt driver](https://github.com/johnnadratowski/golang-neo4j-bolt-driver) - Custom queries in addition to built in functionality - Builder pattern cypher queries using [MindStand's cypher dsl package](https://github.com/mindstand/go-cypherdsl) +- CLI to generate link and unlink functions for gogm structs. ## Usage @@ -160,20 +162,42 @@ func main(){ ``` +### GoGM CLI + +## CLI Installation +``` +go get -u github.com/mindstand/gogm/cli/gogmcli +``` + +## CLI Usage +``` +NAME: + gogmcli - used for neo4j operations from gogm schema + +USAGE: + gogmcli [global options] command [command options] [arguments...] + +VERSION: + 1.0.0 + +COMMANDS: + generate, g, gen to generate link and unlink functions for nodes + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + --debug, -d execute in debug mode (default: false) + --help, -h show help (default: false) + --version, -v print the version (default: false) +``` + ## Inspiration Inspiration came from the Java OGM implementation by Neo4j. ## Road Map -- More validation (refer to issues #2, #8) - Schema Migration -- Generation CLI for link functions - Errors overhaul using go 1.13 error wrapping - TLS Support - Documentation (obviously) -- More to come as we find more bugs! - -## Credits -- [adam hannah's arrayOperations](https://github.com/adam-hanna/arrayOperations) ## How you can help - Report Bugs diff --git a/cmd/gogmcli/gen/gen.go b/cmd/gogmcli/gen/gen.go new file mode 100644 index 0000000..1af11f2 --- /dev/null +++ b/cmd/gogmcli/gen/gen.go @@ -0,0 +1,277 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +// gen provides code to generate link and unlink functions for gogm structs +package gen + +import ( + "bytes" + "errors" + "fmt" + dsl "github.com/mindstand/go-cypherdsl" + "github.com/mindstand/gogm/cmd/gogmcli/util" + "html/template" + "log" + "os" + "path" + "path/filepath" + "strings" +) + +// Generate searches for all go source files, then generates link and unlink functions for all gogm structs +// takes in root directory and whether to log in debug mode +// note: Generate is not recursive, it only looks in the target directory +func Generate(directory string, debug bool) error { + confs := map[string][]*relConf{} + imps := map[string][]string{} + var edges []string + packageName := "" + + err := filepath.Walk(directory, func(filePath string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info == nil { + return errors.New("file info is nil") + } + + if info.IsDir() && filePath != directory { + if debug { + log.Printf("skipping [%s] as it is a directory\n", filePath) + } + return filepath.SkipDir + } + + if path.Ext(filePath) == ".go" { + if debug { + log.Printf("parsing go file [%s]\n", filePath) + } + err := parseFile(filePath, &confs, &edges, imps, &packageName) + if err != nil { + if debug { + log.Printf("failed to parse go file [%s] with error '%s'\n", filePath, err.Error()) + } + return err + } + if debug { + log.Printf("successfully parsed go file [%s]\n", filePath) + } + } else if debug { + log.Printf("skipping non go file [%s]\n", filePath) + } + + return nil + }) + if err != nil { + return err + } + + var imports []string + + for _, imp := range imps { + imports = append(imports, imp...) + } + + imports = util.RemoveDuplicates(imports) + + for i := 0; i < len(imports); i++ { + imports[i] = strings.Replace(imports[i], "\"", "", -1) + } + + relations := make(map[string][]*relConf) + + // sort out relationships + for _, fields := range confs { + for _, field := range fields { + if field == nil { + return errors.New("field can not be nil") + } + + if _, ok := relations[field.RelationshipName]; ok { + relations[field.RelationshipName] = append(relations[field.RelationshipName], field) + } else { + relations[field.RelationshipName] = []*relConf{field} + } + } + } + + // validate relationships (i.e even number) + for name, rel := range relations { + if len(rel)%2 != 0 { + return fmt.Errorf("relationship [%s] is invalid", name) + } + } + + funcs := make(map[string][]*tplRelConf) + + // set template stuff + for _, rels := range relations { + for _, rel := range rels { + tplRel := &tplRelConf{ + StructName: rel.NodeName, + StructField: rel.Field, + OtherStructName: rel.Type, + StructFieldIsMany: rel.IsMany, + } + + var isSpecialEdge bool + + if util.StringSliceContains(edges, rel.Type) { + tplRel.UsesSpecialEdge = true + tplRel.SpecialEdgeType = rel.Type + tplRel.SpecialEdgeDirection = rel.Direction == dsl.DirectionIncoming + isSpecialEdge = true + } + + err = parseDirection(rel, rels, tplRel, isSpecialEdge) + if err != nil { + return err + } + + if tplRel.OtherStructField == "" { + return fmt.Errorf("oposite side not found for node [%s]", rel.NodeName) + } + + if _, ok := funcs[rel.NodeName]; ok { + funcs[rel.NodeName] = append(funcs[rel.NodeName], tplRel) + } else { + funcs[rel.NodeName] = []*tplRelConf{tplRel} + } + } + } + + //write templates out + tpl := template.New("linkFile") + + //register templates + for _, templateString := range []string{singleLink, linkMany, linkSpec, unlinkSingle, unlinkMulti, unlinkSpec, masterTpl} { + tpl, err = tpl.Parse(templateString) + if err != nil { + return err + } + } + + if debug { + log.Printf("packageName: [%s]\n", packageName) + } + + if len(funcs) == 0 { + log.Printf("no functions to write, exiting") + return nil + } + + buf := new(bytes.Buffer) + err = tpl.Execute(buf, templateConfig{ + Imports: imports, + PackageName: packageName, + Funcs: funcs, + }) + if err != nil { + return err + } + + f, err := os.Create(path.Join(directory, "linking.go")) + if err != nil { + return err + } + + lenBytes, err := f.Write(buf.Bytes()) + if err != nil { + return err + } + + if debug { + log.Printf("done after writing [%v] bytes!", lenBytes) + } + + err = f.Close() + if err != nil { + return err + } + + log.Printf("wrote link functions to file [%s/linking.go]", directory) + + return nil +} + +// parseDirection parses gogm struct tags and writes to a holder struct +func parseDirection(rel *relConf, rels []*relConf, tplRel *tplRelConf, isSpecialEdge bool) error { + for _, lookup := range rels { + //check special edge + if rel.Type != lookup.NodeName && !isSpecialEdge { + continue + } + + switch rel.Direction { + case dsl.DirectionOutgoing: + if lookup.Direction == dsl.DirectionIncoming { + tplRel.OtherStructField = lookup.Field + tplRel.OtherStructFieldIsMany = lookup.IsMany + if isSpecialEdge { + tplRel.OtherStructName = lookup.NodeName + } + return nil + } else { + continue + } + + case dsl.DirectionIncoming: + if lookup.Direction == dsl.DirectionOutgoing { + tplRel.OtherStructField = lookup.Field + tplRel.OtherStructFieldIsMany = lookup.IsMany + if isSpecialEdge { + tplRel.OtherStructName = lookup.NodeName + } + return nil + } else { + continue + } + + case dsl.DirectionNone: + if lookup.Direction == dsl.DirectionNone { + tplRel.OtherStructField = lookup.Field + tplRel.OtherStructFieldIsMany = lookup.IsMany + if isSpecialEdge { + tplRel.OtherStructName = lookup.NodeName + } + return nil + } else { + continue + } + + case dsl.DirectionBoth: + if lookup.Direction == dsl.DirectionBoth { + tplRel.OtherStructField = lookup.Field + tplRel.OtherStructFieldIsMany = lookup.IsMany + if isSpecialEdge { + tplRel.OtherStructName = lookup.NodeName + } + return nil + } else { + continue + } + + default: + return fmt.Errorf("invalid direction [%v]", rel.Direction) + } + } + + return nil +} diff --git a/cmd/gogmcli/gen/parse.go b/cmd/gogmcli/gen/parse.go new file mode 100644 index 0000000..3ab8e4b --- /dev/null +++ b/cmd/gogmcli/gen/parse.go @@ -0,0 +1,238 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gen + +import ( + "bytes" + "errors" + go_cypherdsl "github.com/mindstand/go-cypherdsl" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "log" + "strings" +) + +type relConf struct { + NodeName string + Field string + RelationshipName string + Type string + IsMany bool + Direction go_cypherdsl.Direction +} + +// parses each file using ast looking for nodes to handle +func parseFile(filePath string, confs *map[string][]*relConf, edges *[]string, imports map[string][]string, packageName *string) error { + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments) + if err != nil { + log.Fatal(err) + } + + if node.Scope != nil { + *packageName = node.Name.Name + if node.Scope.Objects != nil && len(node.Scope.Objects) != 0 { + for label, config := range node.Scope.Objects { + tSpec, ok := config.Decl.(*ast.TypeSpec) + if !ok { + continue + } + + strType, ok := tSpec.Type.(*ast.StructType) + if !ok { + continue + } + + if node.Imports != nil && len(node.Imports) != 0 { + var imps []string + + for _, impSpec := range node.Imports { + imps = append(imps, impSpec.Path.Value) + } + + imports[label] = imps + } + + //check if its a special edge + isEdge, err := parseGogmEdge(node, label) + if err != nil { + return err + } + + // if its not an edge, parse it as a gogm struct + if !isEdge { + (*confs)[label] = []*relConf{} + err = parseGogmNode(strType, confs, label, fset) + if err != nil { + return err + } + } else { + *edges = append(*edges, label) + } + + } + } + } + + return nil +} + +//parseGogmEdge: checks if node implements `IEdge` +func parseGogmEdge(node *ast.File, label string) (bool, error) { + if node == nil { + return false, errors.New("node can not be nil") + } + + var GetStartNode, GetStartNodeType, SetStartNode, GetEndNode, GetEndNodeType, SetEndNode bool + + for _, decl := range node.Decls { + funcDecl, ok := decl.(*ast.FuncDecl) + if !ok { + continue + } + + if funcDecl != nil { + if funcDecl.Recv != nil { + if funcDecl.Recv.List != nil { + if len(funcDecl.Recv.List) != 0 { + if len(funcDecl.Recv.List[0].Names) != 0 { + decl, ok := funcDecl.Recv.List[0].Names[0].Obj.Decl.(*ast.Field) + if !ok { + continue + } + + startType, ok := decl.Type.(*ast.StarExpr) + if !ok { + continue + } + + x, ok := startType.X.(*ast.Ident) + if !ok { + continue + } + + //check that the function is the right type + if x.Name != label { + continue + } + } + } else { + continue + } + + switch funcDecl.Name.Name { + case "GetStartNode": + GetStartNode = true + break + case "GetStartNodeType": + GetStartNodeType = true + break + case "SetStartNode": + SetStartNode = true + break + case "GetEndNode": + GetEndNode = true + break + case "GetEndNodeType": + GetEndNodeType = true + break + case "SetEndNode": + SetEndNode = true + break + default: + continue + } + } + } + } + } + //check if its an edge node + return !GetStartNode || !GetStartNodeType || !SetStartNode || !GetEndNode || !GetEndNodeType || !SetEndNode, nil +} + +// parseGogmNode generates configuration for GoGM struct +func parseGogmNode(strType *ast.StructType, confs *map[string][]*relConf, label string, fset *token.FileSet) error { + if strType.Fields != nil && strType.Fields.List != nil && len(strType.Fields.List) != 0 { + fieldLoop: + for _, field := range strType.Fields.List { + if field.Tag != nil && field.Tag.Value != "" { + parts := strings.Split(field.Tag.Value, " ") + for _, part := range parts { + if !strings.Contains(part, "gogm") { + continue + } + part = strings.Replace(strings.Replace(part, "`gogm:", "", -1), "\"", "", -1) + if strings.Contains(part, "relationship") && strings.Contains(part, "direction") { + gogmParts := strings.Split(part, ";") + + var dir go_cypherdsl.Direction + var relName string + for _, p := range gogmParts { + if strings.Contains(p, "direction") { + str := strings.ToLower(strings.Replace(strings.Replace(strings.Replace(p, "direction=", "", -1), "\"", "", -1), "`", "", -1)) + switch str { + case "incoming": + dir = go_cypherdsl.DirectionIncoming + break + case "outgoing": + dir = go_cypherdsl.DirectionOutgoing + break + case "both": + dir = go_cypherdsl.DirectionBoth + break + case "none": + dir = go_cypherdsl.DirectionNone + break + default: + log.Printf("direction %s not found", str) + continue fieldLoop + } + } else if strings.Contains(part, "relationship") { + relName = strings.ToLower(strings.Replace(strings.Replace(p, "relationship=", "", -1), "\"", "", -1)) + } + } + + var typeNameBuf bytes.Buffer + + err := printer.Fprint(&typeNameBuf, fset, field.Type) + if err != nil { + log.Fatal(err) + } + + t := typeNameBuf.String() + + (*confs)[label] = append((*confs)[label], &relConf{ + Field: field.Names[0].Name, + RelationshipName: relName, + Type: strings.Replace(strings.Replace(t, "[]", "", -1), "*", "", -1), + IsMany: strings.Contains(t, "[]"), + Direction: dir, + NodeName: label, + }) + } + } + } + } + } + + return nil +} diff --git a/cmd/gogmcli/gen/templ.go b/cmd/gogmcli/gen/templ.go new file mode 100644 index 0000000..dd158df --- /dev/null +++ b/cmd/gogmcli/gen/templ.go @@ -0,0 +1,295 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gen + +//expect .StructName .OtherStructName .StructField .OtherStructField .StructFieldIsMany .OtherStructFieldIsMany +var linkSpec = ` +{{ define "linkSpec" }} +func(l *{{ .StructName }}) LinkTo{{ .OtherStructName }}OnField{{.StructField}}(target *{{ .OtherStructName }}, edge *{{.SpecialEdgeType}}) error { + if target == nil { + return errors.New("start and end can not be nil") + } + + if edge == nil { + return errors.New("edge can not be nil") + } + {{ if .SpecialEdgeDirection }} + err := edge.SetStartNode(l) + if err != nil { + return err + } + + err = edge.SetEndNode(target) + if err != nil { + return err + }{{ else }} + err := edge.SetStartNode(target) + if err != nil { + return err + } + + err = edge.SetEndNode(l) + if err != nil { + return err + }{{ end }} + {{if .StructFieldIsMany }} + if l.{{ .StructField }} == nil { + l.{{ .StructField }} = make([]*{{ .SpecialEdgeType }}, 1, 1) + l.{{ .StructField }}[0] = edge + } else { + l.{{ .StructField }} = append(l.{{ .StructField }}, edge) + }{{ else }} + l.{{ .StructField }} = edge{{ end }} + {{if .OtherStructFieldIsMany }} + if target.{{ .OtherStructField }} == nil { + target.{{ .OtherStructField }} = make([]*{{ .SpecialEdgeType }}, 1, 1) + target.{{ .OtherStructField }}[0] = edge + } else { + target.{{ .OtherStructField }} = append(target.{{ .OtherStructField }}, edge) + }{{ else }} + target.{{ .OtherStructField }} = edge{{ end }} + + return nil +}{{ end }} +` + +var singleLink = ` +{{ define "linkSingle" }}func(l *{{ .StructName }}) LinkTo{{ .OtherStructName }}OnField{{.StructField}}(target *{{ .OtherStructName }}) error { + if target == nil { + return errors.New("start and end can not be nil") + } + {{if .StructFieldIsMany }} + if l.{{ .StructField }} == nil { + l.{{ .StructField }} = make([]*{{ .OtherStructName }}, 1, 1) + l.{{ .StructField }}[0] = target + } else { + l.{{ .StructField }} = append(l.{{ .StructField }}, target) + }{{ else }} + l.{{ .StructField }} = target{{ end }} + {{if .OtherStructFieldIsMany }} + if target.{{ .OtherStructField }} == nil { + target.{{ .OtherStructField }} = make([]*{{ .StructName }}, 1, 1) + target.{{ .OtherStructField }}[0] = l + } else { + target.{{ .OtherStructField }} = append(target.{{ .OtherStructField }}, l) + }{{ else }} + target.{{ .OtherStructField }} = l{{ end }} + + return nil +}{{ end }} +` + +var linkMany = ` +{{ define "linkMany" }} +func(l *{{ .StructName }}) LinkTo{{ .OtherStructName }}OnField{{.StructField}}(targets ...*{{ .OtherStructName }}) error { + if targets == nil { + return errors.New("start and end can not be nil") + } + + for _, target := range targets { + {{if .StructFieldIsMany }} + if l.{{ .StructField }} == nil { + l.{{ .StructField }} = make([]*{{ .OtherStructName }}, 1, 1) + l.{{ .StructField }}[0] = target + } else { + l.{{ .StructField }} = append(l.{{ .StructField }}, target) + }{{ else }} + l.{{ .StructField }} = target{{ end }} + {{if .OtherStructFieldIsMany }} + if target.{{ .OtherStructField }} == nil { + target.{{ .OtherStructField }} = make([]*{{ .StructName }}, 1, 1) + target.{{ .OtherStructField }}[0] = l + } else { + target.{{ .OtherStructField }} = append(target.{{ .OtherStructField }}, l) + }{{ else }} + target.{{ .OtherStructField }} = l{{ end }} + } + + return nil +}{{ end }} +` + +var unlinkSingle = ` +{{ define "unlinkSingle" }}func(l *{{ .StructName }}) UnlinkFrom{{ .OtherStructName }}OnField{{.StructField}}(target *{{ .OtherStructName }}) error { + if target == nil { + return errors.New("start and end can not be nil") + } + {{if .StructFieldIsMany }} + if l.{{ .StructField }} != nil { + for i, unlinkTarget := range l.{{ .StructField }} { + if unlinkTarget.UUID == target.UUID { + a := &l.{{ .StructField }} + (*a)[i] = (*a)[len(*a)-1] + (*a)[len(*a)-1] = nil + *a = (*a)[:len(*a)-1] + break + } + } + }{{ else }} + l.{{ .StructField }} = nil{{ end }} + {{if .OtherStructFieldIsMany }} + if target.{{ .OtherStructField }} != nil { + for i, unlinkTarget := range target.{{ .OtherStructField }} { + if unlinkTarget.UUID == l.UUID { + a := &target.{{ .OtherStructField }} + (*a)[i] = (*a)[len(*a)-1] + (*a)[len(*a)-1] = nil + *a = (*a)[:len(*a)-1] + break + } + } + }{{ else }} + target.{{ .OtherStructField }} = nil{{ end }} + + return nil +}{{ end }} +` + +var unlinkMulti = ` +{{ define "unlinkMulti" }}func(l *{{ .StructName }}) UnlinkFrom{{ .OtherStructName }}OnField{{.StructField}}(targets ...*{{ .OtherStructName }}) error { + if targets == nil { + return errors.New("start and end can not be nil") + } + + for _, target := range targets { + {{if .StructFieldIsMany }} + if l.{{ .StructField }} != nil { + for i, unlinkTarget := range l.{{ .StructField }} { + if unlinkTarget.UUID == target.UUID { + a := &l.{{ .StructField }} + (*a)[i] = (*a)[len(*a)-1] + (*a)[len(*a)-1] = nil + *a = (*a)[:len(*a)-1] + break + } + } + }{{ else }} + l.{{ .StructField }} = nil{{ end }} + {{if .OtherStructFieldIsMany }} + if target.{{ .OtherStructField }} != nil { + for i, unlinkTarget := range target.{{ .OtherStructField }} { + if unlinkTarget.UUID == l.UUID { + a := &target.{{ .OtherStructField }} + (*a)[i] = (*a)[len(*a)-1] + (*a)[len(*a)-1] = nil + *a = (*a)[:len(*a)-1] + break + } + } + }{{ else }} + target.{{ .OtherStructField }} = nil{{ end }} + } + + return nil +}{{ end }} +` + +var unlinkSpec = ` +{{ define "unlinkSpec" }}func(l *{{ .StructName }}) UnlinkFrom{{ .OtherStructName }}OnField{{.StructField}}(target *{{ .OtherStructName }}) error { + if target == nil { + return errors.New("start and end can not be nil") + } + {{if .StructFieldIsMany }} + if l.{{ .StructField }} != nil { + for i, unlinkTarget := range l.{{ .StructField }} { + {{ if .SpecialEdgeDirection }} + obj := unlinkTarget.GetStartNode(){{ else }} + obj := unlinkTarget.GetEndNode(){{end}} + + checkObj, ok := obj.(*{{ .OtherStructName }}) + if !ok { + return errors.New("unable to cast unlinkTarget to [{{ .OtherStructName }}]") + } + if checkObj.UUID == target.UUID { + a := &l.{{ .StructField }} + (*a)[i] = (*a)[len(*a)-1] + (*a)[len(*a)-1] = nil + *a = (*a)[:len(*a)-1] + break + } + } + }{{ else }} + l.{{ .StructField }} = nil{{ end }} + {{if .OtherStructFieldIsMany }} + if target.{{ .OtherStructField }} != nil { + for i, unlinkTarget := range target.{{ .OtherStructField }} { + {{ if .SpecialEdgeDirection }} + obj := unlinkTarget.GetStartNode(){{ else }} + obj := unlinkTarget.GetEndNode(){{end}} + + checkObj, ok := obj.(*{{ .StructName }}) + if !ok { + return errors.New("unable to cast unlinkTarget to [{{ .StructName }}]") + } + if checkObj.UUID == l.UUID { + a := &target.{{ .OtherStructField }} + (*a)[i] = (*a)[len(*a)-1] + (*a)[len(*a)-1] = nil + *a = (*a)[:len(*a)-1] + break + } + } + }{{ else }} + target.{{ .OtherStructField }} = nil{{ end }} + + return nil +}{{ end }} +` + +var masterTpl = ` +{{ define "linkFile" }}// code generated by gogm; DO NOT EDIT +package {{ .PackageName }} + +import ( + "errors" +) +{{range $key, $val := .Funcs}}{{range $val}} {{ if .UsesSpecialEdge }} +{{ template "linkSpec" . }} + +{{ template "unlinkSpec" . }}{{ else if .StructFieldIsMany}} +{{template "linkMany" .}} + +{{ template "unlinkMulti" .}}{{ else }} +{{ template "linkSingle" .}} + +{{ template "unlinkSingle" . }}{{end}} {{end}} {{end}} {{ end }} +` + +type templateConfig struct { + Imports []string + PackageName string + // type: funcs + Funcs map[string][]*tplRelConf +} + +type tplRelConf struct { + StructName string + StructField string + OtherStructField string + OtherStructName string + StructFieldIsMany bool + OtherStructFieldIsMany bool + + //stuff for special edges + UsesSpecialEdge bool + SpecialEdgeType string + // StructName = Start if true + SpecialEdgeDirection bool +} diff --git a/cmd/gogmcli/gogm.go b/cmd/gogmcli/gogm.go new file mode 100644 index 0000000..d7f6e2e --- /dev/null +++ b/cmd/gogmcli/gogm.go @@ -0,0 +1,92 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package main + +import ( + "errors" + "github.com/mindstand/gogm/cmd/gogmcli/gen" + "github.com/urfave/cli/v2" + "log" + "os" +) + +//main is the main function +func main() { + var debug bool + + app := &cli.App{ + Name: "gogmcli", + HelpName: "gogmcli", + Version: "1.0.0", + Usage: "used for neo4j operations from gogm schema", + Description: "cli for generating and executing migrations with gogm", + EnableBashCompletion: true, + Commands: []*cli.Command{ + { + Name: "generate", + Aliases: []string{ + "g", + "gen", + }, + ArgsUsage: "directory to search and write to", + Usage: "to generate link and unlink functions for nodes", + Action: func(c *cli.Context) error { + directory := c.Args().Get(0) + + if directory == "" { + return errors.New("must specify directory") + } + + if debug { + log.Printf("generating link and unlink from directory [%s]", directory) + } + + return gen.Generate(directory, debug) + }, + }, + }, + Authors: []*cli.Author{ + { + Name: "Eric Solender", + Email: "eric@mindstand.com", + }, + { + Name: "Nikita Wootten", + Email: "nikita@mindstand.com", + }, + }, + Copyright: "© MindStand Technologies, Inc 2019", + UseShortOptionHandling: true, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + Aliases: []string{"d"}, + Usage: "execute in debug mode", + Value: false, + Destination: &debug, + }, + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} diff --git a/cmd/gogmcli/util/util.go b/cmd/gogmcli/util/util.go new file mode 100644 index 0000000..051158f --- /dev/null +++ b/cmd/gogmcli/util/util.go @@ -0,0 +1,48 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package util + +// RemoveDuplicates removes duplicates from string slice +func RemoveDuplicates(s []string) []string { + if s == nil { + return []string{} + } + + seen := make(map[string]struct{}, len(s)) + j := 0 + for _, v := range s { + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + s[j] = v + j++ + } + return s[:j] +} + +func StringSliceContains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} diff --git a/setup.go b/config.go similarity index 57% rename from setup.go rename to config.go index 4f447d2..684a23e 100644 --- a/setup.go +++ b/config.go @@ -1,3 +1,22 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm import ( @@ -24,6 +43,7 @@ func getLogger() *logrus.Entry { return externalLog } +// SetLogger sets logrus logger func SetLogger(logger *logrus.Entry) error { if logger == nil { return errors.New("logger can not be nil") @@ -32,20 +52,29 @@ func SetLogger(logger *logrus.Entry) error { return nil } +// Config Defined GoGM config type Config struct { + // Host is the neo4j host Host string `yaml:"host" json:"host"` - Port int `yaml:"port" json:"port"` + // Port is the neo4j port + Port int `yaml:"port" json:"port"` + // IsCluster specifies whether GoGM is connecting to a casual cluster or not IsCluster bool `yaml:"is_cluster" json:"is_cluster"` + // Username is the GoGM username Username string `yaml:"username" json:"username"` + // Password is the GoGM password Password string `yaml:"password" json:"password"` + // PoolSize is the size of the connection pool for GoGM PoolSize int `yaml:"pool_size" json:"pool_size"` + // Index Strategy defines the index strategy for GoGM IndexStrategy IndexStrategy `yaml:"index_strategy" json:"index_strategy"` } +// ConnectionString builds the neo4j bolt/bolt+routing connection string func (c *Config) ConnectionString() string { var protocol string @@ -58,15 +87,19 @@ func (c *Config) ConnectionString() string { return fmt.Sprintf("%s://%s:%s@%s:%v", protocol, c.Username, c.Password, c.Host, c.Port) } +// Index Strategy typedefs int to define different index approaches type IndexStrategy int const ( - ASSERT_INDEX IndexStrategy = 0 + // Assert Index ensures that all indices are set and sets them if they are not there + ASSERT_INDEX IndexStrategy = 0 + // Validate Index ensures that all indices are set VALIDATE_INDEX IndexStrategy = 1 - IGNORE_INDEX IndexStrategy = 2 + // Ignore Index skips the index step of setup + IGNORE_INDEX IndexStrategy = 2 ) -//convert these into concurrent hashmap +//holds mapped types var mappedTypes = &hashmap.HashMap{} //thread pool @@ -81,14 +114,27 @@ func makeRelMapKey(start, edge, direction, rel string) string { var isSetup = false +// Init sets up gogm. Takes in config object and variadic slice of gogm nodes to map. +// Note: Must pass pointers to nodes! func Init(conf *Config, mapTypes ...interface{}) error { return setupInit(false, conf, mapTypes...) } +// Resets GoGM configuration +func Reset() { + mappedTypes = &hashmap.HashMap{} + mappedRelations = &relationConfigs{} + isSetup = false +} + +// internal setup logic for gogm func setupInit(isTest bool, conf *Config, mapTypes ...interface{}) error { if isSetup && !isTest { return errors.New("gogm has already been initialized") + } else if isTest && isSetup { + mappedRelations = &relationConfigs{} } + if !isTest { if conf == nil { return errors.New("config can not be nil") @@ -103,12 +149,16 @@ func setupInit(isTest bool, conf *Config, mapTypes ...interface{}) error { return err } - log.Debugf("mapped type '%s'", name) - log.Infof("mapped type %s", name) mappedTypes.Set(name, *dc) } + log.Debug("validating edges") + if err := mappedRelations.Validate(); err != nil { + log.WithError(err).Error("failed to validate edges") + return err + } + if !isTest { log.Debug("opening connection to neo4j") diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..5c718d6 --- /dev/null +++ b/config_test.go @@ -0,0 +1,20 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gogm diff --git a/decoder.go b/decoder.go index c15e608..8379420 100644 --- a/decoder.go +++ b/decoder.go @@ -1,3 +1,22 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm import ( @@ -11,6 +30,7 @@ import ( "time" ) +// decodes neo4j rows and writes the response to generic interface func decodeNeoRows(rows neo.Rows, respObj interface{}) error { defer rows.Close() @@ -22,8 +42,8 @@ func decodeNeoRows(rows neo.Rows, respObj interface{}) error { return decode(arr, respObj) } -//example query `match p=(n)-[*0..5]-() return p` //decodes raw path response from driver +//example query `match p=(n)-[*0..5]-() return p` func decode(rawArr [][]interface{}, respObj interface{}) (err error) { //check nil params if rawArr == nil { @@ -81,19 +101,20 @@ func decode(rawArr [][]interface{}, respObj interface{}) (err error) { } } nodeLookup := make(map[int64]*reflect.Value) + relMaps := make(map[int64]map[string]*RelationConfig) var pks []int64 rels := make(map[int64]*neoEdgeConfig) labelLookup := map[int64]string{} if paths != nil && len(paths) != 0 { - err = sortPaths(paths, &nodeLookup, &rels, &pks, primaryLabel) + err = sortPaths(paths, &nodeLookup, &rels, &pks, primaryLabel, &relMaps) if err != nil { return err } } if isolatedNodes != nil && len(isolatedNodes) != 0 { - err = sortIsolatedNodes(isolatedNodes, &labelLookup, &nodeLookup, &pks, primaryLabel) + err = sortIsolatedNodes(isolatedNodes, &labelLookup, &nodeLookup, &pks, primaryLabel, &relMaps) if err != nil { return err } @@ -113,7 +134,6 @@ func decode(rawArr [][]interface{}, respObj interface{}) (err error) { //build relationships for _, relationConfig := range rels { - //todo figure out why this is broken if relationConfig.StartNodeType == "" || relationConfig.EndNodeType == "" { continue } @@ -136,6 +156,50 @@ func decode(rawArr [][]interface{}, respObj interface{}) (err error) { return err } + if startMap, ok := relMaps[relationConfig.StartNodeId]; ok { + if conf, ok := startMap[startConfig.FieldName]; ok { + conf.Ids = append(conf.Ids, relationConfig.EndNodeId) + } else { + var rt RelationType + if startConfig.ManyRelationship { + rt = Multi + } else { + rt = Single + } + + newConf := &RelationConfig{ + Ids: []int64{relationConfig.EndNodeId}, + RelationType: rt, + } + + startMap[startConfig.FieldName] = newConf + } + } else { + return fmt.Errorf("relation config not found for id [%v]", relationConfig.StartNodeId) + } + + if endMap, ok := relMaps[relationConfig.EndNodeId]; ok { + if conf, ok := endMap[endConfig.FieldName]; ok { + conf.Ids = append(conf.Ids, relationConfig.EndNodeId) + } else { + var rt RelationType + if endConfig.ManyRelationship { + rt = Multi + } else { + rt = Single + } + + newConf := &RelationConfig{ + Ids: []int64{relationConfig.StartNodeId}, + RelationType: rt, + } + + endMap[endConfig.FieldName] = newConf + } + } else { + return fmt.Errorf("relation config not found for id [%v]", relationConfig.StartNodeId) + } + if startConfig.UsesEdgeNode { var typeConfig structDecoratorConfig @@ -231,6 +295,13 @@ func decode(rawArr [][]interface{}, respObj interface{}) (err error) { } } + //set load maps + if len(rels) != 0 { + for id, val := range nodeLookup { + reflect.Indirect(*val).FieldByName(loadMapField).Set(reflect.ValueOf(relMaps[id])) + } + } + //handle if its returning a slice -- validation has been done at an earlier step if rt.Elem().Kind() == reflect.Slice { @@ -270,6 +341,7 @@ func decode(rawArr [][]interface{}, respObj interface{}) (err error) { } } +// getPrimaryLabel gets the label from a reflect type func getPrimaryLabel(rt reflect.Type) string { //assume its already a pointer rt = rt.Elem() @@ -284,7 +356,8 @@ func getPrimaryLabel(rt reflect.Type) string { return rt.Name() } -func sortIsolatedNodes(isolatedNodes []*graph.Node, labelLookup *map[int64]string, nodeLookup *map[int64]*reflect.Value, pks *[]int64, pkLabel string) error { +// sortIsolatedNodes process nodes that are returned individually from bolt driver +func sortIsolatedNodes(isolatedNodes []*graph.Node, labelLookup *map[int64]string, nodeLookup *map[int64]*reflect.Value, pks *[]int64, pkLabel string, relMaps *map[int64]map[string]*RelationConfig) error { if isolatedNodes == nil { return fmt.Errorf("isolatedNodes can not be nil, %w", ErrInternal) } @@ -303,6 +376,7 @@ func sortIsolatedNodes(isolatedNodes []*graph.Node, labelLookup *map[int64]strin } (*nodeLookup)[node.NodeIdentity] = val + (*relMaps)[node.NodeIdentity] = map[string]*RelationConfig{} //primary to return if node.Labels != nil && len(node.Labels) != 0 && node.Labels[0] == pkLabel { @@ -319,6 +393,7 @@ func sortIsolatedNodes(isolatedNodes []*graph.Node, labelLookup *map[int64]strin return nil } +// sortStrictRels sorts relationships that are strictly defined (i.e direction is pre defined) from the bolt driver func sortStrictRels(strictRels []*graph.Relationship, labelLookup *map[int64]string, rels *map[int64]*neoEdgeConfig) error { if strictRels == nil { return fmt.Errorf("paths is empty, that shouldn't have happened, %w", ErrInternal) @@ -355,7 +430,8 @@ func sortStrictRels(strictRels []*graph.Relationship, labelLookup *map[int64]str return nil } -func sortPaths(paths []*graph.Path, nodeLookup *map[int64]*reflect.Value, rels *map[int64]*neoEdgeConfig, pks *[]int64, pkLabel string) error { +// sortPaths sorts nodes and relationships from bolt driver that dont specify the direction explicitly, instead uses the bolt spec to determine direction +func sortPaths(paths []*graph.Path, nodeLookup *map[int64]*reflect.Value, rels *map[int64]*neoEdgeConfig, pks *[]int64, pkLabel string, relMaps *map[int64]map[string]*RelationConfig) error { if paths == nil { return fmt.Errorf("paths is empty, that shouldn't have happened, %w", ErrInternal) } @@ -379,6 +455,7 @@ func sortPaths(paths []*graph.Path, nodeLookup *map[int64]*reflect.Value, rels * } (*nodeLookup)[node.NodeIdentity] = val + (*relMaps)[node.NodeIdentity] = map[string]*RelationConfig{} //primary to return if node.Labels != nil && len(node.Labels) != 0 && node.Labels[0] == pkLabel { @@ -436,6 +513,7 @@ func sortPaths(paths []*graph.Path, nodeLookup *map[int64]*reflect.Value, rels * return nil } +// getValueAndConfig returns reflect value of specific node and the configuration for the node func getValueAndConfig(id int64, t string, nodeLookup map[int64]*reflect.Value) (val *reflect.Value, conf structDecoratorConfig, err error) { var ok bool @@ -460,6 +538,7 @@ func getValueAndConfig(id int64, t string, nodeLookup map[int64]*reflect.Value) var sliceOfEmptyInterface []interface{} var emptyInterfaceType = reflect.TypeOf(sliceOfEmptyInterface).Elem() +// convertToValue converts properties map from neo4j to golang reflect value func convertToValue(graphId int64, conf structDecoratorConfig, props map[string]interface{}, rtype reflect.Type) (valss *reflect.Value, err error) { defer func() { if r := recover(); r != nil { @@ -493,6 +572,10 @@ func convertToValue(graphId int64, conf structDecoratorConfig, props map[string] continue } + if fieldConfig.Ignore { + continue + } + if fieldConfig.Properties { mapType := reflect.MapOf(reflect.TypeOf(""), emptyInterfaceType) mapVal := reflect.MakeMap(mapType) @@ -568,6 +651,7 @@ func convertToValue(graphId int64, conf structDecoratorConfig, props map[string] return &val, err } +// convertNodeToValue converts raw bolt node to reflect value func convertNodeToValue(boltNode graph.Node) (*reflect.Value, error) { if boltNode.Labels == nil || len(boltNode.Labels) == 0 { diff --git a/decoder_test.go b/decoder_test.go index 11b8969..26bdc6b 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -1,10 +1,27 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm import ( "errors" "github.com/cornelk/hashmap" - dsl "github.com/mindstand/go-cypherdsl" - driver "github.com/mindstand/golang-neo4j-bolt-driver" "github.com/mindstand/golang-neo4j-bolt-driver/structures/graph" "github.com/stretchr/testify/require" "reflect" @@ -12,50 +29,6 @@ import ( "time" ) -func TestDecode(t *testing.T) { - if !testing.Short() { - t.Skip() - return - } - - req := require.New(t) - - req.Nil(setupInit(false, &Config{ - Host: "0.0.0.0", - Port: 7687, - IsCluster: false, - Username: "neo4j", - Password: "password", - PoolSize: 50, - IndexStrategy: IGNORE_INDEX, - }, &a{}, &b{}, &c{})) - - req.EqualValues(3, mappedTypes.Len()) - - query := `match p=(n:a{uuid:'d5c56567-da8e-429f-9cea-300e722195e0'})-[*0..4]-() return p` - - conn, err := driverPool.Open(driver.ReadWriteMode) - if err != nil { - require.Nil(t, err) - } - defer driverPool.Reclaim(conn) - - rows, err := dsl.QB().WithNeo(conn).Cypher(query).Query(nil) - require.Nil(t, err) - require.NotNil(t, rows) - - var stuff a - require.Nil(t, decodeNeoRows(rows, &stuff)) - t.Log(stuff.Id) - t.Log(stuff.UUID) - t.Log(stuff) - req.NotNil(stuff.SingleSpecA) - req.NotNil(stuff.SingleSpecA.End.Single) - //t.Log(stuff.MultiSpecA[0].End.Id) - //req.NotEqual(0, stuff.Id) - //req.True(len(stuff.MultiSpecA) > 0) -} - type TestStruct struct { Id int64 UUID string @@ -158,13 +131,13 @@ type tdString string type tdInt int type f struct { - embedTest - Parents []*f `gogm:"direction=outgoing;relationship=test"` - Children []*f `gogm:"direction=incoming;relationship=test"` + BaseNode + Parents []*f `gogm:"direction=outgoing;relationship=test"` + Children []*f `gogm:"direction=incoming;relationship=test"` } type a struct { - embedTest + BaseNode TestField string `gogm:"name=test_field"` TestTypeDefString tdString `gogm:"name=test_type_def_string"` TestTypeDefInt tdInt `gogm:"name=test_type_def_int"` @@ -176,18 +149,18 @@ type a struct { } type b struct { - embedTest + BaseNode TestField string `gogm:"name=test_field"` TestTime time.Time `gogm:"time;name=test_time"` Single *a `gogm:"direction=outgoing;relationship=test_rel"` - ManyB *a `gogm:"direction=incoming;relationship=testm2o"` + ManyB *a `gogm:"direction=outgoing;relationship=testm2o"` Multi []*a `gogm:"direction=outgoing;relationship=multib"` SingleSpec *c `gogm:"direction=incoming;relationship=special_single"` MultiSpec []*c `gogm:"direction=incoming;relationship=special_multi"` } type c struct { - embedTest + BaseNode Start *a End *b Test string `gogm:"name=test"` @@ -284,21 +257,21 @@ func TestDecoder(t *testing.T) { } f0 := f{ - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 0, UUID: "0", }, } f1 := f{ - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 1, UUID: "1", }, } f2 := f{ - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 2, UUID: "2", }, @@ -315,13 +288,17 @@ func TestDecoder(t *testing.T) { for _, r := range readin10 { if r.Id == 0 { req.True(len(r.Parents) == 1) + req.True(r.LoadMap["Parents"].Ids[0] == 1) req.True(len(r.Children) == 0) } else if r.Id == 1 { req.True(len(r.Parents) == 1) + req.True(r.LoadMap["Parents"].Ids[0] == 2) req.True(len(r.Children) == 1) + req.True(r.LoadMap["Children"].Ids[0] == 0) } else if r.Id == 2 { req.True(len(r.Parents) == 0) req.True(len(r.Children) == 1) + req.True(r.LoadMap["Children"].Ids[0] == 1) } else { t.FailNow() } @@ -366,7 +343,7 @@ func TestDecoder(t *testing.T) { var readin a comp := &a{ - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 1, UUID: "dasdfasd", }, @@ -376,7 +353,7 @@ func TestDecoder(t *testing.T) { } comp22 := &b{ - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 2, UUID: "dasdfas", }, @@ -455,7 +432,7 @@ func TestDecoder(t *testing.T) { var readin2 a comp2 := &a{ - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 1, UUID: "dasdfasd", }, @@ -463,7 +440,7 @@ func TestDecoder(t *testing.T) { } b2 := &b{ - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 2, UUID: "dasdfas", }, @@ -472,7 +449,7 @@ func TestDecoder(t *testing.T) { } c1 := &c{ - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 34, UUID: "asdfasdafsd", }, @@ -529,7 +506,7 @@ func TestDecoder(t *testing.T) { var readin3 a comp3 := a{ - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 1, UUID: "dasdfasd", }, @@ -537,11 +514,11 @@ func TestDecoder(t *testing.T) { MultiA: []*b{ { TestField: "test", - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 2, UUID: "dasdfas", }, - TestTime: fTime, + TestTime: fTime, }, }, } @@ -597,7 +574,7 @@ func TestDecoder(t *testing.T) { comp4 := &a{ TestField: "test", - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 1, UUID: "dasdfasd", }, @@ -605,15 +582,15 @@ func TestDecoder(t *testing.T) { b3 := &b{ TestField: "test", - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 2, UUID: "dasdfas", }, - TestTime: fTime, + TestTime: fTime, } c4 := c{ - embedTest: embedTest{ + BaseNode: BaseNode{ UUID: "asdfasdafsd", }, Start: comp4, diff --git a/decorator.go b/decorator.go index 2f63c44..3521a9b 100644 --- a/decorator.go +++ b/decorator.go @@ -1,3 +1,22 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm import ( @@ -9,44 +28,134 @@ import ( "time" ) +// defined the decorator name for struct tag const decoratorName = "gogm" +// reflect type for go time.Time var timeType = reflect.TypeOf(time.Time{}) //sub fields of the decorator const ( - paramNameField = "name" //requires assignment - relationshipNameField = "relationship" //requires assignment - directionField = "direction" //requires assignment - timeField = "time" - indexField = "index" - uniqueField = "unique" - primaryKeyField = "pk" - propertiesField = "properties" - ignoreField = "-" - deliminator = ";" - assignmentOperator = "=" + // specifies the name in neo4j + //requires assignment (if specified) + paramNameField = "name" + + // specifies the name of the relationship + //requires assignment (if edge field) + relationshipNameField = "relationship" + + //specifies direction, can only be (incoming|outgoing|both|none) + //requires assignment (if edge field) + directionField = "direction" + + //specifies if the field contains time representation + timeField = "time" + + //specifies if the field is to be indexed + indexField = "index" + + //specifies if the field is unique + uniqueField = "unique" + + //specifies is the field is a primary key + primaryKeyField = "pk" + + //specifies if the field is map of type `map[string]interface{}` + propertiesField = "properties" + + //specifies if the field is to be ignored + ignoreField = "-" + + //specifies deliminator between GoGM tags + deliminator = ";" + + //assignment operator for GoGM tags + assignmentOperator = "=" ) +//decorator config defines configuration of GoGM field type decoratorConfig struct { - Type reflect.Type - Name string - FieldName string - Relationship string - Direction dsl.Direction - Unique bool - Index bool - ManyRelationship bool - UsesEdgeNode bool - PrimaryKey bool - Properties bool - IsTime bool - IsTypeDef bool - TypedefActual reflect.Type - Ignore bool + // holds reflect type for the field + Type reflect.Type `json:"-"` + // holds the name of the field for neo4j + Name string `json:"name"` + // holds the name of the field in the struct + FieldName string `json:"field_name"` + // holds the name of the relationship + Relationship string `json:"relationship"` + // holds the direction + Direction dsl.Direction `json:"direction"` + // specifies if field is to be unique + Unique bool `json:"unique"` + // specifies if field is to be indexed + Index bool `json:"index"` + // specifies if field represents many relationship + ManyRelationship bool `json:"many_relationship"` + // uses edge specifies if the edge is a special node + UsesEdgeNode bool `json:"uses_edge_node"` + // specifies whether the field is the nodes primary key + PrimaryKey bool `json:"primary_key"` + // specify if the field holds properties + Properties bool `json:"properties"` + // specifies if the field contains time value + IsTime bool `json:"is_time"` + // specifies if the field contains a typedef of another type + IsTypeDef bool `json:"is_type_def"` + // holds the reflect type of the root type if typedefed + TypedefActual reflect.Type `json:"-"` + // specifies whether to ignore the field + Ignore bool `json:"ignore"` +} + +// Equals checks equality of decorator configs +func (d *decoratorConfig) Equals(comp *decoratorConfig) bool { + if comp == nil { + return false + } + + return d.Name == comp.Name && d.FieldName == comp.FieldName && d.Relationship == comp.Relationship && + d.Direction == comp.Direction && d.Unique == comp.Unique && d.Index == comp.Index && d.ManyRelationship == comp.ManyRelationship && + d.UsesEdgeNode == comp.UsesEdgeNode && d.PrimaryKey == comp.PrimaryKey && d.Properties == comp.Properties && d.IsTime == comp.IsTime && + d.IsTypeDef == comp.IsTypeDef && d.Ignore == comp.Ignore } -//have struct validate itself +// specifies configuration on GoGM node +type structDecoratorConfig struct { + // Holds fields -> their configurations + // field name : decorator configuration + Fields map[string]decoratorConfig `json:"fields"` + // holds label for the node, maps to struct name + Label string `json:"label"` + // specifies if the node is a vertex or an edge (if true, its a vertex) + IsVertex bool `json:"is_vertex"` + // holds the reflect type of the struct + Type reflect.Type `json:"-"` +} + +// Equals checks equality of structDecoratorConfigs +func (s *structDecoratorConfig) Equals(comp *structDecoratorConfig) bool { + if comp == nil { + return false + } + + if comp.Fields != nil && s.Fields != nil { + for field, decConfig := range s.Fields { + if compConfig, ok := comp.Fields[field]; ok { + if !compConfig.Equals(&decConfig) { + return false + } + } else { + return false + } + } + } else { + return false + } + + return s.IsVertex == comp.IsVertex && s.Label == comp.Label +} + +// Validate checks if the configuration is valid func (d *decoratorConfig) Validate() error { if d.Ignore { if d.Relationship != "" || d.Unique || d.Index || d.ManyRelationship || d.UsesEdgeNode || @@ -166,6 +275,8 @@ func (d *decoratorConfig) Validate() error { var edgeType = reflect.TypeOf(new(IEdge)).Elem() +// newDecoratorConfig generates decorator config for field +// takes in the raw tag, name of the field and reflect type func newDecoratorConfig(decorator, name string, varType reflect.Type) (*decoratorConfig, error) { fields := strings.Split(decorator, deliminator) @@ -298,15 +409,7 @@ func newDecoratorConfig(decorator, name string, varType reflect.Type) (*decorato return &toReturn, nil } -type structDecoratorConfig struct { - // field name : decorator configuration - Fields map[string]decoratorConfig - Label string - IsVertex bool - Type reflect.Type -} - -//validates struct configuration +//validates if struct decorator is valid func (s *structDecoratorConfig) Validate() error { if s.Fields == nil { return errors.New("no fields defined") @@ -342,6 +445,7 @@ func (s *structDecoratorConfig) Validate() error { return nil } +// getStructDecoratorConfig generates structDecoratorConfig for struct func getStructDecoratorConfig(i interface{}, mappedRelations *relationConfigs) (*structDecoratorConfig, error) { toReturn := &structDecoratorConfig{} @@ -463,6 +567,7 @@ func getStructDecoratorConfig(i interface{}, mappedRelations *relationConfigs) ( return toReturn, nil } +// getFields gets all fields in a struct, specifically also gets fields from embedded structs func getFields(val reflect.Type) []*reflect.StructField { var fields []*reflect.StructField if val.Kind() == reflect.Ptr { @@ -471,7 +576,7 @@ func getFields(val reflect.Type) []*reflect.StructField { for i := 0; i < val.NumField(); i++ { tempField := val.Field(i) - if tempField.Anonymous && tempField.Type.Kind() == reflect.Struct{ + if tempField.Anonymous && tempField.Type.Kind() == reflect.Struct { fields = append(fields, getFields(tempField.Type)...) } else { fields = append(fields, &tempField) @@ -479,4 +584,4 @@ func getFields(val reflect.Type) []*reflect.StructField { } return fields -} \ No newline at end of file +} diff --git a/decorator_test.go b/decorator_test.go index e278c91..fc5c270 100644 --- a/decorator_test.go +++ b/decorator_test.go @@ -1,3 +1,22 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm import ( @@ -307,8 +326,8 @@ func TestNewDecoratorConfig(t *testing.T) { //structs with decorators for testing type embedTest struct { - Id int64 `gogm:"name=id"` - UUID string `gogm:"pk;name=uuid"` + Id int64 `gogm:"name=id"` + UUID string `gogm:"pk;name=uuid"` } type validStruct struct { diff --git a/defaults.go b/defaults.go index addd244..bf5de72 100644 --- a/defaults.go +++ b/defaults.go @@ -1,6 +1,54 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm +const loadMapField = "LoadMap" + +// BaseNode contains fields that ALL GoGM nodes are required to have type BaseNode struct { - Id int64 `json:"-" gogm:"name=id"` - UUID string `json:"uuid" gogm:"pk;name=uuid"` -} \ No newline at end of file + // Id is the GraphId that neo4j uses internally + Id int64 `json:"-" gogm:"name=id"` + // UUID is the unique identifier GoGM uses as a primary key + UUID string `json:"uuid" gogm:"pk;name=uuid"` + + // LoadMap represents the state of how a node was loaded for neo4j. + // This is used to determine if relationships are removed on save + // field -- relations + LoadMap map[string]*RelationConfig `json:"-" gogm:"-"` +} + +// Specifies Type of Relationship +type RelationType int + +const ( + // Side of relationship can only point to 0 or 1 other nodes + Single RelationType = 0 + + // Side of relationship can point to 0+ other nodes + Multi RelationType = 1 +) + +// RelationConfig specifies how relationships are loaded +type RelationConfig struct { + // stores graph ids + Ids []int64 `json:"-" gomg:"-"` + // specifies relationship type + RelationType RelationType `json:"-" gomg:"-"` +} diff --git a/delete.go b/delete.go index 29c859f..4091b1c 100644 --- a/delete.go +++ b/delete.go @@ -1,3 +1,22 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm import ( @@ -7,6 +26,7 @@ import ( "reflect" ) +// deleteNode is used to remove nodes from the database func deleteNode(conn *driver.BoltConn, deleteObj interface{}) error { rawType := reflect.TypeOf(deleteObj) @@ -55,6 +75,7 @@ func deleteNode(conn *driver.BoltConn, deleteObj interface{}) error { return deleteByIds(conn, ids...) } +// deleteByIds deletes node by graph ids func deleteByIds(conn *driver.BoltConn, ids ...int64) error { rows, err := dsl.QB(). Cypher("UNWIND {rows} as row"). @@ -83,6 +104,7 @@ func deleteByIds(conn *driver.BoltConn, ids ...int64) error { return nil } +// deleteByUuids deletes nodes by uuids func deleteByUuids(conn *driver.BoltConn, ids ...string) error { rows, err := dsl.QB(). Cypher("UNWIND {rows} as row"). diff --git a/delete_test.go b/delete_test.go index 8ed8288..9861b9f 100644 --- a/delete_test.go +++ b/delete_test.go @@ -1,29 +1,43 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm import ( driver "github.com/mindstand/golang-neo4j-bolt-driver" "github.com/stretchr/testify/require" - "testing" ) -func TestDelete(t *testing.T) { - if !testing.Short() { - t.Skip() - return - } +func testDelete(req *require.Assertions) { conn, err := driverPool.Open(driver.ReadWriteMode) if err != nil { - require.Nil(t, err) + req.Nil(err) } defer driverPool.Reclaim(conn) del := a{ - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 0, UUID: "5334ee8c-6231-40fd-83e5-16c8016ccde6", }, } err = deleteNode(conn, &del) - require.Nil(t, err) + req.Nil(err) } diff --git a/errors.go b/errors.go index c2677c4..d01f3f2 100644 --- a/errors.go +++ b/errors.go @@ -1,3 +1,22 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm import ( @@ -5,6 +24,8 @@ import ( "fmt" ) +// todo replace this with go 1.13 errors + type InvalidDecoratorConfigError struct { Field string Issue string @@ -35,8 +56,10 @@ func (i *InvalidStructConfigError) Error() string { return i.issue } +// base errors for gogm 1.13 errors, these are pretty self explanatory var ErrNotFound = errors.New("gogm: data not found") var ErrInternal = errors.New("gogm: internal error") +var ErrValidation = errors.New("gogm: struct validation error") var ErrInvalidParams = errors.New("gogm: invalid params") var ErrConfiguration = errors.New("gogm: configuration was malformed") var ErrTransaction = errors.New("gogm: transaction error") diff --git a/go.mod b/go.mod index 0874990..535b01e 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,11 @@ module github.com/mindstand/gogm go 1.13 require ( + github.com/adam-hanna/arrayOperations v0.2.5 github.com/cornelk/hashmap v1.0.0 + github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/dchest/siphash v1.2.1 // indirect + github.com/google/addlicense v0.0.0-20190907113143-be125746c2c4 // indirect github.com/google/uuid v1.1.1 github.com/kr/pretty v0.1.0 // indirect github.com/mindstand/go-cypherdsl v0.0.0-20191030200322-ed2619be6449 @@ -13,6 +16,8 @@ require ( github.com/sirupsen/logrus v1.4.2 github.com/stretchr/objx v0.2.0 // indirect github.com/stretchr/testify v1.4.0 + github.com/urfave/cli v1.22.2 // indirect + github.com/urfave/cli/v2 v2.0.0 golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/go.sum b/go.sum index f1ec15b..e65c465 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,21 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/adam-hanna/arrayOperations v0.2.5 h1:zphKpB5HGhHDkztF2oLcvnqIAu/L/YU3FB/9UghdsO0= +github.com/adam-hanna/arrayOperations v0.2.5/go.mod h1:PhqKQzzPMRjFcC4Heh+kxha3nMvJ6lQNKuVEgoyimgU= github.com/cornelk/hashmap v1.0.0 h1:jNHWycAM10SO5Ig76HppMQ69jnbqaziRpqVTNvAxdJQ= github.com/cornelk/hashmap v1.0.0/go.mod h1:8wbysTUDnwJGrPZ1Iwsou3m+An6sldFrJItjRhfegCw= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dchest/siphash v1.1.0/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4= github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= +github.com/google/addlicense v0.0.0-20190907113143-be125746c2c4 h1:Bptr91tgP3H4/tg/69DYMrievvj8AgXXr5ktPmm+p38= +github.com/google/addlicense v0.0.0-20190907113143-be125746c2c4/go.mod h1:QtPG26W17m+OIQgE6gQ24gC1M6pUaMBAbFrTIDtwG/E= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jolestar/go-commons-pool v2.0.0+incompatible h1:uHn5uRKsLLQSf9f1J5QPY2xREWx/YH+e4bIIXcAuAaE= @@ -42,6 +52,10 @@ github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQz github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -54,6 +68,10 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.0.0 h1:+HU9SCbu8GnEUFtIBfuUNXN39ofWViIEJIp6SURMpCg= +github.com/urfave/cli/v2 v2.0.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/index.go b/index.go index 6f22a73..67784b0 100644 --- a/index.go +++ b/index.go @@ -1,11 +1,30 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm import ( "errors" "fmt" + "github.com/adam-hanna/arrayOperations" "github.com/cornelk/hashmap" dsl "github.com/mindstand/go-cypherdsl" - "github.com/mindstand/gogm/util" driver "github.com/mindstand/golang-neo4j-bolt-driver" ) @@ -279,14 +298,14 @@ func verifyAllIndexesAndConstraints(mappedTypes *hashmap.HashMap) error { } //verify from there - delta, found := util.Difference(foundIndexes, indexes) + delta, found := arrayOperations.Difference(foundIndexes, indexes) if !found { return fmt.Errorf("found differences in remote vs ogm for found indexes, %v", delta) } log.Debug(delta) - delta, found = util.Difference(foundConstraints, constraints) + delta, found = arrayOperations.Difference(foundConstraints, constraints) if !found { return fmt.Errorf("found differences in remote vs ogm for found constraints, %v", delta) } diff --git a/index_test.go b/index_test.go index 72ef983..b4f9461 100644 --- a/index_test.go +++ b/index_test.go @@ -1,72 +1,34 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm import ( - dsl "github.com/mindstand/go-cypherdsl" driver "github.com/mindstand/golang-neo4j-bolt-driver" "github.com/stretchr/testify/require" "reflect" - "testing" ) -func TestDropAllIndexesAndConstraints(t *testing.T) { - //requires connection - if !testing.Short() { - t.SkipNow() - return - } - - conn, err := driverPool.Open(driver.ReadWriteMode) - if err != nil { - require.Nil(t, err) - } - defer driverPool.Reclaim(conn) - require.Nil(t, err) - - err = dropAllIndexesAndConstraints() - require.Nil(t, err) - - constraintRows, err := dsl.QB().WithNeo(conn).Cypher("CALL db.constraints").Query(nil) - require.Nil(t, err) - - found, _, err := constraintRows.All() - require.Nil(t, err) - - require.Equal(t, 0, len(found)) - - indexRows, err := dsl.QB().WithNeo(conn).Cypher("CALL db.indexes()").Query(nil) - require.Nil(t, err) - - iFound, _, err := indexRows.All() - require.Nil(t, err) - - require.Equal(t, 0, len(iFound)) -} - -func TestIndexManagement(t *testing.T) { - //requires connection - if !testing.Short() { - t.SkipNow() - return - } - - req := require.New(t) - - var err error - - conf := Config{ - Username: "neo4j", - Password: "password", - Host: "0.0.0.0", - Port: 7687, - PoolSize: 15, - } - - driverPool, err = driver.NewClosableDriverPool(conf.ConnectionString(), conf.PoolSize) - req.Nil(err) - +func testIndexManagement(req *require.Assertions) { //init conn, err := driverPool.Open(driver.ReadWriteMode) - require.Nil(t, err) + req.Nil(err) defer driverPool.Reclaim(conn) req.Nil(err) @@ -123,7 +85,7 @@ func TestIndexManagement(t *testing.T) { //create stuff req.Nil(createAllIndexesAndConstraints(mapp)) - t.Log("created indices and constraints") + log.Println("created indices and constraints") //validate req.Nil(verifyAllIndexesAndConstraints(mapp)) diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 0000000..2c8e9f5 --- /dev/null +++ b/integration_test.go @@ -0,0 +1,120 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gogm + +import ( + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestIntegration(t *testing.T) { + if testing.Short() { + t.Skip() + } + + req := require.New(t) + + conf := Config{ + Username: "neo4j", + Password: "password", + Host: "0.0.0.0", + Port: 7687, + PoolSize: 15, + IndexStrategy: IGNORE_INDEX, + } + + req.Nil(Init(&conf, &a{}, &b{}, &c{})) + + sess, err := NewSession(false) + req.Nil(err) + defer sess.Close() + + log.Println("testIndexManagement") + testIndexManagement(req) + + log.Println("test save") + testSave(sess, req) + + req.Nil(sess.PurgeDatabase()) +} + +// runs with integration test +func testSave(sess *Session, req *require.Assertions) { + req.Nil(sess.Begin()) + a2 := &a{ + TestField: "test", + } + + b2 := &b{ + TestField: "test", + TestTime: time.Now().UTC(), + } + + b3 := &b{ + TestField: "dasdfasd", + } + + c1 := &c{ + Start: a2, + End: b2, + Test: "testing", + } + + a2.SingleSpecA = c1 + a2.ManyA = []*b{b3} + b2.SingleSpec = c1 + b3.ManyB = a2 + + req.Nil(sess.SaveDepth(a2, 5)) + + req.Nil(sess.Commit()) + req.Nil(sess.Begin()) + + req.EqualValues(map[string]*RelationConfig{ + "SingleSpecA": { + Ids: []int64{b2.Id}, + RelationType: Single, + }, + "ManyA": { + Ids: []int64{b3.Id}, + RelationType: Multi, + }, + }, a2.LoadMap) + req.EqualValues(map[string]*RelationConfig{ + "SingleSpec": { + Ids: []int64{a2.Id}, + RelationType: Single, + }, + }, b2.LoadMap) + req.EqualValues(map[string]*RelationConfig{ + "ManyB": { + Ids: []int64{a2.Id}, + RelationType: Single, + }, + }, b3.LoadMap) + a2.SingleSpecA = nil + b2.SingleSpec = nil + + req.Nil(sess.SaveDepth(a2, 5)) + req.Nil(sess.Commit()) + req.Nil(a2.SingleSpecA) + req.Nil(b2.SingleSpec) +} diff --git a/interface.go b/interface.go index b0ee451..98f0f22 100644 --- a/interface.go +++ b/interface.go @@ -1,3 +1,22 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm import ( @@ -5,13 +24,20 @@ import ( "reflect" ) +// IEdge specifies required functions for special edge nodes type IEdge interface { + // GetStartNode gets start node of edge GetStartNode() interface{} + // GetStartNodeType gets reflect type of start node GetStartNodeType() reflect.Type + // SetStartNode sets start node of edge SetStartNode(v interface{}) error + // GetEndNode gets end node of edge GetEndNode() interface{} + // GetEndNodeType gets reflect type of end node GetEndNodeType() reflect.Type + // SetEndNode sets end node of edge SetEndNode(v interface{}) error } @@ -70,12 +96,18 @@ type ISession interface { //delete everything, this will literally delete everything PurgeDatabase() error + // closes session Close() error } +// ITransaction specifies functions for Neo4j ACID transactions type ITransaction interface { + // Begin begins transaction Begin() error + // Rollback rolls back transaction Rollback() error + // RollbackWithError wraps original error into rollback error if there is one RollbackWithError(err error) error + // Commit commits transaction Commit() error } diff --git a/load_strategy.go b/load_strategy.go index 06cfa3d..95b4d94 100644 --- a/load_strategy.go +++ b/load_strategy.go @@ -1,3 +1,22 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm import ( @@ -6,13 +25,17 @@ import ( dsl "github.com/mindstand/go-cypherdsl" ) +// Specifies query based load strategy type LoadStrategy int const ( + // PathLoadStrategy uses cypher path PATH_LOAD_STRATEGY LoadStrategy = iota + // SchemaLoadStrategy generates queries specifically from generated schema SCHEMA_LOAD_STRATEGY ) +// PathLoadStrategyMany loads many using path strategy func PathLoadStrategyMany(variable, label string, depth int, additionalConstraints dsl.ConditionOperator) (dsl.Cypher, error) { if variable == "" { return nil, errors.New("variable name cannot be empty") @@ -46,6 +69,7 @@ func PathLoadStrategyMany(variable, label string, depth int, additionalConstrain return builder.Return(false, dsl.ReturnPart{Name: "p"}), nil } +// PathLoadStrategyOne loads one object using path strategy func PathLoadStrategyOne(variable, label string, depth int, additionalConstraints dsl.ConditionOperator) (dsl.Cypher, error) { if variable == "" { return nil, errors.New("variable name cannot be empty") @@ -91,6 +115,7 @@ func PathLoadStrategyOne(variable, label string, depth int, additionalConstraint return builder.Return(false, dsl.ReturnPart{Name: "p"}), nil } +// PathLoadStrategyEdgeConstraint is similar to load many, but requires that it is related to another node via some edge func PathLoadStrategyEdgeConstraint(startVariable, startLabel, endLabel, endTargetField string, minJumps, maxJumps, depth int, additionalConstraints dsl.ConditionOperator) (dsl.Cypher, error) { if startVariable == "" { return nil, errors.New("variable name cannot be empty") diff --git a/load_strategy_test.go b/load_strategy_test.go index 8b79b3c..5c718d6 100644 --- a/load_strategy_test.go +++ b/load_strategy_test.go @@ -1 +1,20 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm diff --git a/mocks/ISession.go b/mocks/ISession.go index ddcc54c..2bf065c 100644 --- a/mocks/ISession.go +++ b/mocks/ISession.go @@ -1,3 +1,22 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + // Code generated by mockery v1.0.0. DO NOT EDIT. package mocks diff --git a/model.go b/model.go index d944da3..92fe84d 100644 --- a/model.go +++ b/model.go @@ -1,14 +1,25 @@ -package gogm - -type Vertex struct { - Id string `json:"-" gogm:"name=id"` - UUID string `json:"uuid" gogm:"pk;name=uuid"` -} +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -type Edge struct { - Id string `json:"-" gogm:"name=id"` -} +package gogm +// specifies how edges are loaded type neoEdgeConfig struct { Id int64 diff --git a/pagination.go b/pagination.go index d4ef18a..c7619ee 100644 --- a/pagination.go +++ b/pagination.go @@ -1,13 +1,38 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm import "errors" +// pagination configuration type Pagination struct { - PageNumber int - LimitPerPage int + // specifies which page number to load + PageNumber int + // limits how many records per page + LimitPerPage int + // specifies variable to order by OrderByVarName string - OrderByField string - OrderByDesc bool + // specifies field to order by on + OrderByField string + // specifies whether orderby is desc or asc + OrderByDesc bool } func (p *Pagination) Validate() error { diff --git a/save.go b/save.go index 54fff41..e4566ac 100644 --- a/save.go +++ b/save.go @@ -1,3 +1,22 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm import ( @@ -6,24 +25,36 @@ import ( dsl "github.com/mindstand/go-cypherdsl" driver "github.com/mindstand/golang-neo4j-bolt-driver" "reflect" + "sync" ) +// maximum supported depth const maxSaveDepth = 10 const defaultSaveDepth = 1 +// nodeCreateConf holds configuration for creating new nodes type nodeCreateConf struct { + // params to save Params map[string]interface{} - Type reflect.Type - IsNew bool + // type to save by + Type reflect.Type + // whether the node is new or not + IsNew bool } +// relCreateConf holds configuration for nodes to link together type relCreateConf struct { + // start uuid of relationship StartNodeUUID string - EndNodeUUID string - Params map[string]interface{} - Direction dsl.Direction + // end uuid of relationship + EndNodeUUID string + // any data to store in edge + Params map[string]interface{} + // holds direction of the edge + Direction dsl.Direction } +// saves target node and connected node to specified depth func saveDepth(sess *driver.BoltConn, obj interface{}, depth int) error { if sess == nil { return errors.New("session can not be nil") @@ -38,7 +69,7 @@ func saveDepth(sess *driver.BoltConn, obj interface{}, depth int) error { } if depth > maxSaveDepth { - return fmt.Errorf("saving depth of (%v) is currently not supported, maximum depth is (%v)", depth, maxSaveDepth) + return fmt.Errorf("saving depth of (%v) is currently not supported, maximum depth is (%v), %w", depth, maxSaveDepth, ErrConfiguration) } //validate that obj is a pointer @@ -61,27 +92,200 @@ func saveDepth(sess *driver.BoltConn, obj interface{}, depth int) error { //signature is [LABEL] []{config} relations := map[string][]relCreateConf{} + // node id -- [field] config + oldRels := map[string]map[string]*RelationConfig{} + curRels := map[string]map[string]*RelationConfig{} + + // uuid -> reflect value + nodeRef := map[string]*reflect.Value{} + + newNodes := []*string{} + rootVal := reflect.ValueOf(obj) - err := parseStruct("", "", false, 0, nil, &rootVal, 0, depth, &nodes, &relations) + err := parseStruct("", "", false, 0, nil, &rootVal, 0, depth, &nodes, &relations, &oldRels, &newNodes, &nodeRef) + if err != nil { + return err + } + + ids, err := createNodes(sess, nodes, &nodeRef) if err != nil { return err } - ids, err := createNodes(sess, nodes) + err = generateCurRels("", &rootVal, 0, depth, &curRels) if err != nil { return err } - //no relations to make - if len(ids) == 1 { + dels := calculateDels(oldRels, curRels) + + var wg sync.WaitGroup + var err1, err2, err3 error + //fix the cur rels and write them to their perspective nodes + wg.Add(1) + go func(wg *sync.WaitGroup, _curRels *map[string]map[string]*RelationConfig, _nodeRef *map[string]*reflect.Value, _ids *map[string]int64, _err *error) { + for uuid, val := range *_nodeRef { + loadConf, ok := (*_curRels)[uuid] + if !ok { + *_err = fmt.Errorf("load config not found for node [%s]", uuid) + wg.Done() + return + } + + //handle if its a pointer + if val.Kind() == reflect.Ptr { + *val = val.Elem() + } + + reflect.Indirect(*val).FieldByName("LoadMap").Set(reflect.ValueOf(loadConf)) + } + + wg.Done() + }(&wg, &curRels, &nodeRef, &ids, &err3) + + //execute concurrently + //calculate dels + + if len(dels) != 0 { + wg.Add(1) + + go func(wg *sync.WaitGroup, _dels map[string][]int64, _conn *driver.BoltConn, _err *error) { + err := removeRelations(_conn, _dels) + if err != nil { + *_err = err + } + wg.Done() + }(&wg, dels, sess, &err1) + } + + if len(relations) != 0 { + wg.Add(1) + go func(wg *sync.WaitGroup, _conn *driver.BoltConn, _relations map[string][]relCreateConf, _ids map[string]int64, _err *error) { + err := relateNodes(_conn, _relations, _ids) + if err != nil { + *_err = err + } + wg.Done() + }(&wg, sess, relations, ids, &err2) + } + + wg.Wait() + + if err1 != nil || err2 != nil || err3 != nil { + return fmt.Errorf("delErr=(%v) | relErr=(%v) | reallocErr=(%v)", err1, err2, err3) + } else { + return nil + } +} + +// calculates which relationships to delete +func calculateDels(oldRels, curRels map[string]map[string]*RelationConfig) map[string][]int64 { + if len(oldRels) == 0 { + return map[string][]int64{} + } + + dels := map[string][]int64{} + + for uuid, oldRelConf := range oldRels { + curRelConf, ok := curRels[uuid] + deleteAllRels := false + if !ok { + //this means that the node is gone, remove all rels to this node + deleteAllRels = true + } else { + for field, oldConf := range oldRelConf { + curConf, ok := curRelConf[field] + deleteAllRelsOnField := false + if !ok { + //this means that either the field has been removed or there are no more rels on this field, + //either way delete anything left over + deleteAllRelsOnField = true + } + for _, id := range oldConf.Ids { + //check if this id is new rels in the same location + if deleteAllRels || deleteAllRelsOnField { + if _, ok := dels[uuid]; !ok { + dels[uuid] = []int64{id} + } else { + dels[uuid] = append(dels[uuid], id) + } + } else { + if !int64SliceContains(curConf.Ids, id) { + if _, ok := dels[uuid]; !ok { + dels[uuid] = []int64{id} + } else { + dels[uuid] = append(dels[uuid], id) + } + } + } + } + } + } + } + + return dels +} + +// removes relationships between specified nodes +func removeRelations(conn *driver.BoltConn, dels map[string][]int64) error { + if dels == nil || len(dels) == 0 { return nil } - return relateNodes(sess, relations, ids) + if conn == nil { + return fmt.Errorf("connection can not be nil, %w", ErrInternal) + } + + var params []interface{} + + for uuid, ids := range dels { + params = append(params, map[string]interface{}{ + "startNodeId": uuid, + "endNodeIds": ids, + }) + } + + startParams, err := dsl.ParamsFromMap(map[string]interface{}{ + "uuid": dsl.ParamString("row.startNodeId"), + }) + if err != nil { + return fmt.Errorf("%s, %w", err.Error(), ErrInternal) + } + + res, err := dsl.QB(). + Cypher("UNWIND {rows} as row"). + Match(dsl.Path(). + V(dsl.V{ + Name: "start", + Params: startParams, + }).E(dsl.E{ + Name: "e", + }).V(dsl.V{ + Name: "end", + }).Build()). + Cypher("WHERE id(end) IN row.endNodeIds"). + Delete(false, "e"). + WithNeo(conn). + Exec(map[string]interface{}{ + "rows": params, + }, + ) + if err != nil { + return fmt.Errorf("%s, %w", err.Error(), ErrInternal) + } + + if rows, err := res.RowsAffected(); err != nil { + return fmt.Errorf("%s, %w", err.Error(), ErrInternal) + } else if int(rows) != len(dels) { + return fmt.Errorf("sanity check failed, rows affected [%v] not equal to num deletions [%v], %w", rows, len(dels), ErrInternal) + } else { + return nil + } } -func createNodes(conn *driver.BoltConn, crNodes map[string]map[string]nodeCreateConf) (map[string]int64, error) { +// creates nodes +func createNodes(conn *driver.BoltConn, crNodes map[string]map[string]nodeCreateConf, nodeRef *map[string]*reflect.Value) (map[string]int64, error) { idMap := map[string]int64{} for label, nodes := range crNodes { @@ -146,7 +350,24 @@ func createNodes(conn *driver.BoltConn, crNodes map[string]map[string]nodeCreate continue } - idMap[row[0].(string)] = row[1].(int64) + uuid, ok := row[0].(string) + if !ok { + return nil, fmt.Errorf("cannot cast row[0] to string, %w", ErrInternal) + } + + graphId, ok := row[1].(int64) + if !ok { + return nil, fmt.Errorf("cannot cast row[1] to int64, %w", ErrInternal) + } + + idMap[uuid] = graphId + //set the new id + val, ok := (*nodeRef)[uuid] + if !ok { + return nil, fmt.Errorf("cannot find val for uuid [%s]", uuid) + } + + reflect.Indirect(*val).FieldByName("Id").Set(reflect.ValueOf(graphId)) } err = res.Close() @@ -158,6 +379,7 @@ func createNodes(conn *driver.BoltConn, crNodes map[string]map[string]nodeCreate return idMap, nil } +// relateNodes connects nodes together using edge config func relateNodes(conn *driver.BoltConn, relations map[string][]relCreateConf, ids map[string]int64) error { if relations == nil || len(relations) == 0 { return errors.New("relations can not be nil or empty") @@ -258,6 +480,7 @@ func relateNodes(conn *driver.BoltConn, relations map[string][]relCreateConf, id return nil } +// validates data used by parse struct func parseValidate(currentDepth, maxDepth int, current *reflect.Value, nodesPtr *map[string]map[string]nodeCreateConf, relationsPtr *map[string][]relCreateConf) error { if currentDepth > maxDepth { return nil @@ -274,7 +497,131 @@ func parseValidate(currentDepth, maxDepth int, current *reflect.Value, nodesPtr return nil } -func parseStruct(parentId, edgeLabel string, parentIsStart bool, direction dsl.Direction, edgeParams map[string]interface{}, current *reflect.Value, currentDepth int, maxDepth int, nodesPtr *map[string]map[string]nodeCreateConf, relationsPtr *map[string][]relCreateConf) error { +// generates load map for updated structs +func generateCurRels(parentId string, current *reflect.Value, currentDepth, maxDepth int, curRels *map[string]map[string]*RelationConfig) error { + if currentDepth > maxDepth { + return nil + } + + uuid := reflect.Indirect(*current).FieldByName("UUID").String() + if uuid == "" { + return errors.New("uuid not set") + } + + if _, ok := (*curRels)[uuid]; ok { + //this node has already been seen + return nil + } + + //get the type + tString, err := getTypeName(current.Type()) + if err != nil { + return err + } + + //get the config + actual, ok := mappedTypes.Get(tString) + if !ok { + return fmt.Errorf("struct config not found type (%s)", tString) + } + + //cast the config + currentConf, ok := actual.(structDecoratorConfig) + if !ok { + return errors.New("unable to cast into struct decorator config") + } + for _, conf := range currentConf.Fields { + if conf.Relationship == "" { + continue + } + + relField := reflect.Indirect(*current).FieldByName(conf.FieldName) + + //if its nil, just skip it + if relField.IsNil() { + continue + } + + if conf.ManyRelationship { + slLen := relField.Len() + if slLen == 0 { + continue + } + + for i := 0; i < slLen; i++ { + relVal := relField.Index(i) + + newParentId, _, _, _, _, followVal, followId, _, err := processStruct(conf, &relVal, uuid, parentId) + if err != nil { + return err + } + + //makes us go backwards + //if skip { + // continue + //} + + //check that the map is there for this id + if _, ok := (*curRels)[uuid]; !ok { + (*curRels)[uuid] = map[string]*RelationConfig{} + } + + //check the config is there for the specified field + if _, ok = (*curRels)[uuid][conf.FieldName]; !ok { + (*curRels)[uuid][conf.FieldName] = &RelationConfig{ + Ids: []int64{}, + RelationType: Multi, + } + } + + (*curRels)[uuid][conf.FieldName].Ids = append((*curRels)[uuid][conf.FieldName].Ids, followId) + + err = generateCurRels(newParentId, followVal, currentDepth+1, maxDepth, curRels) + if err != nil { + return err + } + } + } else { + newParentId, _, _, _, _, followVal, followId, _, err := processStruct(conf, &relField, uuid, parentId) + if err != nil { + return err + } + + //makes us go backwards + //if skip { + // continue + //} + + //check that the map is there for this id + if _, ok := (*curRels)[uuid]; !ok { + (*curRels)[uuid] = map[string]*RelationConfig{} + } + + //check the config is there for the specified field + if _, ok = (*curRels)[uuid][conf.FieldName]; !ok { + (*curRels)[uuid][conf.FieldName] = &RelationConfig{ + Ids: []int64{}, + RelationType: Single, + } + } + + (*curRels)[uuid][conf.FieldName].Ids = append((*curRels)[uuid][conf.FieldName].Ids, followId) + + err = generateCurRels(newParentId, followVal, currentDepth+1, maxDepth, curRels) + if err != nil { + return err + } + + } + } + + return nil +} + +// parses tree of structs +func parseStruct(parentId, edgeLabel string, parentIsStart bool, direction dsl.Direction, edgeParams map[string]interface{}, current *reflect.Value, + currentDepth, maxDepth int, nodesPtr *map[string]map[string]nodeCreateConf, relationsPtr *map[string][]relCreateConf, oldRels *map[string]map[string]*RelationConfig, + newNodes *[]*string, nodeRef *map[string]*reflect.Value) error { //check if its done if currentDepth > maxDepth { return nil @@ -312,6 +659,30 @@ func parseStruct(parentId, edgeLabel string, parentIsStart bool, direction dsl.D return err } + if !isNewNode { + if _, ok := (*oldRels)[id]; !ok { + iConf := reflect.Indirect(*current).FieldByName("LoadMap").Interface() + + var relConf map[string]*RelationConfig + + if iConf != nil { + relConf, ok = iConf.(map[string]*RelationConfig) + if !ok { + relConf = map[string]*RelationConfig{} + } + } + + (*oldRels)[id] = relConf + } + } else { + *newNodes = append(*newNodes, &id) + } + + //set the reflect pointer so we can update the map later + if _, ok := (*nodeRef)[id]; !ok { + (*nodeRef)[id] = current + } + //convert params params, err := toCypherParamsMap(*current, currentConf) if err != nil { @@ -384,22 +755,23 @@ func parseStruct(parentId, edgeLabel string, parentIsStart bool, direction dsl.D for i := 0; i < slLen; i++ { relVal := relField.Index(i) - newParentId, newEdgeLabel, newParentIdStart, newDirection, newEdgeParams, followVal, skip, err := processStruct(conf, &relVal, id, parentId) + newParentId, newEdgeLabel, newParentIdStart, newDirection, newEdgeParams, followVal, _, skip, err := processStruct(conf, &relVal, id, parentId) if err != nil { return err } + //makes us go backwards if skip { continue } - err = parseStruct(newParentId, newEdgeLabel, newParentIdStart, newDirection, newEdgeParams, followVal, currentDepth+1, maxDepth, nodesPtr, relationsPtr) + err = parseStruct(newParentId, newEdgeLabel, newParentIdStart, newDirection, newEdgeParams, followVal, currentDepth+1, maxDepth, nodesPtr, relationsPtr, oldRels, newNodes, nodeRef) if err != nil { return err } } } else { - newParentId, newEdgeLabel, newParentIdStart, newDirection, newEdgeParams, followVal, skip, err := processStruct(conf, &relField, id, parentId) + newParentId, newEdgeLabel, newParentIdStart, newDirection, newEdgeParams, followVal, _, skip, err := processStruct(conf, &relField, id, parentId) if err != nil { return err } @@ -408,7 +780,7 @@ func parseStruct(parentId, edgeLabel string, parentIsStart bool, direction dsl.D continue } - err = parseStruct(newParentId, newEdgeLabel, newParentIdStart, newDirection, newEdgeParams, followVal, currentDepth+1, maxDepth, nodesPtr, relationsPtr) + err = parseStruct(newParentId, newEdgeLabel, newParentIdStart, newDirection, newEdgeParams, followVal, currentDepth+1, maxDepth, nodesPtr, relationsPtr, oldRels, newNodes, nodeRef) if err != nil { return err } @@ -418,22 +790,23 @@ func parseStruct(parentId, edgeLabel string, parentIsStart bool, direction dsl.D return nil } -func processStruct(fieldConf decoratorConfig, relVal *reflect.Value, id, oldParentId string) (parentId, edgeLabel string, parentIsStart bool, direction dsl.Direction, edgeParams map[string]interface{}, followVal *reflect.Value, skip bool, err error) { +// processStruct generates configuration for individual struct for saving +func processStruct(fieldConf decoratorConfig, relVal *reflect.Value, id, oldParentId string) (parentId, edgeLabel string, parentIsStart bool, direction dsl.Direction, edgeParams map[string]interface{}, followVal *reflect.Value, followId int64, skip bool, err error) { edgeLabel = fieldConf.Relationship relValName, err := getTypeName(relVal.Type()) if err != nil { - return "", "", false, 0, nil, nil, false, err + return "", "", false, 0, nil, nil, -1, false, err } actual, ok := mappedTypes.Get(relValName) if !ok { - return "", "", false, 0, nil, nil, false, fmt.Errorf("cannot find config for %s", edgeLabel) + return "", "", false, 0, nil, nil, -1, false, fmt.Errorf("cannot find config for %s", edgeLabel) } edgeConf, ok := actual.(structDecoratorConfig) if !ok { - return "", "", false, 0, nil, nil, false, errors.New("can not cast to structDecoratorConfig") + return "", "", false, 0, nil, nil, -1, false, errors.New("can not cast to structDecoratorConfig") } if relVal.Type().Implements(edgeType) { @@ -441,7 +814,7 @@ func processStruct(fieldConf decoratorConfig, relVal *reflect.Value, id, oldPare endValSlice := relVal.MethodByName("GetEndNode").Call(nil) if len(startValSlice) == 0 || len(endValSlice) == 0 { - return "", "", false, 0, nil, nil, false, errors.New("edge is invalid, sides are not set") + return "", "", false, 0, nil, nil, -1, false, errors.New("edge is invalid, sides are not set") } startId := reflect.Indirect(startValSlice[0].Elem()).FieldByName("UUID").String() @@ -449,7 +822,7 @@ func processStruct(fieldConf decoratorConfig, relVal *reflect.Value, id, oldPare params, err := toCypherParamsMap(*relVal, edgeConf) if err != nil { - return "", "", false, 0, nil, nil, false, err + return "", "", false, 0, nil, nil, -1, false, err } //if its nil, just default it @@ -459,37 +832,72 @@ func processStruct(fieldConf decoratorConfig, relVal *reflect.Value, id, oldPare if startId == id { + //follow the end + retVal := endValSlice[0].Elem() + + Iid := reflect.Indirect(retVal).FieldByName("Id").Interface() + + followId, ok := Iid.(int64) + if !ok { + followId = 0 + } + //check that we're not going in circles if oldParentId != "" { if endId == oldParentId { - return "", "", false, 0, nil, &reflect.Value{}, true, nil + return startId, edgeLabel, true, fieldConf.Direction, params, &retVal, followId, true, nil } } - //follow the end - retVal := endValSlice[0].Elem() - return startId, edgeLabel, true, fieldConf.Direction, params, &retVal, false, nil + return startId, edgeLabel, true, fieldConf.Direction, params, &retVal, followId, false, nil } else if endId == id { ///follow the start retVal := startValSlice[0].Elem() + + Iid := reflect.Indirect(retVal).FieldByName("Id").Interface() + + followId, ok := Iid.(int64) + if !ok { + followId = 0 + } + if oldParentId != "" { if startId == oldParentId { - return "", "", false, 0, nil, &reflect.Value{}, true, nil + return endId, edgeLabel, false, fieldConf.Direction, params, &retVal, followId, true, nil } } - return endId, edgeLabel, false, fieldConf.Direction, params, &retVal, false, nil + + return endId, edgeLabel, false, fieldConf.Direction, params, &retVal, followId, false, nil } else { - return "", "", false, 0, nil, nil, false, errors.New("edge is invalid, doesn't point to parent vertex") + return "", "", false, 0, nil, nil, -1, false, errors.New("edge is invalid, doesn't point to parent vertex") } } else { + var followId int64 + if oldParentId != "" { if relVal.Kind() == reflect.Ptr { *relVal = relVal.Elem() } + + Iid := reflect.Indirect(*relVal).FieldByName("Id").Interface() + + followId, ok = Iid.(int64) + if !ok { + followId = 0 + } + if relVal.FieldByName("UUID").String() == oldParentId { - return "", "", false, 0, nil, &reflect.Value{}, true, nil + return id, edgeLabel, fieldConf.Direction == dsl.DirectionOutgoing, fieldConf.Direction, map[string]interface{}{}, relVal, followId, true, nil + } + } else { + Iid := reflect.Indirect(*relVal).FieldByName("Id").Interface() + + followId, ok = Iid.(int64) + if !ok { + followId = 0 } } - return id, edgeLabel, fieldConf.Direction == dsl.DirectionOutgoing, fieldConf.Direction, map[string]interface{}{}, relVal, false, nil + + return id, edgeLabel, fieldConf.Direction == dsl.DirectionOutgoing, fieldConf.Direction, map[string]interface{}{}, relVal, followId, false, nil } } diff --git a/save_test.go b/save_test.go index 211e37b..10e5430 100644 --- a/save_test.go +++ b/save_test.go @@ -1,11 +1,28 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm import ( - driver "github.com/mindstand/golang-neo4j-bolt-driver" "github.com/stretchr/testify/require" "reflect" "testing" - "time" ) func TestParseStruct(t *testing.T) { @@ -26,14 +43,28 @@ func parseO2O(req *require.Assertions) { TestField: "test", TestTypeDefString: "dasdfas", TestTypeDefInt: 600, - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 1, + UUID: "comp1uuid", + LoadMap: map[string]*RelationConfig{ + "SingleSpecA": { + Ids: []int64{2}, + RelationType: Single, + }, + }, }, } b1 := &b{ - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 2, + UUID: "b1uuid", + LoadMap: map[string]*RelationConfig{ + "SingleSpec": { + Ids: []int64{1}, + RelationType: Single, + }, + }, }, TestField: "test", } @@ -49,15 +80,24 @@ func parseO2O(req *require.Assertions) { nodes := map[string]map[string]nodeCreateConf{} relations := map[string][]relCreateConf{} + oldRels := map[string]map[string]*RelationConfig{} + curRels := map[string]map[string]*RelationConfig{} + ids := []*string{} val := reflect.ValueOf(comp1) + nodeRef := map[string]*reflect.Value{} - err := parseStruct("", "", false, 0, nil, &val, 0, 5, &nodes, &relations) - req.Nil(err) + req.Nil(parseStruct("", "", false, 0, nil, &val, 0, 5, &nodes, &relations, &oldRels, &ids, &nodeRef)) + req.Nil(generateCurRels("", &val, 0, 5, &curRels)) req.Equal(2, len(nodes)) req.Equal(1, len(nodes["a"])) req.Equal(1, len(nodes["b"])) req.Equal(1, len(relations)) + req.Equal(2, len(oldRels)) + req.Equal(2, len(curRels)) + req.Equal(int64(2), curRels["comp1uuid"]["SingleSpecA"].Ids[0]) + req.Equal(int64(1), curRels["b1uuid"]["SingleSpec"].Ids[0]) + req.EqualValues(oldRels, curRels) } func parseM2O(req *require.Assertions) { @@ -66,16 +106,30 @@ func parseM2O(req *require.Assertions) { TestField: "test", TestTypeDefString: "dasdfas", TestTypeDefInt: 600, - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 1, + UUID: "a1uuid", + LoadMap: map[string]*RelationConfig{ + "ManyA": { + Ids: []int64{2}, + RelationType: Multi, + }, + }, }, - ManyA: []*b{}, + ManyA: []*b{}, } b1 := &b{ TestField: "test", - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 2, + UUID: "b1uuid", + LoadMap: map[string]*RelationConfig{ + "ManyB": { + Ids: []int64{1}, + RelationType: Single, + }, + }, }, } @@ -84,15 +138,19 @@ func parseM2O(req *require.Assertions) { nodes := map[string]map[string]nodeCreateConf{} relations := map[string][]relCreateConf{} + oldRels := map[string]map[string]*RelationConfig{} + curRels := map[string]map[string]*RelationConfig{} + ids := []*string{} val := reflect.ValueOf(a1) - - err := parseStruct("", "", false, 0, nil, &val, 0, 5, &nodes, &relations) - req.Nil(err) + nodeRef := map[string]*reflect.Value{} + req.Nil(parseStruct("", "", false, 0, nil, &val, 0, 5, &nodes, &relations, &oldRels, &ids, &nodeRef)) + req.Nil(generateCurRels("", &val, 0, 5, &curRels)) req.Equal(2, len(nodes)) req.Equal(1, len(nodes["a"])) req.Equal(1, len(nodes["b"])) req.Equal(1, len(relations)) + req.EqualValues(oldRels, curRels) } func parseM2M(req *require.Assertions) { @@ -101,19 +159,33 @@ func parseM2M(req *require.Assertions) { TestField: "test", TestTypeDefString: "dasdfas", TestTypeDefInt: 600, - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 1, + UUID: "a1uuid", + LoadMap: map[string]*RelationConfig{ + "MultiA": { + Ids: []int64{2}, + RelationType: Multi, + }, + }, }, - ManyA: []*b{}, - MultiA: []*b{}, + ManyA: []*b{}, + MultiA: []*b{}, } b1 := &b{ TestField: "test", - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 2, + UUID: "b1uuid", + LoadMap: map[string]*RelationConfig{ + "Multi": { + Ids: []int64{1}, + RelationType: Multi, + }, + }, }, - Multi: []*a{}, + Multi: []*a{}, } b1.Multi = append(b1.Multi, a1) @@ -121,52 +193,149 @@ func parseM2M(req *require.Assertions) { nodes := map[string]map[string]nodeCreateConf{} relations := map[string][]relCreateConf{} + oldRels := map[string]map[string]*RelationConfig{} + curRels := map[string]map[string]*RelationConfig{} + ids := []*string{} + + nodeRef := map[string]*reflect.Value{} val := reflect.ValueOf(a1) - err := parseStruct("", "", false, 0, nil, &val, 0, 5, &nodes, &relations) - req.Nil(err) + req.Nil(parseStruct("", "", false, 0, nil, &val, 0, 5, &nodes, &relations, &oldRels, &ids, &nodeRef)) + req.Nil(generateCurRels("", &val, 0, 5, &curRels)) req.Equal(2, len(nodes)) req.Equal(1, len(nodes["a"])) req.Equal(1, len(nodes["b"])) req.Equal(1, len(relations)) + req.EqualValues(oldRels, curRels) } -func TestSave(t *testing.T) { - t.Skip() +func TestCalculateDels(t *testing.T) { req := require.New(t) - req.Nil(setupInit(true, nil, &a{}, &b{}, &c{})) - - comp2 := &a{ - TestField: "test", - embedTest: embedTest{ - Id: 1, + //test node removed + dels := calculateDels(map[string]map[string]*RelationConfig{ + "node1": { + "RelField": { + Ids: []int64{2}, + RelationType: Single, + }, }, - } - - b2 := &b{ - TestField: "test", - TestTime: time.Now().UTC(), - embedTest: embedTest{ - Id: 2, + "node2": { + "RelField2": { + Ids: []int64{1}, + RelationType: Single, + }, }, - } - - c1 := &c{ - Start: comp2, - End: b2, - Test: "testing", - } - - comp2.SingleSpecA = c1 - b2.SingleSpec = c1 - - conn, err := driverPool.Open(driver.ReadWriteMode) - if err != nil { - require.Nil(t, err) - } - defer driverPool.Reclaim(conn) + }, map[string]map[string]*RelationConfig{ + "node1": { + "RelField": { + Ids: []int64{}, + RelationType: Single, + }, + }, + }) + + req.EqualValues(map[string][]int64{ + "node1": {2}, + }, dels) + + //test field removed + dels = calculateDels(map[string]map[string]*RelationConfig{ + "node1": { + "RelField": { + Ids: []int64{2}, + RelationType: Single, + }, + }, + "node2": { + "RelField2": { + Ids: []int64{1}, + RelationType: Single, + }, + }, + }, map[string]map[string]*RelationConfig{ + "node1": { + "RelField": { + Ids: []int64{}, + RelationType: Single, + }, + }, + "node2": { + "RelFieldNew": { + Ids: []int64{}, + RelationType: Single, + }, + }, + }) + + req.EqualValues(map[string][]int64{ + "node1": {2}, + "node2": {1}, + }, dels) + + //test field empty + dels = calculateDels(map[string]map[string]*RelationConfig{ + "node1": { + "RelField": { + Ids: []int64{2}, + RelationType: Single, + }, + }, + "node2": { + "RelField2": { + Ids: []int64{1}, + RelationType: Single, + }, + }, + }, map[string]map[string]*RelationConfig{ + "node1": { + "RelField": { + Ids: []int64{}, + RelationType: Single, + }, + }, + "node2": { + "RelField2": { + Ids: []int64{}, + RelationType: Single, + }, + }, + }) + + req.EqualValues(map[string][]int64{ + "node1": {2}, + "node2": {1}, + }, dels) + + //test nothing changed + dels = calculateDels(map[string]map[string]*RelationConfig{ + "node1": { + "RelField": { + Ids: []int64{2}, + RelationType: Single, + }, + }, + "node2": { + "RelField2": { + Ids: []int64{1}, + RelationType: Single, + }, + }, + }, map[string]map[string]*RelationConfig{ + "node1": { + "RelField": { + Ids: []int64{2}, + RelationType: Single, + }, + }, + "node2": { + "RelField2": { + Ids: []int64{1}, + RelationType: Single, + }, + }, + }) - req.Nil(saveDepth(conn, comp2, defaultSaveDepth)) + req.EqualValues(map[string][]int64{}, dels) } diff --git a/session.go b/session.go index 871a1d6..15410d5 100644 --- a/session.go +++ b/session.go @@ -1,3 +1,22 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm import ( diff --git a/session_test.go b/session_test.go deleted file mode 100644 index 0bc3578..0000000 --- a/session_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package gogm - -import ( - "github.com/stretchr/testify/require" - "testing" -) - -func TestSession(t *testing.T) { - if !testing.Short() { - t.Skip() - return - } - - req := require.New(t) - - conf := Config{ - Username: "neo4j", - Password: "password", - Host: "0.0.0.0", - Port: 7687, - PoolSize: 15, - IndexStrategy: VALIDATE_INDEX, - } - - req.Nil(Init(&conf, &a{}, &b{}, &c{})) - - req.EqualValues(3, mappedTypes.Len()) - - sess, err := NewSession(true) - req.NotNil(err) - - var stuffs []a - - req.Nil(sess.LoadAll(&stuffs)) - - req.EqualValues(1, len(stuffs)) -} diff --git a/setup_test.go b/setup_test.go deleted file mode 100644 index 8b79b3c..0000000 --- a/setup_test.go +++ /dev/null @@ -1 +0,0 @@ -package gogm diff --git a/testing_/linking.go b/testing_/linking.go new file mode 100644 index 0000000..945e8d7 --- /dev/null +++ b/testing_/linking.go @@ -0,0 +1,311 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +// code generated by gogm; DO NOT EDIT +package testing_ + +import ( + "errors" +) + +func (l *ExampleObject) LinkToExampleObject2OnFieldSpecial(target *ExampleObject2, edge *SpecialEdge) error { + if target == nil { + return errors.New("start and end can not be nil") + } + + if edge == nil { + return errors.New("edge can not be nil") + } + + err := edge.SetStartNode(l) + if err != nil { + return err + } + + err = edge.SetEndNode(target) + if err != nil { + return err + } + + l.Special = edge + + if target.Special == nil { + target.Special = make([]*SpecialEdge, 1, 1) + target.Special[0] = edge + } else { + target.Special = append(target.Special, edge) + } + + return nil +} + +func (l *ExampleObject) UnlinkFromExampleObject2OnFieldSpecial(target *ExampleObject2) error { + if target == nil { + return errors.New("start and end can not be nil") + } + + l.Special = nil + + if target.Special != nil { + for i, unlinkTarget := range target.Special { + + obj := unlinkTarget.GetStartNode() + + checkObj, ok := obj.(*ExampleObject) + if !ok { + return errors.New("unable to cast unlinkTarget to [ExampleObject]") + } + if checkObj.UUID == l.UUID { + a := &target.Special + (*a)[i] = (*a)[len(*a)-1] + (*a)[len(*a)-1] = nil + *a = (*a)[:len(*a)-1] + break + } + } + } + + return nil +} + +func (l *ExampleObject) LinkToExampleObjectOnFieldChildren(targets ...*ExampleObject) error { + if targets == nil { + return errors.New("start and end can not be nil") + } + + for _, target := range targets { + + if l.Children == nil { + l.Children = make([]*ExampleObject, 1, 1) + l.Children[0] = target + } else { + l.Children = append(l.Children, target) + } + + target.Parents = l + } + + return nil +} + +func (l *ExampleObject) UnlinkFromExampleObjectOnFieldChildren(targets ...*ExampleObject) error { + if targets == nil { + return errors.New("start and end can not be nil") + } + + for _, target := range targets { + + if l.Children != nil { + for i, unlinkTarget := range l.Children { + if unlinkTarget.UUID == target.UUID { + a := &l.Children + (*a)[i] = (*a)[len(*a)-1] + (*a)[len(*a)-1] = nil + *a = (*a)[:len(*a)-1] + break + } + } + } + + target.Parents = nil + } + + return nil +} +func (l *ExampleObject) LinkToExampleObjectOnFieldParents(target *ExampleObject) error { + if target == nil { + return errors.New("start and end can not be nil") + } + + l.Parents = target + + if target.Children == nil { + target.Children = make([]*ExampleObject, 1, 1) + target.Children[0] = l + } else { + target.Children = append(target.Children, l) + } + + return nil +} + +func (l *ExampleObject) UnlinkFromExampleObjectOnFieldParents(target *ExampleObject) error { + if target == nil { + return errors.New("start and end can not be nil") + } + + l.Parents = nil + + if target.Children != nil { + for i, unlinkTarget := range target.Children { + if unlinkTarget.UUID == l.UUID { + a := &target.Children + (*a)[i] = (*a)[len(*a)-1] + (*a)[len(*a)-1] = nil + *a = (*a)[:len(*a)-1] + break + } + } + } + + return nil +} + +func (l *ExampleObject2) LinkToExampleObjectOnFieldSpecial(target *ExampleObject, edge *SpecialEdge) error { + if target == nil { + return errors.New("start and end can not be nil") + } + + if edge == nil { + return errors.New("edge can not be nil") + } + + err := edge.SetStartNode(target) + if err != nil { + return err + } + + err = edge.SetEndNode(l) + if err != nil { + return err + } + + if l.Special == nil { + l.Special = make([]*SpecialEdge, 1, 1) + l.Special[0] = edge + } else { + l.Special = append(l.Special, edge) + } + + target.Special = edge + + return nil +} + +func (l *ExampleObject2) UnlinkFromExampleObjectOnFieldSpecial(target *ExampleObject) error { + if target == nil { + return errors.New("start and end can not be nil") + } + + if l.Special != nil { + for i, unlinkTarget := range l.Special { + + obj := unlinkTarget.GetEndNode() + + checkObj, ok := obj.(*ExampleObject) + if !ok { + return errors.New("unable to cast unlinkTarget to [ExampleObject]") + } + if checkObj.UUID == target.UUID { + a := &l.Special + (*a)[i] = (*a)[len(*a)-1] + (*a)[len(*a)-1] = nil + *a = (*a)[:len(*a)-1] + break + } + } + } + + target.Special = nil + + return nil +} + +func (l *ExampleObject2) LinkToExampleObject2OnFieldChildren2(targets ...*ExampleObject2) error { + if targets == nil { + return errors.New("start and end can not be nil") + } + + for _, target := range targets { + + if l.Children2 == nil { + l.Children2 = make([]*ExampleObject2, 1, 1) + l.Children2[0] = target + } else { + l.Children2 = append(l.Children2, target) + } + + target.Parents2 = l + } + + return nil +} + +func (l *ExampleObject2) UnlinkFromExampleObject2OnFieldChildren2(targets ...*ExampleObject2) error { + if targets == nil { + return errors.New("start and end can not be nil") + } + + for _, target := range targets { + + if l.Children2 != nil { + for i, unlinkTarget := range l.Children2 { + if unlinkTarget.UUID == target.UUID { + a := &l.Children2 + (*a)[i] = (*a)[len(*a)-1] + (*a)[len(*a)-1] = nil + *a = (*a)[:len(*a)-1] + break + } + } + } + + target.Parents2 = nil + } + + return nil +} +func (l *ExampleObject2) LinkToExampleObject2OnFieldParents2(target *ExampleObject2) error { + if target == nil { + return errors.New("start and end can not be nil") + } + + l.Parents2 = target + + if target.Children2 == nil { + target.Children2 = make([]*ExampleObject2, 1, 1) + target.Children2[0] = l + } else { + target.Children2 = append(target.Children2, l) + } + + return nil +} + +func (l *ExampleObject2) UnlinkFromExampleObject2OnFieldParents2(target *ExampleObject2) error { + if target == nil { + return errors.New("start and end can not be nil") + } + + l.Parents2 = nil + + if target.Children2 != nil { + for i, unlinkTarget := range target.Children2 { + if unlinkTarget.UUID == l.UUID { + a := &target.Children2 + (*a)[i] = (*a)[len(*a)-1] + (*a)[len(*a)-1] = nil + *a = (*a)[:len(*a)-1] + break + } + } + } + + return nil +} diff --git a/testing_/linking_test.go b/testing_/linking_test.go new file mode 100644 index 0000000..0a1929f --- /dev/null +++ b/testing_/linking_test.go @@ -0,0 +1,77 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package testing_ + +import ( + "github.com/mindstand/gogm" + "github.com/stretchr/testify/require" + "testing" +) + +func TestLinking(t *testing.T) { + req := require.New(t) + + id1 := "SDFdasasdf" + id2 := "aasdfasdfa" + + obj1 := &ExampleObject{ + BaseNode: gogm.BaseNode{ + Id: 0, + UUID: id1, + LoadMap: map[string]*gogm.RelationConfig{}, + }, + } + + obj2 := &ExampleObject{ + BaseNode: gogm.BaseNode{ + Id: 1, + UUID: id2, + LoadMap: map[string]*gogm.RelationConfig{}, + }, + } + + req.Nil(obj1.LinkToExampleObjectOnFieldParents(obj2)) + + req.Equal(1, len(obj2.Children)) + req.NotNil(obj1.Parents) + + req.Nil(obj1.UnlinkFromExampleObjectOnFieldParents(obj2)) + req.Equal(0, len(obj2.Children)) + req.Nil(obj1.Parents) + + // test special edge + specEdge := &SpecialEdge{ + SomeField: "asdfad", + } + + obj3 := &ExampleObject2{ + BaseNode: gogm.BaseNode{ + UUID: "adfadsfasd", + }, + } + + req.Nil(obj3.LinkToExampleObjectOnFieldSpecial(obj1, specEdge)) + req.Equal(obj1.Special.End.UUID, obj3.UUID) + req.Equal(1, len(obj3.Special)) + + req.Nil(obj1.UnlinkFromExampleObject2OnFieldSpecial(obj3)) + req.Nil(obj1.Special) + req.Equal(0, len(obj3.Special)) +} diff --git a/testing_/readme.md b/testing_/readme.md new file mode 100644 index 0000000..ffdc784 --- /dev/null +++ b/testing_/readme.md @@ -0,0 +1 @@ +This directory is used to test link and unlink generator from gogmcli. `linking.go` is generated by the gogmcli. \ No newline at end of file diff --git a/testing_/test_edge.go b/testing_/test_edge.go new file mode 100644 index 0000000..97e49fb --- /dev/null +++ b/testing_/test_edge.go @@ -0,0 +1,60 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package testing_ + +import ( + "github.com/mindstand/gogm" + "reflect" +) + +type SpecialEdge struct { + gogm.BaseNode + + Start *ExampleObject + End *ExampleObject2 + + SomeField string `gogm:"name=some_field"` +} + +func (s *SpecialEdge) GetStartNode() interface{} { + return s.Start +} + +func (s *SpecialEdge) GetStartNodeType() reflect.Type { + return reflect.TypeOf(&ExampleObject{}) +} + +func (s *SpecialEdge) SetStartNode(v interface{}) error { + s.Start = v.(*ExampleObject) + return nil +} + +func (s *SpecialEdge) GetEndNode() interface{} { + return s.End +} + +func (s *SpecialEdge) GetEndNodeType() reflect.Type { + return reflect.TypeOf(&ExampleObject2{}) +} + +func (s *SpecialEdge) SetEndNode(v interface{}) error { + s.End = v.(*ExampleObject2) + return nil +} diff --git a/testing_/test_obj.go b/testing_/test_obj.go new file mode 100644 index 0000000..4106462 --- /dev/null +++ b/testing_/test_obj.go @@ -0,0 +1,30 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package testing_ + +import "github.com/mindstand/gogm" + +type ExampleObject struct { + gogm.BaseNode + + Children []*ExampleObject `gogm:"direction=incoming;relationship=test" json:"children"` + Parents *ExampleObject `gogm:"direction=outgoing;relationship=test" json:"parents"` + Special *SpecialEdge `gogm:"direction=incoming;relationship=special" json:"special"` +} diff --git a/testing_/test_obj2.go b/testing_/test_obj2.go new file mode 100644 index 0000000..792add4 --- /dev/null +++ b/testing_/test_obj2.go @@ -0,0 +1,30 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package testing_ + +import "github.com/mindstand/gogm" + +type ExampleObject2 struct { + gogm.BaseNode + + Children2 []*ExampleObject2 `gogm:"direction=incoming;relationship=test" json:"children_2"` + Parents2 *ExampleObject2 `gogm:"direction=outgoing;relationship=test" json:"parents_2"` + Special []*SpecialEdge `gogm:"direction=outgoing;relationship=special" json:"special"` +} diff --git a/testing_/test_omit.go b/testing_/test_omit.go new file mode 100644 index 0000000..3b5892e --- /dev/null +++ b/testing_/test_omit.go @@ -0,0 +1,24 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package testing_ + +type OrdinaryNonGogmStruct struct { + SomeField string +} diff --git a/util.go b/util.go index 39e5730..63225ee 100644 --- a/util.go +++ b/util.go @@ -1,3 +1,22 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm import ( @@ -6,10 +25,12 @@ import ( "github.com/google/uuid" go_cypherdsl "github.com/mindstand/go-cypherdsl" "reflect" + "strings" "sync" "time" ) +// checks if integer is in slice func int64SliceContains(s []int64, e int64) bool { for _, a := range s { if a == e { @@ -19,6 +40,17 @@ func int64SliceContains(s []int64, e int64) bool { return false } +// checks if string is in slice +func stringSliceContains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + +// sets uuid for stuct if uuid field is empty func setUuidIfNeeded(val *reflect.Value, fieldName string) (bool, string, error) { if val == nil { return false, "", errors.New("value can not be nil") @@ -39,6 +71,7 @@ func setUuidIfNeeded(val *reflect.Value, fieldName string) (bool, string, error) return true, newUuid, nil } +// gets the type name from reflect type func getTypeName(val reflect.Type) (string, error) { if val.Kind() == reflect.Ptr { val = val.Elem() @@ -58,6 +91,7 @@ func getTypeName(val reflect.Type) (string, error) { } } +// converts struct fields to map that cypher can use func toCypherParamsMap(val reflect.Value, config structDecoratorConfig) (map[string]interface{}, error) { var err error defer func() { @@ -73,7 +107,7 @@ func toCypherParamsMap(val reflect.Value, config structDecoratorConfig) (map[str ret := map[string]interface{}{} for _, conf := range config.Fields { - if conf.Relationship != "" || conf.Name == "id" { + if conf.Relationship != "" || conf.Name == "id" || conf.Ignore { continue } @@ -206,6 +240,84 @@ func (r *relationConfigs) getConfig(nodeType, relationship, fieldType string, di } } +type validation struct { + Incoming []string + Outgoing []string + None []string + Both []string +} + +func (r *relationConfigs) Validate() error { + r.mutex.Lock() + defer r.mutex.Unlock() + + checkMap := map[string]*validation{} + + for title, confMap := range r.configs { + parts := strings.Split(title, "-") + if len(parts) != 2 { + return fmt.Errorf("invalid length for parts [%v] should be 2. Rel is [%s], %w", len(parts), title, ErrValidation) + } + + //vType := parts[0] + relType := parts[1] + + for field, configs := range confMap { + for _, config := range configs { + if _, ok := checkMap[relType]; !ok { + checkMap[relType] = &validation{ + Incoming: []string{}, + Outgoing: []string{}, + None: []string{}, + Both: []string{}, + } + } + + validate := checkMap[relType] + + switch config.Direction { + case go_cypherdsl.DirectionIncoming: + validate.Incoming = append(validate.Incoming, field) + break + case go_cypherdsl.DirectionOutgoing: + validate.Outgoing = append(validate.Outgoing, field) + break + case go_cypherdsl.DirectionNone: + validate.None = append(validate.None, field) + break + case go_cypherdsl.DirectionBoth: + validate.Both = append(validate.Both, field) + break + default: + return fmt.Errorf("unrecognized direction [%s], %w", config.Direction.ToString(), ErrValidation) + } + } + } + } + + for relType, validateConfig := range checkMap { + //check normal + if len(validateConfig.Outgoing) != len(validateConfig.Incoming) { + return fmt.Errorf("invalid directional configuration on relationship [%s], %w", relType, ErrValidation) + } + + //check both direction + if len(validateConfig.Both) != 0 { + if len(validateConfig.Both)%2 != 0 { + return fmt.Errorf("invalid length for 'both' validation, %w", ErrValidation) + } + } + + //check none direction + if len(validateConfig.None) != 0 { + if len(validateConfig.None)%2 != 0 { + return fmt.Errorf("invalid length for 'both' validation, %w", ErrValidation) + } + } + } + return nil +} + //isDifferentType, differentType, error func getActualTypeIfAliased(iType reflect.Type) (bool, reflect.Type, error) { if iType == nil { diff --git a/util/arrayOperations.go b/util/arrayOperations.go deleted file mode 100644 index 409e0eb..0000000 --- a/util/arrayOperations.go +++ /dev/null @@ -1,716 +0,0 @@ -package util - -import ( - "reflect" -) - -// Distinct returns the unique vals of a slice -// -// [1, 1, 2, 3] >> [1, 2, 3] -func Distinct(arr interface{}) (reflect.Value, bool) { - // create a slice from our input interface - slice, ok := takeArg(arr, reflect.Slice) - if !ok { - return reflect.Value{}, ok - } - - // put the values of our slice into a map - // the key's of the map will be the slice's unique values - c := slice.Len() - m := make(map[interface{}]bool) - for i := 0; i < c; i++ { - m[slice.Index(i).Interface()] = true - } - mapLen := len(m) - - // create the output slice and populate it with the map's keys - out := reflect.MakeSlice(reflect.TypeOf(arr), mapLen, mapLen) - i := 0 - for k := range m { - v := reflect.ValueOf(k) - o := out.Index(i) - o.Set(v) - i++ - } - - return out, ok -} - -// Intersect returns a slice of values that are present in all of the input slices -// -// [1, 1, 3, 4, 5, 6] & [2, 3, 6] >> [3, 6] -// -// [1, 1, 3, 4, 5, 6] >> [1, 3, 4, 5, 6] -func Intersect(arrs ...interface{}) (reflect.Value, bool) { - // create a map to count all the instances of the slice elems - arrLength := len(arrs) - var kind reflect.Kind - var kindHasBeenSet bool - - tempMap := make(map[interface{}]int) - for _, arg := range arrs { - tempArr, ok := Distinct(arg) - if !ok { - return reflect.Value{}, ok - } - - // check to be sure the type hasn't changed - if kindHasBeenSet && tempArr.Len() > 0 && tempArr.Index(0).Kind() != kind { - return reflect.Value{}, false - } - if tempArr.Len() > 0 { - kindHasBeenSet = true - kind = tempArr.Index(0).Kind() - } - - c := tempArr.Len() - for idx := 0; idx < c; idx++ { - // how many times have we encountered this elem? - if _, ok := tempMap[tempArr.Index(idx).Interface()]; ok { - tempMap[tempArr.Index(idx).Interface()]++ - } else { - tempMap[tempArr.Index(idx).Interface()] = 1 - } - } - } - - // find the keys equal to the length of the input args - numElems := 0 - for _, v := range tempMap { - if v == arrLength { - numElems++ - } - } - out := reflect.MakeSlice(reflect.TypeOf(arrs[0]), numElems, numElems) - i := 0 - for key, val := range tempMap { - if val == arrLength { - v := reflect.ValueOf(key) - o := out.Index(i) - o.Set(v) - i++ - } - } - - return out, true -} - -// Union returns a slice that contains the unique values of all the input slices -// -// [1, 2, 2, 4, 6] & [2, 4, 5] >> [1, 2, 4, 5, 6] -// -// [1, 1, 3, 4, 5, 6] >> [1, 3, 4, 5, 6] -func Union(arrs ...interface{}) (reflect.Value, bool) { - // create a temporary map to hold the contents of the arrays - tempMap := make(map[interface{}]uint8) - var kind reflect.Kind - var kindHasBeenSet bool - - // write the contents of the arrays as keys to the map. The map values don't matter - for _, arg := range arrs { - tempArr, ok := Distinct(arg) - if !ok { - return reflect.Value{}, ok - } - - // check to be sure the type hasn't changed - if kindHasBeenSet && tempArr.Len() > 0 && tempArr.Index(0).Kind() != kind { - return reflect.Value{}, false - } - if tempArr.Len() > 0 { - kindHasBeenSet = true - kind = tempArr.Index(0).Kind() - } - - c := tempArr.Len() - for idx := 0; idx < c; idx++ { - tempMap[tempArr.Index(idx).Interface()] = 0 - } - } - - // the map keys are now unique instances of all of the array contents - mapLen := len(tempMap) - out := reflect.MakeSlice(reflect.TypeOf(arrs[0]), mapLen, mapLen) - i := 0 - for key := range tempMap { - v := reflect.ValueOf(key) - o := out.Index(i) - o.Set(v) - i++ - } - - return out, true -} - -// Difference returns a slice of values that are only present in one of the input slices -// -// [1, 2, 2, 4, 6] & [2, 4, 5] >> [1, 5, 6] -// -// [1, 1, 3, 4, 5, 6] >> [1, 3, 4, 5, 6] -func Difference(arrs ...interface{}) (reflect.Value, bool) { - // create a temporary map to hold the contents of the arrays - tempMap := make(map[interface{}]int) - var kind reflect.Kind - var kindHasBeenSet bool - - for _, arg := range arrs { - tempArr, ok := Distinct(arg) - if !ok { - return reflect.Value{}, ok - } - - // check to be sure the type hasn't changed - if kindHasBeenSet && tempArr.Len() > 0 && tempArr.Index(0).Kind() != kind { - return reflect.Value{}, false - } - if tempArr.Len() > 0 { - kindHasBeenSet = true - kind = tempArr.Index(0).Kind() - } - - c := tempArr.Len() - for idx := 0; idx < c; idx++ { - // how many times have we encountered this elem? - if _, ok := tempMap[tempArr.Index(idx).Interface()]; ok { - tempMap[tempArr.Index(idx).Interface()]++ - } else { - tempMap[tempArr.Index(idx).Interface()] = 1 - } - } - } - - // write the final val of the diffMap to an array and return - numElems := 0 - for _, v := range tempMap { - if v == 1 { - numElems++ - } - } - out := reflect.MakeSlice(reflect.TypeOf(arrs[0]), numElems, numElems) - i := 0 - for key, val := range tempMap { - if val == 1 { - v := reflect.ValueOf(key) - o := out.Index(i) - o.Set(v) - i++ - } - } - - return out, true -} - -func takeArg(arg interface{}, kind reflect.Kind) (val reflect.Value, ok bool) { - val = reflect.ValueOf(arg) - if val.Kind() == kind { - ok = true - } - return -} - -/* *************************************************************** -* -* THE SECTIONS BELOW ARE DEPRECATED -* -/* *************************************************************** */ - -/* *************************************************************** -* -* THIS SECTION IS FOR STRINGS -* -/* *************************************************************** */ - -// IntersectString finds the intersection of two arrays. -// -// Deprecated: use Intersect instead. -func IntersectString(args ...[]string) []string { - // create a map to count all the instances of the strings - arrLength := len(args) - tempMap := make(map[string]int) - for _, arg := range args { - tempArr := DistinctString(arg) - for idx := range tempArr { - // how many times have we encountered this elem? - if _, ok := tempMap[tempArr[idx]]; ok { - tempMap[tempArr[idx]]++ - } else { - tempMap[tempArr[idx]] = 1 - } - } - } - - // find the keys equal to the length of the input args - tempArray := make([]string, 0) - for key, val := range tempMap { - if val == arrLength { - tempArray = append(tempArray, key) - } - } - - return tempArray -} - -// IntersectStringArr finds the intersection of two arrays using a multidimensional array as inputs -// -// Deprecated: use Intersect instead. -func IntersectStringArr(arr [][]string) []string { - // create a map to count all the instances of the strings - arrLength := len(arr) - tempMap := make(map[string]int) - for idx1 := range arr { - tempArr := DistinctString(arr[idx1]) - for idx2 := range tempArr { - // how many times have we encountered this elem? - if _, ok := tempMap[tempArr[idx2]]; ok { - tempMap[tempArr[idx2]]++ - } else { - tempMap[tempArr[idx2]] = 1 - } - } - } - - // find the keys equal to the length of the input args - tempArray := make([]string, 0) - for key, val := range tempMap { - if val == arrLength { - tempArray = append(tempArray, key) - } - } - - return tempArray -} - -// UnionString finds the union of two arrays. -// -// Deprecated: use Union instead. -func UnionString(args ...[]string) []string { - // create a temporary map to hold the contents of the arrays - tempMap := make(map[string]uint8) - - // write the contents of the arrays as keys to the map. The map values don't matter - for _, arg := range args { - for idx := range arg { - tempMap[arg[idx]] = 0 - } - } - - // the map keys are now unique instances of all of the array contents - tempArray := make([]string, 0) - for key := range tempMap { - tempArray = append(tempArray, key) - } - - return tempArray -} - -// UnionStringArr finds the union of two arrays using a multidimensional array as inputs -// -// Deprecated: use Union instead. -func UnionStringArr(arr [][]string) []string { - // create a temporary map to hold the contents of the arrays - tempMap := make(map[string]uint8) - - // write the contents of the arrays as keys to the map. The map values don't matter - for idx1 := range arr { - for idx2 := range arr[idx1] { - tempMap[arr[idx1][idx2]] = 0 - } - } - - // the map keys are now unique instances of all of the array contents - tempArray := make([]string, 0) - for key := range tempMap { - tempArray = append(tempArray, key) - } - - return tempArray -} - -// DifferenceString finds the difference of two arrays. -// -// Deprecated: use Difference instead. -func DifferenceString(args ...[]string) []string { - // create a temporary map to hold the contents of the arrays - tempMap := make(map[string]int) - for _, arg := range args { - tempArr := DistinctString(arg) - for idx := range tempArr { - // how many times have we encountered this elem? - if _, ok := tempMap[tempArr[idx]]; ok { - tempMap[tempArr[idx]]++ - } else { - tempMap[tempArr[idx]] = 1 - } - } - } - - // write the final val of the diffMap to an array and return - tempArray := make([]string, 0) - for key, val := range tempMap { - if val == 1 { - tempArray = append(tempArray, key) - } - } - - return tempArray -} - -// DifferenceStringArr finds the difference of two arrays using a multidimensional array as inputs -// -// Deprecated: use Difference instead. -func DifferenceStringArr(arr [][]string) []string { - // create a temporary map to hold the contents of the arrays - tempMap := make(map[string]int) - for idx1 := range arr { - tempArr := DistinctString(arr[idx1]) - for idx2 := range tempArr { - // how many times have we encountered this elem? - if _, ok := tempMap[tempArr[idx2]]; ok { - tempMap[tempArr[idx2]]++ - } else { - tempMap[tempArr[idx2]] = 1 - } - } - } - - // write the final val of the diffMap to an array and return - tempArray := make([]string, 0) - for key, val := range tempMap { - if val == 1 { - tempArray = append(tempArray, key) - } - } - - return tempArray -} - -// DistinctString removes duplicate values from one array. -// -// Deprecated: use Distinct instead. -func DistinctString(arg []string) []string { - tempMap := make(map[string]uint8) - - for idx := range arg { - tempMap[arg[idx]] = 0 - } - - tempArray := make([]string, 0) - for key := range tempMap { - tempArray = append(tempArray, key) - } - return tempArray -} - -/* *************************************************************** -* -* THIS SECTION IS FOR uint64's -* -/* *************************************************************** */ - -// IntersectUint64 finds the intersection of two arrays. -// -// Deprecated: use Intersect instead. -func IntersectUint64(args ...[]uint64) []uint64 { - // create a map to count all the instances of the strings - arrLength := len(args) - tempMap := make(map[uint64]int) - for _, arg := range args { - tempArr := DistinctUint64(arg) - for idx := range tempArr { - // how many times have we encountered this elem? - if _, ok := tempMap[tempArr[idx]]; ok { - tempMap[tempArr[idx]]++ - } else { - tempMap[tempArr[idx]] = 1 - } - } - } - - // find the keys equal to the length of the input args - tempArray := make([]uint64, 0) - for key, val := range tempMap { - if val == arrLength { - tempArray = append(tempArray, key) - } - } - - return tempArray -} - -// DistinctIntersectUint64 finds the intersection of two arrays of distinct vals. -// -// Deprecated: use Intersect instead. -func DistinctIntersectUint64(args ...[]uint64) []uint64 { - // create a map to count all the instances of the strings - arrLength := len(args) - tempMap := make(map[uint64]int) - for _, arg := range args { - for idx := range arg { - // how many times have we encountered this elem? - if _, ok := tempMap[arg[idx]]; ok { - tempMap[arg[idx]]++ - } else { - tempMap[arg[idx]] = 1 - } - } - } - - // find the keys equal to the length of the input args - tempArray := make([]uint64, 0) - for key, val := range tempMap { - if val == arrLength { - tempArray = append(tempArray, key) - } - } - - return tempArray -} - -func sortedIntersectUintHelper(a1 []uint64, a2 []uint64) []uint64 { - intersection := make([]uint64, 0) - n1 := len(a1) - n2 := len(a2) - i := 0 - j := 0 - for i < n1 && j < n2 { - switch { - case a1[i] > a2[j]: - j++ - case a2[j] > a1[i]: - i++ - default: - intersection = append(intersection, a1[i]) - i++ - j++ - } - } - return intersection -} - -// SortedIntersectUint64 finds the intersection of two sorted arrays. -// -// Deprecated: use Intersect instead. -func SortedIntersectUint64(args ...[]uint64) []uint64 { - // create an array to hold the intersection and write the first array to it - tempIntersection := args[0] - argsLen := len(args) - - for k := 1; k < argsLen; k++ { - // do we have any intersections? - switch len(tempIntersection) { - case 0: - // nope! Give them an empty array! - return tempIntersection - - default: - // yup, keep chugging - tempIntersection = sortedIntersectUintHelper(tempIntersection, args[k]) - } - } - - return tempIntersection -} - -// IntersectUint64Arr finds the intersection of two arrays using a multidimensional array as inputs -// -// Deprecated: use Intersect instead. -func IntersectUint64Arr(arr [][]uint64) []uint64 { - // create a map to count all the instances of the strings - arrLength := len(arr) - tempMap := make(map[uint64]int) - for idx1 := range arr { - tempArr := DistinctUint64(arr[idx1]) - for idx2 := range tempArr { - // how many times have we encountered this elem? - if _, ok := tempMap[tempArr[idx2]]; ok { - tempMap[tempArr[idx2]]++ - } else { - tempMap[tempArr[idx2]] = 1 - } - } - } - - // find the keys equal to the length of the input args - tempArray := make([]uint64, 0) - for key, val := range tempMap { - if val == arrLength { - tempArray = append(tempArray, key) - } - } - - return tempArray -} - -// SortedIntersectUint64Arr finds the intersection of two arrays using a multidimensional array as inputs -// -// Deprecated: use Intersect instead. -func SortedIntersectUint64Arr(arr [][]uint64) []uint64 { - // create an array to hold the intersection and write the first array to it - tempIntersection := arr[0] - argsLen := len(arr) - - for k := 1; k < argsLen; k++ { - // do we have any intersections? - switch len(tempIntersection) { - case 0: - // nope! Give them an empty array! - return tempIntersection - - default: - // yup, keep chugging - tempIntersection = sortedIntersectUintHelper(tempIntersection, arr[k]) - } - } - - return tempIntersection -} - -// DistinctIntersectUint64Arr finds the intersection of two distinct arrays using a multidimensional array as inputs -// -// Deprecated: use Distinct instead. -func DistinctIntersectUint64Arr(arr [][]uint64) []uint64 { - // create a map to count all the instances of the strings - arrLength := len(arr) - tempMap := make(map[uint64]int) - for idx1 := range arr { - for idx2 := range arr[idx1] { - // how many times have we encountered this elem? - if _, ok := tempMap[arr[idx1][idx2]]; ok { - tempMap[arr[idx1][idx2]]++ - } else { - tempMap[arr[idx1][idx2]] = 1 - } - } - } - - // find the keys equal to the length of the input args - tempArray := make([]uint64, 0) - for key, val := range tempMap { - if val == arrLength { - tempArray = append(tempArray, key) - } - } - - return tempArray -} - -// UnionUint64 finds the union of two arrays. -// -// Deprecated: use Union instead. -func UnionUint64(args ...[]uint64) []uint64 { - // create a temporary map to hold the contents of the arrays - tempMap := make(map[uint64]uint8) - - // write the contents of the arrays as keys to the map. The map values don't matter - for _, arg := range args { - for idx := range arg { - tempMap[arg[idx]] = 0 - } - } - - // the map keys are now unique instances of all of the array contents - tempArray := make([]uint64, 0) - for key := range tempMap { - tempArray = append(tempArray, key) - } - - return tempArray -} - -// UnionUint64Arr finds the union of two arrays using a multidimensional array as inputs -// -// Deprecated: use Union instead. -func UnionUint64Arr(arr [][]uint64) []uint64 { - // create a temporary map to hold the contents of the arrays - tempMap := make(map[uint64]uint8) - - // write the contents of the arrays as keys to the map. The map values don't matter - for idx1 := range arr { - for idx2 := range arr[idx1] { - tempMap[arr[idx1][idx2]] = 0 - } - } - - // the map keys are now unique instances of all of the array contents - tempArray := make([]uint64, 0) - for key := range tempMap { - tempArray = append(tempArray, key) - } - - return tempArray -} - -// DifferenceUint64 finds the difference of two arrays. -// -// Deprecated: use Difference instead. -func DifferenceUint64(args ...[]uint64) []uint64 { - // create a temporary map to hold the contents of the arrays - tempMap := make(map[uint64]int) - for _, arg := range args { - tempArr := DistinctUint64(arg) - for idx := range tempArr { - // how many times have we encountered this elem? - if _, ok := tempMap[tempArr[idx]]; ok { - tempMap[tempArr[idx]]++ - } else { - tempMap[tempArr[idx]] = 1 - } - } - } - - // write the final val of the diffMap to an array and return - tempArray := make([]uint64, 0) - for key, val := range tempMap { - if val == 1 { - tempArray = append(tempArray, key) - } - } - - return tempArray -} - -// DifferenceUint64Arr finds the difference of two arrays using a multidimensional array as inputs. -// -// Deprecated: use Difference instead. -func DifferenceUint64Arr(arr [][]uint64) []uint64 { - // create a temporary map to hold the contents of the arrays - tempMap := make(map[uint64]int) - for idx1 := range arr { - tempArr := DistinctUint64(arr[idx1]) - for idx2 := range tempArr { - // how many times have we encountered this elem? - if _, ok := tempMap[tempArr[idx2]]; ok { - tempMap[tempArr[idx2]]++ - } else { - tempMap[tempArr[idx2]] = 1 - } - } - } - - // write the final val of the diffMap to an array and return - tempArray := make([]uint64, 0) - for key, val := range tempMap { - if val == 1 { - tempArray = append(tempArray, key) - } - } - - return tempArray -} - -// DistinctUint64 removes duplicate values from one array. -// -// Deprecated: use Distinct instead. -func DistinctUint64(arg []uint64) []uint64 { - tempMap := make(map[uint64]uint8) - - for idx := range arg { - tempMap[arg[idx]] = 0 - } - - tempArray := make([]uint64, 0) - for key := range tempMap { - tempArray = append(tempArray, key) - } - return tempArray -} diff --git a/util/arrayOperations_test.go b/util/arrayOperations_test.go deleted file mode 100644 index fea8f4a..0000000 --- a/util/arrayOperations_test.go +++ /dev/null @@ -1,605 +0,0 @@ -package util - -import ( - "fmt" - "reflect" - "testing" -) - -var stringArr1 = []string{"a", "a", "b", "d"} -var stringArr2 = []string{"b", "c", "e"} -var intArr1 = []uint64{1, 1, 2, 4} -var intArr2 = []uint64{2, 3, 5} -var tempInterface interface{} - -func TestDistinct(t *testing.T) { - var myTests = []struct { - input interface{} - pass bool - expected interface{} - }{ - {stringArr1, true, []string{"a", "b", "d"}}, - {stringArr2, true, []string{"b", "c", "e"}}, - {intArr1, true, []uint64{1, 2, 4}}, - {intArr2, true, []uint64{2, 3, 5}}, - {[]int{}, true, []int{}}, - } - - for _, tt := range myTests { - actual, ok := Distinct(tt.input) - - if tt.pass && ok && !testEq(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } else if !tt.pass && ok { - t.Errorf("expected fail but received: %v, ok: %v", actual, ok) - } - } -} - -func TestIntersect(t *testing.T) { - var myTests = []struct { - input1 interface{} - input2 interface{} - pass bool - expected interface{} - }{ - {stringArr1, stringArr2, true, []string{"b"}}, - {intArr1, intArr2, true, []uint64{2}}, - {stringArr1, intArr1, false, tempInterface}, - {[]string{}, []string{"1"}, true, []string{}}, - } - - for _, tt := range myTests { - actual, ok := Intersect(tt.input1, tt.input2) - - if tt.pass && ok && !testEq(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } else if !tt.pass && ok { - t.Errorf("expected fail but received: %v, ok: %v", actual, ok) - } - } -} - -func TestUnion(t *testing.T) { - var myTests = []struct { - input1 interface{} - input2 interface{} - pass bool - expected interface{} - }{ - {stringArr1, stringArr2, true, []string{"a", "b", "c", "d", "e"}}, - {intArr1, intArr2, true, []uint64{1, 2, 3, 4, 5}}, - {stringArr1, intArr1, false, tempInterface}, - {[]string{}, []string{"1"}, true, []string{"1"}}, - } - - for _, tt := range myTests { - actual, ok := Union(tt.input1, tt.input2) - - if tt.pass && ok && !testEq(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } else if !tt.pass && ok { - t.Errorf("expected fail but received: %v, ok: %v", actual, ok) - } - } -} - -func TestDifference(t *testing.T) { - var myTests = []struct { - input1 interface{} - input2 interface{} - pass bool - expected interface{} - }{ - {stringArr1, stringArr2, true, []string{"a", "c", "d", "e"}}, - {intArr1, intArr2, true, []uint64{1, 3, 4, 5}}, - {stringArr1, intArr1, false, tempInterface}, - {[]string{}, []string{"1"}, true, []string{"1"}}, - } - - for _, tt := range myTests { - actual, ok := Difference(tt.input1, tt.input2) - - if tt.pass && ok && !testEq(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } else if !tt.pass && ok { - t.Errorf("expected fail but received: %v, ok: %v", actual, ok) - } - } -} - -func TestIntersectString(t *testing.T) { - var myTests = []struct { - input1 []string - input2 []string - expected []string - }{ - {stringArr1, stringArr2, []string{"b"}}, - } - - for _, tt := range myTests { - actual := IntersectString(tt.input1, tt.input2) - - if !testString(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } - } -} - -func TestIntersectStringArr(t *testing.T) { - var myTests = []struct { - input [][]string - expected []string - }{ - {[][]string{stringArr1, stringArr2}, []string{"b"}}, - } - - for _, tt := range myTests { - actual := IntersectStringArr(tt.input) - - if !testString(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } - } -} - -func TestUnionString(t *testing.T) { - var myTests = []struct { - input1 []string - input2 []string - expected []string - }{ - {stringArr1, stringArr2, []string{"a", "b", "c", "d", "e"}}, - } - - for _, tt := range myTests { - actual := UnionString(tt.input1, tt.input2) - - if !testString(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } - } -} - -func TestUnionStringArr(t *testing.T) { - var myTests = []struct { - input [][]string - expected []string - }{ - {[][]string{stringArr1, stringArr2}, []string{"a", "b", "c", "d", "e"}}, - } - - for _, tt := range myTests { - actual := UnionStringArr(tt.input) - - if !testString(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } - } -} - -func TestDifferenceString(t *testing.T) { - var myTests = []struct { - input1 []string - input2 []string - expected []string - }{ - {stringArr1, stringArr2, []string{"a", "c", "d", "e"}}, - } - - for _, tt := range myTests { - actual := DifferenceString(tt.input1, tt.input2) - - if !testString(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } - } -} - -func TestDifferenceStringArr(t *testing.T) { - var myTests = []struct { - input [][]string - expected []string - }{ - {[][]string{stringArr1, stringArr2}, []string{"a", "c", "d", "e"}}, - } - - for _, tt := range myTests { - actual := DifferenceStringArr(tt.input) - - if !testString(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } - } -} - -func TestDistinctString(t *testing.T) { - var myTests = []struct { - input []string - expected []string - }{ - {stringArr1, []string{"a", "b", "d"}}, - {stringArr2, []string{"b", "c", "e"}}, - } - - for _, tt := range myTests { - actual := DistinctString(tt.input) - - if !testString(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } - } -} - -///////////// -///////////// - -func TestIntersectUint64(t *testing.T) { - var myTests = []struct { - input1 []uint64 - input2 []uint64 - expected []uint64 - }{ - {intArr1, intArr2, []uint64{2}}, - } - - for _, tt := range myTests { - actual := IntersectUint64(tt.input1, tt.input2) - - if !testuInt64(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } - } -} - -func TestDistinctIntersectUint64(t *testing.T) { - var myTests = []struct { - input1 []uint64 - input2 []uint64 - expected []uint64 - }{ - {[]uint64{1, 2, 4}, []uint64{2, 3, 5}, []uint64{2}}, - } - - for _, tt := range myTests { - actual := DistinctIntersectUint64(tt.input1, tt.input2) - - if !testuInt64(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } - } -} - -func TestDistinctIntersectUint64Arr(t *testing.T) { - var myTests = []struct { - input [][]uint64 - expected []uint64 - }{ - {[][]uint64{{1, 2, 4}, {2, 3, 5}}, []uint64{2}}, - } - - for _, tt := range myTests { - actual := DistinctIntersectUint64Arr(tt.input) - - if !testuInt64(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } - } -} - -func TestIntersectUint64Arr(t *testing.T) { - var myTests = []struct { - input [][]uint64 - expected []uint64 - }{ - {[][]uint64{intArr1, intArr2}, []uint64{2}}, - } - - for _, tt := range myTests { - actual := IntersectUint64Arr(tt.input) - - if !testuInt64(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } - } -} - -func TestSortedIntersectUint64(t *testing.T) { - var myTests = []struct { - input1 []uint64 - input2 []uint64 - expected []uint64 - }{ - {[]uint64{1, 2, 4}, []uint64{2, 3, 5}, []uint64{2}}, - } - - for _, tt := range myTests { - actual := SortedIntersectUint64(tt.input1, tt.input2) - - if !testuInt64(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } - } -} - -func TestSortedIntersectUint64Arr(t *testing.T) { - var myTests = []struct { - input [][]uint64 - expected []uint64 - }{ - {[][]uint64{{1, 2, 4}, {2, 3, 5}}, []uint64{2}}, - } - - for _, tt := range myTests { - actual := SortedIntersectUint64Arr(tt.input) - - if !testuInt64(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } - } -} - -func TestUnionUint64(t *testing.T) { - var myTests = []struct { - input1 []uint64 - input2 []uint64 - expected []uint64 - }{ - {intArr1, intArr2, []uint64{1, 2, 3, 4, 5}}, - } - - for _, tt := range myTests { - actual := UnionUint64(tt.input1, tt.input2) - - if !testuInt64(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } - } -} - -func TestUnionUint64Arr(t *testing.T) { - var myTests = []struct { - input [][]uint64 - expected []uint64 - }{ - {[][]uint64{intArr1, intArr2}, []uint64{1, 2, 3, 4, 5}}, - } - - for _, tt := range myTests { - actual := UnionUint64Arr(tt.input) - - if !testuInt64(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } - } -} - -func TestDifferenceUint64(t *testing.T) { - var myTests = []struct { - input1 []uint64 - input2 []uint64 - expected []uint64 - }{ - {intArr1, intArr2, []uint64{1, 3, 4, 5}}, - } - - for _, tt := range myTests { - actual := DifferenceUint64(tt.input1, tt.input2) - - if !testuInt64(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } - } -} - -func TestDifferenceUint64Arr(t *testing.T) { - var myTests = []struct { - input [][]uint64 - expected []uint64 - }{ - {[][]uint64{intArr1, intArr2}, []uint64{1, 3, 4, 5}}, - } - - for _, tt := range myTests { - actual := DifferenceUint64Arr(tt.input) - - if !testuInt64(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } - } -} - -func TestDistinctUint64(t *testing.T) { - var myTests = []struct { - input []uint64 - expected []uint64 - }{ - {intArr1, []uint64{1, 2, 4}}, - {intArr2, []uint64{2, 3, 5}}, - } - - for _, tt := range myTests { - actual := DistinctUint64(tt.input) - - if !testuInt64(tt.expected, actual) { - t.Errorf("expected: %v, received: %v", tt.expected, actual) - } - } -} - -// Examples -func ExampleDistinct() { - var a = []int{1, 1, 2, 3} - - z, ok := Distinct(a) - if !ok { - fmt.Println("Cannot find distinct") - } - - slice, ok := z.Interface().([]int) - if !ok { - fmt.Println("Cannot convert to slice") - } - fmt.Println(slice, reflect.TypeOf(slice)) // [1, 2, 3] []int -} - -func ExampleIntersect() { - var a = []int{1, 1, 2, 3} - var b = []int{2, 4} - - z, ok := Intersect(a, b) - if !ok { - fmt.Println("Cannot find intersect") - } - - slice, ok := z.Interface().([]int) - if !ok { - fmt.Println("Cannot convert to slice") - } - fmt.Println(slice, reflect.TypeOf(slice)) // [2] []int -} - -func ExampleUnion() { - var a = []int{1, 1, 2, 3} - var b = []int{2, 4} - - z, ok := Union(a, b) - if !ok { - fmt.Println("Cannot find union") - } - - slice, ok := z.Interface().([]int) - if !ok { - fmt.Println("Cannot convert to slice") - } - fmt.Println(slice, reflect.TypeOf(slice)) // [1, 2, 3, 4] []int -} - -func ExampleDifference() { - var a = []int{1, 1, 2, 3} - var b = []int{2, 4} - - z, ok := Difference(a, b) - if !ok { - fmt.Println("Cannot find difference") - } - - slice, ok := z.Interface().([]int) - if !ok { - fmt.Println("Cannot convert to slice") - } - fmt.Println(slice, reflect.TypeOf(slice)) // [1, 3] []int -} - -// Thanks! http://stackoverflow.com/a/15312097/3512709 -func testEq(a, b interface{}) bool { - - if a == nil && b == nil { - fmt.Println("Both nil") - return true - } - - if a == nil || b == nil { - fmt.Println("One nil") - return false - } - - aSlice, ok := takeArg(a, reflect.Slice) - if !ok { - fmt.Println("Can't takeArg a") - return ok - } - bSlice, ok := b.(reflect.Value) - if !ok { - fmt.Println("Can't takeArg b") - return ok - } - aLen := aSlice.Len() - bLen := bSlice.Len() - - if aLen != bLen { - fmt.Println("Arr lengths not equal") - return false - } - -OUTER: - for i := 0; i < aLen; i++ { - foundVal := false - for j := 0; j < bLen; j++ { - if aSlice.Index(i).Interface() == bSlice.Index(j).Interface() { - foundVal = true - continue OUTER - } - } - - if !foundVal { - return false - } - } - - return true -} - -func testString(a, b []string) bool { - - if a == nil && b == nil { - return true - } - - if a == nil || b == nil { - return false - } - - if len(a) != len(b) { - return false - } - -OUTER: - for _, aEl := range a { - foundVal := false - for _, bEl := range b { - if aEl == bEl { - foundVal = true - continue OUTER - } - } - - if !foundVal { - return false - } - } - - return true -} - -func testuInt64(a, b []uint64) bool { - - if a == nil && b == nil { - return true - } - - if a == nil || b == nil { - return false - } - - if len(a) != len(b) { - return false - } - -OUTER: - for _, aEl := range a { - foundVal := false - for _, bEl := range b { - if aEl == bEl { - foundVal = true - continue OUTER - } - } - - if !foundVal { - return false - } - } - - return true -} diff --git a/util_test.go b/util_test.go index c51761f..7b77a93 100644 --- a/util_test.go +++ b/util_test.go @@ -1,3 +1,22 @@ +// Copyright (c) 2019 MindStand Technologies, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package gogm import ( @@ -41,7 +60,7 @@ func TestGetTypeName(t *testing.T) { func TestToCypherParamsMap(t *testing.T) { val := a{ - embedTest: embedTest{ + BaseNode: BaseNode{ Id: 0, UUID: "testuuid", },