├── .github └── workflows │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .scalafmt.conf ├── LICENSE.txt ├── README.md ├── build.sbt ├── doc └── images │ ├── gremlin-scala-logo-hd.png │ ├── gremlin-scala-logo-white.png │ ├── gremlin-scala-logo.png │ ├── scaladays-talk-shot.png │ └── yourkit.png ├── gremlin-scala.bat ├── gremlin-scala.sh ├── gremlin-scala └── src │ ├── assembly │ ├── distribution.xml │ └── standalone.xml │ ├── main │ ├── bin │ │ ├── gremlin-scala.bat │ │ └── gremlin-scala.sh │ └── scala │ │ └── gremlin │ │ └── scala │ │ ├── BranchOption.scala │ │ ├── By.scala │ │ ├── DefaultsToAny.scala │ │ ├── DetachedElements.scala │ │ ├── GraphHelper.scala │ │ ├── GremlinScala.scala │ │ ├── P.scala │ │ ├── ProjectionBuilder.scala │ │ ├── ScalaEdge.scala │ │ ├── ScalaElement.scala │ │ ├── ScalaGraph.scala │ │ ├── ScalaVertex.scala │ │ ├── Schema.scala │ │ ├── SelectAllStep.scala │ │ ├── SemiEdge.scala │ │ ├── StepLabel.scala │ │ ├── TraversalSource.scala │ │ ├── UnionTraversals.scala │ │ ├── dsl │ │ ├── AnonymousVertex.scala │ │ ├── Constructor.scala │ │ ├── Converter.scala │ │ ├── NodeSteps.scala │ │ └── Steps.scala │ │ └── package.scala │ └── test │ ├── resources │ └── dummy │ └── scala │ └── gremlin │ └── scala │ ├── .gitignore │ ├── AlgorithmSpec.scala │ ├── ArrowSyntaxSpec.scala │ ├── ElementSpec.scala │ ├── FilterSpec.scala │ ├── GraphHelperSpec.scala │ ├── GraphSerialisationSpec.scala │ ├── LogicalSpec.scala │ ├── MonadSpec.scala │ ├── PSpec.scala │ ├── ProjectSpec.scala │ ├── SchemaSpec.scala │ ├── SelectSpec.scala │ ├── TestBase.scala │ ├── TraversalSpec.scala │ ├── TraversalStrategySpec.scala │ ├── dsl │ └── DslSpec.scala │ └── marshallable │ └── MarshallableSpec.scala ├── macros └── src │ └── main │ └── scala │ └── gremlin │ └── scala │ ├── Annotations.scala │ └── Marshallable.scala └── project ├── build.properties └── plugins.sbt /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: pr 2 | on: pull_request 3 | jobs: 4 | pr: 5 | runs-on: ubuntu-18.04 6 | steps: 7 | - uses: actions/checkout@v2 8 | with: 9 | fetch-depth: 1 10 | - uses: olafurpg/setup-scala@v10 11 | - uses: actions/cache@v2 12 | with: 13 | path: | 14 | ~/.sbt 15 | ~/.coursier 16 | key: ${{ runner.os }}-sbt-${{ hashfiles('**/build.sbt') }} 17 | - run: sbt +test 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | branches: [master, main] 5 | tags: ["*"] 6 | jobs: 7 | release: 8 | concurrency: release 9 | runs-on: ubuntu-18.04 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | - uses: olafurpg/setup-scala@v10 15 | - run: echo $PGP_SECRET | base64 --decode | gpg --batch --import 16 | env: 17 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 18 | - uses: actions/cache@v2 19 | with: 20 | path: | 21 | ~/.sbt 22 | ~/.coursier 23 | key: ${{ runner.os }}-sbt-${{ hashfiles('**/build.sbt') }} 24 | - run: sbt +test ciReleaseTagNextVersion ciReleaseSonatype 25 | env: 26 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 27 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | private-key.pem 3 | .idea/ 4 | /.bsp 5 | /gnupg-* 6 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | maxColumn = 100 2 | rewrite.rules = [AvoidInfix, SortImports] -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2013-2015 Michael Pollmeier 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](https://github.com/mpollmeier/gremlin-scala/raw/master/doc/images/gremlin-scala-logo.png) 2 | 3 | [![Build Status](https://github.com/mpollmeier/gremlin-scala/workflows/release/badge.svg)](https://github.com/mpollmeier/gremlin-scala/actions?query=workflow%3Arelease) 4 | [![Join the chat at https://gitter.im/mpollmeier/gremlin-scala](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/mpollmeier/gremlin-scala?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.michaelpollmeier/gremlin-scala_2.13/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.michaelpollmeier/gremlin-scala_2.13) 6 | [![scaladoc](http://www.javadoc.io/badge/com.michaelpollmeier/gremlin-scala_2.13.svg?color=blue&label=scaladoc)](http://www.javadoc.io/doc/com.michaelpollmeier/gremlin-scala_2.13) 7 | 8 | ## Gremlin-Scala for Apache Tinkerpop 3 9 | 10 | A wrapper to use [Apache Tinkerpop3](https://github.com/apache/incubator-tinkerpop) - a JVM graph traversal library - from Scala. 11 | 12 | * Beautiful DSL to create vertices and edges 13 | * Type safe traversals 14 | * Scala friendly function signatures 15 | * Minimal runtime overhead - only allocates additional instances if absolutely necessary 16 | * Nothing is hidden away, you can always easily access the underlying Gremlin-Java objects if needed, e.g. to access graph db specifics things like indexes 17 | 18 | ### TOC 19 | 20 | - [Getting started](#getting-started) 21 | - [Using the sbt console](#using-the-sbt-console) 22 | - [Simple traversals](#simple-traversals) 23 | - [Vertices and edges with type safe properties](#vertices-and-edges-with-type-safe-properties) 24 | - [Compiler helps to eliminate invalid traversals](#compiler-helps-to-eliminate-invalid-traversals) 25 | - [Type safe traversals](#type-safe-traversals) 26 | - [A note on predicates](#a-note-on-predicates) 27 | - [Build a custom DSL on top of Gremlin-Scala](#build-a-custom-dsl-on-top-of-gremlin-scala) 28 | - [Common and useful steps](#common-and-useful-steps) 29 | - [Mapping vertices from/to case classes](#mapping-vertices-fromto-case-classes) 30 | - [More advanced traversals](#more-advanced-traversals) 31 | - [Serialise to and from files](#serialise-to-and-from-files) 32 | - [Help - it's open source!](#help---its-open-source) 33 | - [Why such a long version number?](#why-such-a-long-version-number) 34 | - [Talks](#talks) 35 | - [Further reading](#further-reading) 36 | - [Random things worth knowing](#random-things-worth-knowing) 37 | - [Releases](#releases) 38 | - [Breaking changes](#breaking-changes) 39 | 40 | ### Getting started 41 | The [examples project](https://github.com/mpollmeier/gremlin-scala-examples) comes with working examples for different graph databases. Typically you just need to add a dependency on `"com.michaelpollmeier" %% "gremlin-scala" % "SOME_VERSION"` and one for the graph db of your choice to your `build.sbt` (this readme assumes tinkergraph). The latest version is displayed at the top of this readme in the maven badge. 42 | 43 | ### Using the sbt console 44 | * `sbt gremlin-scala/Test/console` 45 | ```scala 46 | import gremlin.scala._ 47 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerFactory 48 | implicit val graph = TinkerFactory.createModern.asScala 49 | 50 | val name = Key[String]("name") 51 | 52 | val g = graph.traversal 53 | g.V.hasLabel("person").value(name).toList 54 | // List(marko, vadas, josh, peter) 55 | ``` 56 | 57 | ### Simple traversals 58 | 59 | The below create traversals, which are lazy computations. To run a traversal, you can use e.g. `toSet`, `toList`, `head`, `headOption` etc. 60 | 61 | ```scala 62 | import gremlin.scala._ 63 | import org.apache.tinkerpop.gremlin.process.traversal.{Order, P} 64 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerFactory 65 | 66 | implicit val graph = TinkerFactory.createModern.asScala 67 | val g = graph.traversal 68 | 69 | g.V //all vertices 70 | g.E //all edges 71 | 72 | g.V(1).outE("knows") //follow outgoing edges 73 | g.V(1).out("knows") //follow outgoing edges to incoming vertex 74 | 75 | val weight = Key[Double]("weight") 76 | for { 77 | person <- g.V.hasLabel("person") 78 | favorite <- person.outE("likes").order(By(weight, Order.decr)).limit(1).inV 79 | } yield (person, favorite.label) 80 | 81 | // remove all people over 30 from the g - also removes corresponding edges 82 | val Age = Key[Int]("age") 83 | g.V.hasLabel("person").has(Age, P.gte(30)).drop.iterate 84 | ``` 85 | 86 | Warning: GremlinScala is _not_ a monad, because the underlying Tinkerpop GraphTraversal is not. I.e. while GremlinScala offers `map`, `flatMap` etc. and you can use it in a for comprehension for syntactic sugar, it does not fulfil all monad laws. 87 | 88 | More working examples in [TraversalSpec](https://github.com/mpollmeier/gremlin-scala/blob/master/gremlin-scala/src/test/scala/gremlin/scala/TraversalSpec.scala). 89 | 90 | ### Vertices and edges with type safe properties 91 | 92 | ```scala 93 | import gremlin.scala._ 94 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph 95 | import scala.language.postfixOps 96 | implicit val graph = TinkerGraph.open.asScala 97 | 98 | // Keys for properties which can later be used for type safe traversals 99 | val Founded = Key[String]("founded") 100 | val Distance = Key[Int]("distance") 101 | 102 | // create labelled vertex 103 | val paris = graph + "Paris" 104 | 105 | // create vertex with typed properties 106 | val london = graph + ("London", Founded -> "43 AD") 107 | 108 | // create labelled edges 109 | paris --- "OneWayRoad" --> london 110 | paris <-- "OtherWayAround" --- london 111 | paris <-- "Eurostar" --> london 112 | 113 | // create edge with typed properties 114 | paris --- ("Eurostar", Distance -> 495) --> london 115 | 116 | // type safe access to properties 117 | paris.out("Eurostar").value(Founded).head //43 AD 118 | paris.outE("Eurostar").value(Distance).head //495 119 | london.valueOption(Founded) //Some(43 AD) 120 | london.valueOption(Distance) //None 121 | paris.setProperty(Founded, "300 BC") 122 | 123 | val Name = Key[String]("name") 124 | val Age = Key[Int]("age") 125 | 126 | val v1 = graph + ("person", Name -> "marko", Age -> 29) asScala 127 | 128 | v1.keys // Set(Key("name"), Key("age")) 129 | v1.property(Name) // "marko" 130 | v1.valueMap // Map("name" -> "marko", "age" -> 29) 131 | v1.valueMap("name", "age") // Map("name" -> "marko", "age" -> 29) 132 | ``` 133 | 134 | More working examples in [SchemaSpec](https://github.com/mpollmeier/gremlin-scala/blob/master/gremlin-scala/src/test/scala/gremlin/scala/SchemaSpec.scala), [ArrowSyntaxSpec](https://github.com/mpollmeier/gremlin-scala/blob/master/gremlin-scala/src/test/scala/gremlin/scala/ArrowSyntaxSpec.scala) and [ElementSpec](https://github.com/mpollmeier/gremlin-scala/blob/master/gremlin-scala/src/test/scala/gremlin/scala/ElementSpec.scala). 135 | 136 | ### Compiler helps to eliminate invalid traversals 137 | Gremlin-Scala aims to helps you at compile time as much as possible. Take this simple example: 138 | 139 | ```scala 140 | import gremlin.scala._ 141 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph 142 | implicit val graph = TinkerGraph.open.asScala 143 | val g = graph.traversal 144 | g.V.outE.inV //compiles 145 | g.V.outE.outE //does _not_ compile 146 | ``` 147 | 148 | In Gremlin-Groovy there's nothing stopping you to create the second traversal - it will explode at runtime, as outgoing edges do not have outgoing edges. In Gremlin-Scala this simply doesn't compile. 149 | 150 | ### Type safe traversals 151 | You can label any step using `as(StepLabel)` and the compiler will infer the correct types for you in the select step using an HList (a type safe list, i.e. the compiler knows the types of the elements of the list). In Gremlin-Java and Gremlin-Groovy you get a `Map[String, Any]`, so you have to cast to the type you *think* it will be, which is ugly and error prone. For example: 152 | 153 | ```scala 154 | import gremlin.scala._ 155 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerFactory 156 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph 157 | def graph = TinkerFactory.createModern.asScala 158 | val g = graph.traversal 159 | 160 | // select all labelled steps 161 | g.V(1).as("a").outE.as("b").select.toList 162 | // returns a `(Vertex, Edge)` for each path 163 | 164 | // select subset of labelled steps 165 | val a = StepLabel[Vertex]() 166 | val b = StepLabel[Edge]() 167 | val c = StepLabel[Double]() 168 | 169 | val traversal = g.V(1).as(a).outE("created").as(b).value("weight").as(c) 170 | 171 | traversal.select((b, c)).head 172 | // returns a `(Edge, Double)` 173 | ``` 174 | 175 | More working examples in [SelectSpec](https://github.com/mpollmeier/gremlin-scala/blob/master/gremlin-scala/src/test/scala/gremlin/scala/SelectSpec.scala). Kudos to [shapeless](https://github.com/milessabin/shapeless/) and Scala's sophisticated type system that made this possible. 176 | 177 | As of 3.3.3.2 there is a typesafe `union` step that supports heterogeneous queries: 178 | ```scala 179 | val traversal = 180 | g.V(1).union( 181 | _.join(_.outE) 182 | .join(_.out) 183 | ) 184 | // derived type: GremlinScala[(List[Edge], List[Vertex])] 185 | val (outEdges, outVertices) = traversal.head 186 | ``` 187 | 188 | ### A note on predicates 189 | tl;dr: use gremlin.scala.P to create predicates of type P. 190 | 191 | Many steps in take a tinkerpop3 predicate of type `org.apache.tinkerpop.gremlin.process.traversal.P`. Creating Ps that take collection types is dangerous though, because you need to ensure you're creating the correct P. For example `P.within(Set("a", "b"))` would be calling the wrong overload (which checks if the value IS the given set). In that instance you actually wanted to create `P.within(Set("a", "b").asJava: java.util.Collection[String])`. To avoid that confusion, it's best to just `import gremlin.scala._` and create it as `P.within(Set("a", "b"))`. 192 | 193 | ### Build a custom DSL on top of Gremlin-Scala 194 | You can now build your own domain specific language, which is super helpful if you don't want to expose your users to the world of graphs and tinkerpop, but merely build an API for them. All you need to do is setup your ADT as case classes, define your DSL as Steps and create one implicit constructor (the only boilerplate code). The magic in gremlin.scala.dsl._ allows you to even write for comprehensions like this (DSL for tinkerpop testgraph): 195 | 196 | ```scala 197 | case class Person (name: String, age: Integer) extends DomainRoot 198 | case class Software(name: String, lang: String) extends DomainRoot 199 | 200 | val traversal = for { 201 | person <- PersonSteps(graph) 202 | software <- person.created 203 | } yield (person.name, software) 204 | 205 | // note: `traversal` is inferred by the compiler as `gremlin.scala.dsl.Steps[(String, Software)]` 206 | 207 | traversal.toSet // returns: 208 | Set( 209 | ("marko", Software("lop", "java")), 210 | ("josh", Software("lop", "java")), 211 | ("peter", Software("lop", "java")), 212 | ("josh", Software("ripple", "java")) 213 | ) 214 | 215 | // DSL also supports typesafe as/select: 216 | PersonSteps(graph) 217 | .as("person") 218 | .created 219 | .as("software") 220 | .select 221 | .toList 222 | // inferred return type is `List[(Person, Software)]` 223 | ``` 224 | 225 | See the full setup and more tests in [DslSpec](https://github.com/mpollmeier/gremlin-scala/blob/master/gremlin-scala/src/test/scala/gremlin/scala/dsl/DslSpec.scala). 226 | 227 | ### Common and useful steps 228 | 229 | ```scala 230 | // get a vertex by id 231 | g.V(1).headOption 232 | 233 | // get all vertices 234 | g.V.toList 235 | 236 | // group all vertices by their label 237 | g.V.group(By.label) 238 | 239 | // group vertices by a property 240 | val age = Key[Int]("age") 241 | g.V.has(age).group(By(age)) 242 | 243 | // order by property decreasing 244 | val age = Key[Int]("age") 245 | g.V.has(age).order(By(age, Order.decr)) 246 | ``` 247 | 248 | More working examples in [TraversalSpec](https://github.com/mpollmeier/gremlin-scala/blob/master/gremlin-scala/src/test/scala/gremlin/scala/TraversalSpec.scala). 249 | 250 | ### Mapping vertices from/to case classes 251 | You can save and load case classes as a vertex - implemented with a [blackbox macro](http://docs.scala-lang.org/overviews/macros/blackbox-whitebox.html). 252 | 253 | * You can optionally specify the label of your class using `@label` 254 | * `Option` members will be automatically unwrapped, i.e. a `Some[A]` will be stored as the value of type `A` in the database, or `null` if it's `None`. If we wouldn't unwrap it, the database would have to understand Scala's Option type itself. 255 | * The same goes for value classes, i.e. a `case class ShoeSize(value: Int) extends AnyVal` will be stored as an Integer. 256 | * `List` members will be stored as multi-properties, i.e. Cardinality.list 257 | * Annotating members with `@id` and `@underlying` will instruct the marshaller to set the element id and/or the underlying element in the class. Note: you cannot specify the id when adding a vertex like this. Using `@id` only works when retrieving the vertex back from the graph and it therefor must be an `Option`. 258 | * Your classes must be defined outside the scope where they are being used (e.g. in the code below the class `Example` cannot be inside `object Main`). 259 | 260 | _Warning_: this may not work with your particular remote graph, depending on the implementation/configuration. That's because the graph [may choose to only return *referenced elements* which doesn't contain it's properties](http://tinkerpop.apache.org/docs/3.4.1/reference/#_properties_of_elements). 261 | 262 | 263 | ```scala 264 | // this does _not_ work in a REPL 265 | import gremlin.scala._ 266 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph 267 | 268 | @label("my_custom_label") 269 | case class Example(longValue: Long, stringValue: Option[String], @underlying vertex: Option[Vertex] = None) 270 | 271 | object Main extends App { 272 | implicit val graph = TinkerGraph.open.asScala 273 | val example = Example(Long.MaxValue, Some("optional value")) 274 | val v = graph + example 275 | v.toCC[Example] // equal to `example`, but with `vertex` set 276 | 277 | // find all vertices with the label of the case class `Example` 278 | graph.V.hasLabel[Example] 279 | 280 | // modify the vertex like a case class 281 | v.updateAs[Example](_.copy(longValue = 0L)) 282 | } 283 | ``` 284 | 285 | You can also define your own marshaller, if the macro generated one doesn't quite cut it. For that and more examples check out the [MarshallableSpec](https://github.com/mpollmeier/gremlin-scala/blob/master/gremlin-scala/src/test/scala/gremlin/scala/marshallable/MarshallableSpec.scala). 286 | 287 | ### More advanced traversals 288 | Here are some examples of more complex traversals from the [examples repo](https://github.com/mpollmeier/gremlin-scala-examples/). If you want to run them yourself, check out the tinkergraph examples in there. 289 | 290 | _What's the property distribution of all vertices?_ 291 | ```scala 292 | graph.V.groupCount(By(__.properties().count)).head 293 | ``` 294 | 295 | _What is `Die Hard's` average rating?_ 296 | ```scala 297 | graph.V.has("movie", "name", "Die Hard") 298 | .inE("rated") 299 | .values("stars") 300 | .mean 301 | .head 302 | ``` 303 | 304 | _Get the maximum number of movies a single user rated_ 305 | ```scala 306 | g.V.hasLabel("person") 307 | .flatMap(_.outE("rated").count) 308 | .max 309 | .head 310 | ``` 311 | 312 | _What 80's action movies do 30-something programmers like?_ 313 | _Group count the movies by their name and sort the group count map in decreasing order by value._ 314 | ```scala 315 | g.V 316 | .`match`( 317 | __.as("a").hasLabel("movie"), 318 | __.as("a").out("hasGenre").has("name", "Action"), 319 | __.as("a").has("year", P.between(1980, 1990)), 320 | __.as("a").inE("rated").as("b"), 321 | __.as("b").has("stars", 5), 322 | __.as("b").outV().as("c"), 323 | __.as("c").out("hasOccupation").has("name", "programmer"), 324 | __.as("c").has("age", P.between(30, 40)) 325 | ) 326 | .select[Vertex]("a") 327 | .map(_.value[String]("name")) 328 | .groupCount() 329 | .order(Scope.local).by(Order.valueDecr) 330 | .limit(Scope.local, 10) 331 | .head 332 | ``` 333 | 334 | _What is the most liked movie in each decade?_ 335 | ```scala 336 | g.V() 337 | .hasLabel(Movie) 338 | .where(_.inE(Rated).count().is(P.gt(10))) 339 | .groupBy { movie => 340 | val year = movie.value2(Year) 341 | val decade = (year / 10) 342 | (decade * 10): Integer 343 | } 344 | .map { moviesByDecade => 345 | val highestRatedByDecade = moviesByDecade.mapValues { movies => 346 | movies.toList 347 | .sortBy { _.inE(Rated).value(Stars).mean().head } 348 | .reverse.head //get the movie with the highest mean rating 349 | } 350 | highestRatedByDecade.mapValues(_.value2(Name)) 351 | } 352 | .order(Scope.local).by(Order.keyIncr) 353 | .head 354 | ``` 355 | 356 | ### Serialise to and from files 357 | Currently graphML, graphson and gryo/kryo are supported file formats, it is very easy to serialise and deserialise into those: see [GraphSerialisationSpec](https://github.com/mpollmeier/gremlin-scala/blob/master/gremlin-scala/src/test/scala/gremlin/scala/GraphSerialisationSpec.scala). 358 | An easy way to visualise your graph is to export it into graphML and import it into [gephi](https://gephi.org/). 359 | 360 | ### Help - it's open source! 361 | If you would like to help, here's a list of things that needs to be addressed: 362 | * add more graph databases and examples into the [examples project](https://github.com/mpollmeier/gremlin-scala-examples) 363 | * port over more TP3 steps - see [TP3 testsuite](https://github.com/apache/incubator-tinkerpop/tree/master/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step) and [Gremlin-Scala StandardTests](https://github.com/mpollmeier/gremlin-scala/blob/master/gremlin-scala/src/test/scala/gremlin/scala/GremlinStandardTestSuite.scala) 364 | * ideas for more type safety in traversals 365 | * fill this readme and provide other documentation, or how-tos, e.g. a blog post or tutorial 366 | 367 | ### Why such a long version number? 368 | The first three digits is the TP3 version number, only the last digit is automatically incremented on every release of gremlin-scala. 369 | 370 | ### Talks 371 | 372 | ScalaDays Berlin 2018 373 | 374 | 375 | ### Further reading 376 | For more information about Gremlin see the [Gremlin docs](http://tinkerpop.incubator.apache.org/) and the [Gremlin users mailinglist](https://groups.google.com/forum/#!forum/gremlin-users). 377 | Please note that while Gremlin-Scala is very close to the original Gremlin, there are differences to Gremlin-Groovy - don't be afraid, they hopefully all make sense to a Scala developer ;) 378 | 379 | Random links: 380 | * Social Network using Titan Db: [part 1](https://partialflow.wordpress.com/2017/02/26/social-network-using-titan-db-part-1/) and [part 2](https://partialflow.wordpress.com/2017/03/04/social-network-using-titan-db-part-2/) 381 | * [Shortest path algorithm with Gremlin-Scala 3.0.0 (Michael Pollmeier)](http://www.michaelpollmeier.com/2014/12/27/gremlin-scala-shortest-path) 382 | * [Shortest path algorithm with Gremlin-Scala 2.4.1 (Stefan Bleibinhaus)](http://bleibinha.us/blog/2013/10/scala-and-graph-databases-with-gremlin-scala) 383 | 384 | ### Random things worth knowing 385 | * `org.apache.tinkerpop.gremlin.structure.Transaction` is not thread-safe. It's implemented with Apache's ThreadLocal class, see https://github.com/mpollmeier/gremlin-scala/issues/196#issuecomment-301625679 386 | 387 | ### Releases 388 | ... happen automatically for every commit on `master` from [travis.ci](https://travis-ci.org/mpollmeier/gremlin-scala) thanks to [sbt-ci-release-early](https://github.com/ShiftLeftSecurity/sbt-ci-release-early) 389 | 390 | ### YourKit endorsement 391 | YourKit supports open source projects with innovative and intelligent tools for monitoring and profiling Java and .NET applications. 392 | YourKit is the creator of YourKit Java Profiler, 393 | YourKit .NET Profiler, and YourKit YouMonitor.
394 | 395 | 396 | ### Breaking changes 397 | #### 3.4.7.2 398 | Marshallable now treats Sets as multi-properties, i.e. one property for each element in the set. This is similar to how 399 | List properties are handled and allows for a natural representation of vertex properties whose cardinality is `set` 400 | in case classes. This change breaks compatibility with Marshallable's previous behaviour in which Sets were effectively 401 | treated as `single` cardinality properties, i.e. a single property whose value is the entire set. 402 | 403 | #### 3.4.1.13 404 | The implementation for `@label` with non literal values (e.g. `@label(SomeClass.LABEL)`) was dropped due to it's [bad performance](https://github.com/mpollmeier/gremlin-scala/issues/288). Please use String literals instead, e.g. `@label("MyLabel")`. 405 | 406 | #### 3.3.3.2 407 | We now have a fully typed `union` step which supports heterogeneous queries. The old version is still available as `unionFlat`, since it may still be relevant in some situations where the union traversals are homogeneous. 408 | 409 | #### 3.3.2.0 410 | The `by` modulator is now called `By`. E.g. `order(by(Order.decr))` becomes `order(By(Order.decr))`. 411 | Background: case insensitive platforms like OSX (default) and Windows fail to compile `object by` and `trait By` because they lead to two separate .class files. I decided for this option because it conforms to Scala's naming best practices. 412 | See https://github.com/mpollmeier/gremlin-scala/issues/237#issuecomment-375928284. 413 | 414 | #### 3.3.1.2 415 | To fix problems with remote graphs and the arrow syntax (e.g. `vertex1 --- "label" --> vertex2`) there now needs to be an `implicit ScalaGraph` in scope. Background: the configuration for remote is unfortunately not stored in the Tinkerpop Graph instance, but in the TraversalSource. Since a vertex only holds a reference to the graph instance, this configuration must be passed somehow. `ScalaGraph` does contain the configuration, e.g. for remote connections, so we now pass it implicitly. 416 | 417 | #### 3.3.1.1 418 | The type signature of GremlinScala changed: the former type parameter `Labels` is now a type member, which shortens the type if you don't care about Labels. The Labels were only used in a small percentage of steps, but had to be written out by users all the time even if they didn't care. 419 | Rewrite rules (old -> new), using `Vertex` as an example: 420 | `GremlinScala[Vertex, _]` -> `GremlinScala[Vertex]` (existential type: most common, i.e the user doesn't use or care about the Labels) 421 | `GremlinScala[Vertex, HNil]` -> `GremlinScala.Aux[Vertex, HNil]` (equivalent: `GremlinScala[Vertex] {type Labels = HNil}`) 422 | `GremlinScala[Vertex, Vertex :: HNil]` -> `GremlinScala.Aux[Vertex, Vertex :: HNil]` (equivalent: `GremlinScala[Vertex] {type Labels = Vertex :: HNil}`) 423 | Notice: GremlinScala isn't a case class any more - it shouldn't have been in the first place. 424 | 425 | #### 3.2.4.8 426 | The `filter` step changed it's signature and now takes a traversal: `filter(predicate: GremlinScala[End, _] => GremlinScala[_, _])`. The old `filter(predicate: End => Boolean)` is now called `filterOnEnd`, in case you still need it. This change might affect your for comprehensions. 427 | 428 | The reasoning for the change is that it's discouraged to use lambdas (see http://tinkerpop.apache.org/docs/current/reference/#a-note-on-lambdas). Instead we are now creating anonymous traversals, which can be optimised by the driver, sent over the wire as gremlin binary for remote execution etc. 429 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | val gremlinVersion = "3.5.3" 2 | val scalaTestVersion = "3.2.12" 3 | 4 | ThisBuild / organization := "com.michaelpollmeier" 5 | ThisBuild / scalaVersion := "2.13.8" 6 | ThisBuild / crossScalaVersions := Seq("2.11.12", "2.12.15", "2.13.8") 7 | 8 | ThisBuild / libraryDependencies ++= Seq( 9 | "org.apache.tinkerpop" % "gremlin-core" % gremlinVersion, 10 | "com.chuusai" %% "shapeless" % "2.3.9", 11 | "org.slf4j" % "slf4j-nop" % "1.7.36" % Test, 12 | "org.apache.tinkerpop" % "tinkergraph-gremlin" % gremlinVersion % Test, 13 | "org.apache.tinkerpop" % "gremlin-test" % gremlinVersion % Test, 14 | "org.scalatest" %% "scalatest-shouldmatchers" % scalaTestVersion % Test, 15 | "org.scalatest" %% "scalatest-wordspec" % scalaTestVersion % Test, 16 | "org.scalatest" %% "scalatest-funspec" % scalaTestVersion % Test, 17 | "org.scalamock" %% "scalamock" % "5.2.0" % Test 18 | ) 19 | ThisBuild / resolvers += "Apache public".at("https://repository.apache.org/content/groups/public/") 20 | ThisBuild / resolvers += Resolver.mavenLocal 21 | ThisBuild / scalacOptions ++= Seq( 22 | // "-Xlint" 23 | // "-Xfatal-warnings", 24 | // , "-Xlog-implicits" 25 | //"-Ydebug", 26 | "-target:jvm-1.8", 27 | "-language:implicitConversions", 28 | "-language:existentials", 29 | "-feature", 30 | "-deprecation" //hard to handle when supporting multiple scala versions... 31 | ) 32 | 33 | ThisBuild / Test / console / initialCommands := 34 | """|import gremlin.scala._ 35 | |import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerFactory 36 | |import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph 37 | |import org.apache.tinkerpop.gremlin.process.traversal.{Order, P, Scope} 38 | |implicit val graph = TinkerFactory.createModern.asScala 39 | |val g = graph.traversal""".stripMargin 40 | 41 | ThisBuild / scmInfo := Some( 42 | ScmInfo(url("https://github.com/mpollmeier/gremlin-scala"), 43 | "scm:git@github.com:mpollmeier/gremlin-scala.git")) 44 | ThisBuild / developers := List( 45 | Developer("mpollmeier", 46 | "Michael Pollmeier", 47 | "michael@michaelpollmeier.com", 48 | url("https://michaelpollmeier.com"))) 49 | ThisBuild / homepage := Some(url("https://github.com/mpollmeier/gremlin-scala")) 50 | ThisBuild / licenses += ("Apache-2.0", url("https://www.apache.org/licenses/LICENSE-2.0.html")) 51 | ThisBuild / publishTo := sonatypePublishToBundle.value 52 | 53 | // virtual root project 54 | name := "root" 55 | publish / skip := true 56 | 57 | lazy val macros = project // macros must be in a separate compilation unit 58 | .in(file("macros")) 59 | .settings(libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value) 60 | lazy val `gremlin-scala` = project.in(file("gremlin-scala")).dependsOn(macros) 61 | -------------------------------------------------------------------------------- /doc/images/gremlin-scala-logo-hd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpollmeier/gremlin-scala/b68ca4e1cc6b050c0e98966af8c08c17ad984156/doc/images/gremlin-scala-logo-hd.png -------------------------------------------------------------------------------- /doc/images/gremlin-scala-logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpollmeier/gremlin-scala/b68ca4e1cc6b050c0e98966af8c08c17ad984156/doc/images/gremlin-scala-logo-white.png -------------------------------------------------------------------------------- /doc/images/gremlin-scala-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpollmeier/gremlin-scala/b68ca4e1cc6b050c0e98966af8c08c17ad984156/doc/images/gremlin-scala-logo.png -------------------------------------------------------------------------------- /doc/images/scaladays-talk-shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpollmeier/gremlin-scala/b68ca4e1cc6b050c0e98966af8c08c17ad984156/doc/images/scaladays-talk-shot.png -------------------------------------------------------------------------------- /doc/images/yourkit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpollmeier/gremlin-scala/b68ca4e1cc6b050c0e98966af8c08c17ad984156/doc/images/yourkit.png -------------------------------------------------------------------------------- /gremlin-scala.bat: -------------------------------------------------------------------------------- 1 | :: Windows launcher script for Gremlin 2 | @echo off 3 | 4 | cd %CD%\target\ 5 | 6 | set TARGET= 7 | 8 | for /f "tokens=*" %%a in ('dir /b /ad') do ( 9 | if exist "%%a\bin\gremlin-scala.bat" set TARGET=%%a 10 | ) 11 | 12 | cd %TARGET%\bin\ 13 | call gremlin-scala.bat %* 14 | -------------------------------------------------------------------------------- /gremlin-scala.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | `dirname $0`/target/gremlin-scala-*-standalone/bin/gremlin-scala.sh $@ -------------------------------------------------------------------------------- /gremlin-scala/src/assembly/distribution.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | zip 4 | 5 | 6 | 7 | 8 | pom.xml 9 | 10 | 11 | 12 | 0775 13 | 14 | gremlin-scala.sh 15 | gremlin-scala.bat 16 | 17 | 18 | 19 | src 20 | 21 | 22 | data 23 | 24 | 25 | target/apidocs 26 | 27 | 28 | target/site 29 | 30 | 31 | target/${project.artifactId}-${project.version}-standalone 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /gremlin-scala/src/assembly/standalone.xml: -------------------------------------------------------------------------------- 1 | 2 | standalone 3 | 4 | dir 5 | 6 | false 7 | 8 | 9 | 10 | src/main/bin 11 | /bin 12 | 0755 13 | 14 | 15 | target/*.jar 16 | /lib 17 | 18 | 19 | 20 | 21 | 22 | /lib 23 | false 24 | compile 25 | 26 | 27 | /lib 28 | false 29 | provided 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/bin/gremlin-scala.bat: -------------------------------------------------------------------------------- 1 | :: Windows launcher script for Gremlin Scala 2 | 3 | @echo off 4 | 5 | 6 | ::cd ..\lib 7 | 8 | ::set LIBDIR=%CD% 9 | 10 | 11 | set LIBDIR=..\lib 12 | 13 | 14 | set OLD_CLASSPATH=%CLASSPATH% 15 | set CP= 16 | 17 | 18 | for %%i in (%LIBDIR%\*.jar) do call :concatsep %%i 19 | 20 | :: cd ..\..\..\ 21 | 22 | 23 | set JAVA_OPTIONS=-Xms32m -Xmx512m 24 | 25 | 26 | :: Launch the application 27 | 28 | if "%1" == "" goto console 29 | 30 | if "%1" == "-v" goto version 31 | 32 | 33 | 34 | :console 35 | 36 | set CLASSPATH=%CP%;%OLD_CLASSPATH% 37 | java %JAVA_OPTIONS% %JAVA_ARGS% com.tinkerpop.gremlin.scala.console.Console 38 | set CLASSPATH=%OLD_CLASSPATH% 39 | 40 | set CLASSPATH=%OLD_CLASSPATH% 41 | goto :eof 42 | 43 | 44 | 45 | :version 46 | 47 | set CLASSPATH=%CP%;%OLD_CLASSPATH% 48 | java %JAVA_OPTIONS% %JAVA_ARGS% com.tinkerpop.gremlin.Version 49 | 50 | set CLASSPATH=%OLD_CLASSPATH% 51 | goto :eof 52 | 53 | 54 | 55 | :concat 56 | 57 | if %1 == %2 goto skip 58 | 59 | SET strg=%strg% %1 60 | 61 | 62 | 63 | :concatsep 64 | 65 | if "%CP%" == "" ( 66 | 67 | set CP=%LIBDIR%\%1 68 | 69 | )else ( 70 | 71 | set CP=%CP%;%LIBDIR%\%1 72 | 73 | ) 74 | 75 | 76 | 77 | :skip 78 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/bin/gremlin-scala.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CP=$( echo `dirname $0`/../lib/*.jar . | sed 's/ /:/g') 4 | #echo $CP 5 | 6 | # Find Java 7 | if [ "$JAVA_HOME" = "" ] ; then 8 | JAVA="java -server" 9 | else 10 | JAVA="$JAVA_HOME/bin/java -server" 11 | fi 12 | 13 | # Set Java options 14 | if [ "$JAVA_OPTIONS" = "" ] ; then 15 | JAVA_OPTIONS="-Xms32m -Xmx512m" 16 | fi 17 | 18 | # Launch the application 19 | if [ "$1" = "-v" ]; then 20 | $JAVA $JAVA_OPTIONS -cp $CP:$CLASSPATH com.tinkerpop.gremlin.Version 21 | else 22 | $JAVA $JAVA_OPTIONS -cp $CP:$CLASSPATH com.tinkerpop.gremlin.scala.console.Console 23 | fi 24 | 25 | #Something in JLine 2.9.1 is leaving stty in "no echo" (-echo) mode; try to repair that on exit... 26 | stty echo 27 | 28 | # Return the program's exit code 29 | exit $? 30 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/BranchOption.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalOptionParent.Pick 4 | 5 | /** Define the traversal to run if the given predicate is true - used in branch step 6 | * 7 | * you might think that traversal should be `GremlinScala[End, _] => GremlinScala[Boolean, _]`, 8 | * but that's not how tp3 works: e.g. `.value(Age).is(30)` returns `30`, not `true` 9 | */ 10 | trait BranchOption[End, NewEnd] { 11 | def traversal: GremlinScala[End] => GremlinScala[NewEnd] 12 | def pickToken: Any 13 | } 14 | 15 | case class BranchCase[BranchOn, End, NewEnd](pickToken: BranchOn, 16 | traversal: GremlinScala[End] => GremlinScala[NewEnd]) 17 | extends BranchOption[End, NewEnd] 18 | 19 | case class BranchMatchAll[End, NewEnd](traversal: GremlinScala[End] => GremlinScala[NewEnd]) 20 | extends BranchOption[End, NewEnd] { 21 | override def pickToken = Pick.any 22 | } 23 | 24 | /* if nothing else matched in branch/choose step */ 25 | case class BranchOtherwise[End, NewEnd](traversal: GremlinScala[End] => GremlinScala[NewEnd]) 26 | extends BranchOption[End, NewEnd] { 27 | override def pickToken = Pick.none 28 | } 29 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/By.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.tinkerpop.gremlin.process.traversal.Order 4 | import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal 5 | import org.apache.tinkerpop.gremlin.structure.T 6 | import java.util.function.{Function => JFunction} 7 | import java.util.{Collection => JCollection} 8 | 9 | /** 10 | * By step can be used in combination with all sorts of other steps 11 | * e.g. group, groupCount, order, dedup, sack, ... 12 | * http://tinkerpop.apache.org/docs/current/reference/#by-step 13 | * n.b. `By` can be used in place of `OrderBy`, hence extending OrderBy */ 14 | trait By[Modulated] extends OrderBy[Modulated] { 15 | 16 | /** When used as the latter By of group method */ 17 | type ValueFold[X] 18 | def apply[End](traversal: GraphTraversal[_, End]): GraphTraversal[_, End] 19 | } 20 | trait OrderBy[Modulated] { 21 | def apply[End](traversal: GraphTraversal[_, End]): GraphTraversal[_, End] 22 | } 23 | 24 | object By { 25 | 26 | /* modulate by property */ 27 | def apply[Modulated](key: Key[Modulated]) = new By[Modulated] { 28 | override type ValueFold[A] = JCollection[A] 29 | override def apply[End](traversal: GraphTraversal[_, End]) = 30 | traversal.by(key.name) 31 | } 32 | 33 | /* modulate by property and order */ 34 | def apply[Modulated](key: Key[Modulated], order: Order) = 35 | new OrderBy[Modulated] { 36 | override def apply[End](traversal: GraphTraversal[_, End]) = 37 | traversal.by(key.name, order) 38 | } 39 | 40 | /* modulate by label - alias for `apply[String](T.label)` */ 41 | def label = new By[Label] { 42 | override type ValueFold[A] = JCollection[A] 43 | override def apply[End](traversal: GraphTraversal[_, End]) = 44 | traversal.by(T.label) 45 | } 46 | 47 | /* modulate by label and order - alias for `apply[String](T.label, Order)` */ 48 | def label(order: Order) = new OrderBy[Label] { 49 | override def apply[End](traversal: GraphTraversal[_, End]) = 50 | traversal.by(T.label, order) 51 | } 52 | 53 | /* modulate by T(oken) */ 54 | def apply[Modulated](token: T) = new By[Modulated] { 55 | override type ValueFold[A] = JCollection[A] 56 | override def apply[End](traversal: GraphTraversal[_, End]) = 57 | traversal.by(token) 58 | } 59 | 60 | /* modulate by T(oken) and order */ 61 | def apply[Modulated](token: T, order: Order) = new OrderBy[Modulated] { 62 | override def apply[End](traversal: GraphTraversal[_, End]) = 63 | traversal.by(token, order) 64 | } 65 | 66 | /* modulate by anonymous traversal, e.g. __.inE.value(Name) */ 67 | def apply[Modulated](by: GremlinScala[Modulated]) = new By[Modulated] { 68 | override type ValueFold[A] = A 69 | override def apply[End](traversal: GraphTraversal[_, End]) = 70 | traversal.by(by.traversal) 71 | } 72 | 73 | /* modulate by anonymous traversal and order, e.g. (__.inE.value(Name), Order.decr) */ 74 | def apply[Modulated](by: GremlinScala[Modulated], order: Order) = 75 | new OrderBy[Modulated] { 76 | override def apply[End](traversal: GraphTraversal[_, End]) = 77 | traversal.by(by.traversal, order) 78 | } 79 | 80 | /* modulate by function 81 | * n.b. prefer one of the other modulators, see http://tinkerpop.apache.org/docs/current/reference/#a-note-on-lambdas */ 82 | def apply[From, Modulated](fun: From => Modulated) = new By[Modulated] { 83 | override type ValueFold[A] = JCollection[A] 84 | override def apply[End](traversal: GraphTraversal[_, End]) = 85 | traversal.by[From](new JFunction[From, AnyRef] { 86 | override def apply(from: From): AnyRef = fun(from).asInstanceOf[AnyRef] 87 | }) 88 | } 89 | 90 | /* modulate by function and order 91 | * n.b. prefer one of the other modulators, see http://tinkerpop.apache.org/docs/current/reference/#a-note-on-lambdas */ 92 | def apply[From, Modulated](fun: From => Modulated, order: Order) = 93 | new OrderBy[Modulated] { 94 | override def apply[End](traversal: GraphTraversal[_, End]) = 95 | traversal.by[From]( 96 | new JFunction[From, AnyRef] { 97 | override def apply(from: From): AnyRef = 98 | fun(from).asInstanceOf[AnyRef] 99 | }, 100 | order 101 | ) 102 | } 103 | 104 | def apply[Modulated](order: Order) = new OrderBy[Modulated] { 105 | override def apply[End](traversal: GraphTraversal[_, End]) = 106 | traversal.by(order) 107 | } 108 | 109 | /* identity modulator */ 110 | def apply[Modulated]() = new By[Modulated] { 111 | override type ValueFold[A] = JCollection[A] 112 | override def apply[End](traversal: GraphTraversal[_, End]) = traversal.by() 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/DefaultsToAny.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | sealed class DefaultsToAny[A] 4 | 5 | object DefaultsToAny { 6 | implicit def overrideDefault[A] = new DefaultsToAny[A] 7 | 8 | implicit def default = new DefaultsToAny[Any] 9 | } 10 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/DetachedElements.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import scala.collection.JavaConverters._ 4 | 5 | // useful e.g. for optional step with a default 6 | 7 | object DetachedVertex { 8 | def apply(id: AnyRef = "detached", 9 | label: String = "detached", 10 | properties: Map[String, AnyRef] = Map.empty) = 11 | new org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex(id, 12 | label, 13 | properties.asJava) 14 | } 15 | 16 | object DetachedEdge { 17 | def apply(id: AnyRef = "detached", 18 | label: String = "detached", 19 | properties: Map[String, AnyRef] = Map.empty, 20 | outVId: AnyRef = "", 21 | outVLabel: String = "", 22 | inVId: AnyRef = "", 23 | inVLabel: String = "") = 24 | new org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdge(id, 25 | label, 26 | properties.asJava, 27 | outVId, 28 | outVLabel, 29 | inVId, 30 | inVLabel) 31 | } 32 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/GraphHelper.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedFactory 4 | import org.apache.tinkerpop.gremlin.structure.util.Attachable 5 | import scala.collection.JavaConverters._ 6 | 7 | object GraphHelper { 8 | 9 | def cloneElements(original: ScalaGraph, clone: ScalaGraph): ScalaGraph = { 10 | cloneElements(original.graph, clone.graph).asScala() 11 | } 12 | 13 | /** 14 | * make a deep clone of the graph elements that preserves ids 15 | */ 16 | def cloneElements(original: Graph, clone: Graph): Graph = { 17 | original 18 | .vertices() 19 | .asScala 20 | .foreach(v => DetachedFactory.detach(v, true).attach(Attachable.Method.create(clone))) 21 | original 22 | .edges() 23 | .asScala 24 | .foreach(e => DetachedFactory.detach(e, true).attach(Attachable.Method.create(clone))) 25 | clone 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/P.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.tinkerpop.gremlin.process.traversal.{P => JavaP} 4 | import java.util.{Collection => JCollection} 5 | import scala.collection.JavaConverters._ 6 | 7 | /** the scala version of tinkerpop's P, mostly to avoid unnecessarily complicated constructs 8 | * like P.within(vertices.asJava: JCollection[Vertex]) */ 9 | object P { 10 | // same as `eq`, in case you're having problems with overloaded definition of `eq` 11 | def is[A](value: A) = JavaP.eq(value) 12 | def eq[A](value: A) = JavaP.eq(value) 13 | def neq[A](value: A) = JavaP.neq(value) 14 | def gt[A](value: A) = JavaP.gt(value) 15 | def gte[A](value: A) = JavaP.gte(value) 16 | def lt[A](value: A) = JavaP.lt(value) 17 | def lte[A](value: A) = JavaP.lte(value) 18 | def between[A](a1: A, a2: A) = JavaP.between(a1, a2) 19 | def inside[A](a1: A, a2: A) = JavaP.inside(a1, a2) 20 | def outside[A](a1: A, a2: A) = JavaP.outside(a1, a2) 21 | def within[A](iterable: Iterable[A]) = 22 | JavaP.within(iterable.asJavaCollection: JCollection[A]) 23 | def without[A](iterable: Iterable[A]) = 24 | JavaP.without(iterable.asJavaCollection: JCollection[A]) 25 | 26 | def fromPredicate[A](predicate: (A, A) => Boolean, value: A) = 27 | new JavaP(toJavaBiPredicate(predicate), value) 28 | } 29 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/ProjectionBuilder.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import java.util.{Map => JMap, UUID} 4 | import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal 5 | import shapeless.ops.tuple.{Prepend => TuplePrepend} 6 | import shapeless.syntax.std.tuple._ 7 | 8 | class ProjectionBuilder[T <: Product] private[gremlin] ( 9 | labels: Seq[String], 10 | addBy: GraphTraversal[_, JMap[String, Any]] => GraphTraversal[_, JMap[String, Any]], 11 | buildResult: JMap[String, Any] => T) { 12 | 13 | def apply[U, TR <: Product](by: By[U])( 14 | implicit prepend: TuplePrepend.Aux[T, Tuple1[U], TR]): ProjectionBuilder[TR] = { 15 | val label = UUID.randomUUID().toString 16 | new ProjectionBuilder[TR](labels :+ label, 17 | addBy.andThen(by.apply), 18 | map => buildResult(map) :+ map.get(label).asInstanceOf[U]) 19 | } 20 | 21 | def and[U, TR <: Product](by: By[U])( 22 | implicit prepend: TuplePrepend.Aux[T, Tuple1[U], TR]): ProjectionBuilder[TR] = apply(by) 23 | 24 | private[gremlin] def build(g: GremlinScala[_]): GremlinScala[T] = { 25 | GremlinScala(addBy(g.traversal.project(labels.head, labels.tail: _*))).map(buildResult) 26 | } 27 | } 28 | 29 | object ProjectionBuilder { 30 | def apply() = new ProjectionBuilder[Nil.type](Nil, scala.Predef.identity, _ => Nil) 31 | } 32 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/ScalaEdge.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import scala.collection.JavaConverters._ 4 | 5 | case class ScalaEdge(edge: Edge) extends ScalaElement[Edge] { 6 | override def element = edge 7 | 8 | override def setProperty[A](key: Key[A], value: A): Edge = { 9 | element.property(key.name, value) 10 | edge 11 | } 12 | 13 | def setProperties(properties: Map[Key[Any], Any]): Edge = { 14 | properties.foreach { case (k, v) => setProperty(k, v) } 15 | edge 16 | } 17 | 18 | def setProperties[CC <: Product: Marshallable](cc: CC): Edge = { 19 | val fromCC = implicitly[Marshallable[CC]].fromCC(cc) 20 | fromCC.properties.foreach { case (k, v) => element.property(k, v) } 21 | edge 22 | } 23 | 24 | override def removeProperty(key: Key[_]): Edge = { 25 | val p = property(key) 26 | if (p.isPresent) p.remove() 27 | edge 28 | } 29 | 30 | override def removeProperties(keys: Key[_]*): Edge = { 31 | keys.foreach(removeProperty) 32 | edge 33 | } 34 | 35 | def toCC[CC <: Product: Marshallable] = 36 | implicitly[Marshallable[CC]].toCC(edge) 37 | 38 | override def properties[A: DefaultsToAny]: Stream[Property[A]] = 39 | edge.properties[A](keys.map(_.name).toSeq: _*).asScala.toStream 40 | 41 | override def properties[A: DefaultsToAny](wantedKeys: String*): Stream[Property[A]] = 42 | edge.properties[A](wantedKeys: _*).asScala.toStream 43 | 44 | //TODO: wait until this is consistent in T3 between Vertex and Edge 45 | //currently Vertex.outE returns a GraphTraversal, Edge.inV doesnt quite exist 46 | //def inV() = GremlinScala[Vertex, HNil](edge.inV()) 47 | } 48 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/ScalaElement.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import scala.collection.JavaConverters._ 4 | import shapeless.HNil 5 | 6 | trait ScalaElement[ElementType <: Element] { 7 | def element: ElementType 8 | 9 | def graph: ScalaGraph = element.graph 10 | 11 | /** start a new traversal from this element */ 12 | def start(): GremlinScala.Aux[ElementType, HNil] = element.graph().inject(element) 13 | 14 | /** start a new traversal from this element and configure it */ 15 | def start(configure: TraversalSource => TraversalSource): GremlinScala.Aux[ElementType, HNil] = 16 | GremlinScala[ElementType, HNil]( 17 | configure(TraversalSource(element.graph)).underlying.inject(element) 18 | ) 19 | 20 | def id[A: DefaultsToAny]: A = element.id.asInstanceOf[A] 21 | 22 | def label: String = element.label 23 | 24 | def keys: Set[Key[Any]] = element.keys.asScala.toSet.map(Key.apply[Any]) 25 | 26 | def setProperty[A](key: Key[A], value: A): ElementType 27 | 28 | def removeProperty(key: Key[_]): ElementType 29 | 30 | def removeProperties(keys: Key[_]*): ElementType 31 | 32 | def property[A](key: Key[A]): Property[A] = element.property[A](key.name) 33 | 34 | def properties[A: DefaultsToAny]: Stream[Property[A]] 35 | 36 | def properties[A: DefaultsToAny](keys: String*): Stream[Property[A]] 37 | 38 | // note: this may throw an IllegalStateException - better use `valueOption` or `Property` 39 | def value[A: DefaultsToAny](key: String): A = 40 | element.value[A](key) 41 | 42 | // typesafe version of `value. have to call it `value2` because of a scala compiler bug :( 43 | // https://issues.scala-lang.org/browse/SI-9523 44 | def value2[A](key: Key[A]): A = 45 | element.value[A](key.name) 46 | 47 | def valueOption[A: DefaultsToAny](key: String): Option[A] = 48 | element.property[A](key).toOption 49 | 50 | def valueOption[A](key: Key[A]): Option[A] = 51 | element.property[A](key.name).toOption 52 | 53 | def valueOption[A](key: Key[A], value: Option[A]): ElementType = value match { 54 | case Some(v) => setProperty(key, v) 55 | case None => removeProperty(key) 56 | } 57 | 58 | // note: this may throw an IllegalStateException - better use `Property` 59 | def values[A: DefaultsToAny](keys: String*): Iterator[A] = 60 | element.values[A](keys: _*).asScala 61 | 62 | def valueMap[A: DefaultsToAny]: Map[String, A] = 63 | valueMap[A](keys.toSeq.map(_.name): _*) 64 | 65 | def valueMap[A: DefaultsToAny](keys: String*): Map[String, A] = 66 | properties[A](keys: _*).map(p => (p.key, p.value)).toMap 67 | 68 | def toCC[CC <: Product: Marshallable]: CC 69 | 70 | def updateWith[CC <: Product: Marshallable](update: CC): ElementType = { 71 | val properties: List[(String, Any)] = implicitly[Marshallable[CC]].fromCC(update).properties 72 | val propMap = properties.toMap 73 | this.valueMap.keySet.diff(propMap.keySet).foreach { key => 74 | val prop = element.property(key) 75 | if (prop.isPresent) prop.remove() 76 | } 77 | propMap.foreach { case (key, value) => element.property(key, value) } 78 | 79 | element 80 | } 81 | 82 | def updateAs[CC <: Product: Marshallable](f: CC => CC): ElementType = 83 | updateWith(f(toCC[CC])) 84 | 85 | } 86 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/ScalaGraph.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.commons.configuration2.Configuration 4 | import org.apache.tinkerpop.gremlin.process.computer.GraphComputer 5 | import org.apache.tinkerpop.gremlin.structure.Graph.Variables 6 | import org.apache.tinkerpop.gremlin.structure.{T, Transaction} 7 | import shapeless._ 8 | 9 | object ScalaGraph { 10 | def apply(graph: Graph): ScalaGraph = 11 | ScalaGraph(TraversalSource(graph)) 12 | } 13 | 14 | case class ScalaGraph(traversalSource: TraversalSource) { 15 | lazy val traversal = traversalSource 16 | lazy val graph = traversalSource.graph 17 | 18 | def configure(conf: TraversalSource => TraversalSource) = 19 | ScalaGraph(conf(TraversalSource(graph))) 20 | 21 | def addVertex(): Vertex = 22 | traversalSource.underlying.addV().next 23 | 24 | def addVertex(label: String): Vertex = 25 | traversalSource.underlying.addV(label).next 26 | 27 | def addVertex(properties: (String, Any)*): Vertex = { 28 | val traversal = traversalSource.underlying.addV() 29 | properties.foreach { case (key, value) => traversal.property(key, value) } 30 | traversal.next 31 | } 32 | 33 | def addVertex(label: String, properties: (String, Any)*): Vertex = { 34 | val traversal = traversalSource.underlying.addV(label) 35 | properties.foreach { case (key, value) => traversal.property(key, value) } 36 | traversal.next 37 | } 38 | 39 | def addVertex(label: String, properties: Map[String, Any]): Vertex = 40 | addVertex(label, properties.toSeq: _*) 41 | 42 | def addVertex(properties: Map[String, Any]): Vertex = 43 | addVertex(properties.toSeq: _*) 44 | 45 | /** 46 | * Save an object's values as a new vertex 47 | * Note: `@id` members cannot be set for all graphs (e.g. remote graphs), so it is ignored here generally 48 | */ 49 | def addVertex[CC <: Product: Marshallable](cc: CC): Vertex = { 50 | val fromCC = implicitly[Marshallable[CC]].fromCC(cc) 51 | addVertex(fromCC.label, fromCC.properties: _*) 52 | } 53 | 54 | def +[CC <: Product: Marshallable](cc: CC): Vertex = addVertex(cc) 55 | 56 | def +(label: String): Vertex = addVertex(label) 57 | 58 | def +(label: String, properties: KeyValue[_]*): Vertex = 59 | addVertex(label, properties.map(v => (v.key.name, v.value)).toMap) 60 | 61 | /** start a traversal with `addV` */ 62 | def addV(): GremlinScala.Aux[Vertex, HNil] = 63 | traversalSource.addV() 64 | 65 | /** start a traversal with `addV` */ 66 | def addV(label: String): GremlinScala.Aux[Vertex, HNil] = 67 | traversalSource.addV(label) 68 | 69 | /** start a traversal with `addV` */ 70 | def addE(label: String): GremlinScala.Aux[Edge, HNil] = 71 | traversalSource.addE(label) 72 | 73 | /** start a traversal with given `starts`` */ 74 | def inject[S](starts: S*): GremlinScala.Aux[S, HNil] = 75 | traversalSource.inject(starts: _*) 76 | 77 | /** start traversal with all vertices */ 78 | def V(): GremlinScala.Aux[Vertex, HNil] = 79 | traversalSource.V() 80 | 81 | /** start traversal with all edges */ 82 | def E(): GremlinScala.Aux[Edge, HNil] = 83 | traversalSource.E() 84 | 85 | /** start traversal with some vertices identified by given ids */ 86 | def V(vertexIds: Any*): GremlinScala.Aux[Vertex, HNil] = 87 | traversalSource.V(vertexIds: _*) 88 | 89 | /** start traversal with some edges identified by given ids */ 90 | def E(edgeIds: Any*): GremlinScala.Aux[Edge, HNil] = 91 | traversalSource.E(edgeIds: _*) 92 | 93 | def tx(): Transaction = graph.tx() 94 | 95 | def variables(): Variables = graph.variables() 96 | 97 | def configuration(): Configuration = graph.configuration() 98 | 99 | def compute[C <: GraphComputer](graphComputerClass: Class[C]): C = 100 | graph.compute(graphComputerClass) 101 | 102 | def compute(): GraphComputer = graph.compute() 103 | 104 | def close(): Unit = graph.close() 105 | 106 | /* TODO: reimplement with createThreadedTx, if the underlying graph supports it */ 107 | // def transactional[R](work: Graph => R) = graph.tx.submit(work) 108 | } 109 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/ScalaVertex.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import java.util 4 | import org.apache.tinkerpop.gremlin.structure.VertexProperty.Cardinality 5 | import org.apache.tinkerpop.gremlin.structure.{Direction, T, VertexProperty} 6 | import scala.collection.JavaConverters._ 7 | 8 | case class ScalaVertex(vertex: Vertex) extends ScalaElement[Vertex] { 9 | override def element = vertex 10 | 11 | def toCC[CC <: Product: Marshallable] = 12 | implicitly[Marshallable[CC]].toCC(vertex) 13 | 14 | override def setProperty[A](key: Key[A], value: A): Vertex = { 15 | element.property(key.name, value) 16 | vertex 17 | } 18 | 19 | def setProperties(properties: Map[Key[Any], Any]): Vertex = { 20 | properties.foreach { case (k, v) => setProperty(k, v) } 21 | vertex 22 | } 23 | 24 | def setProperties[CC <: Product: Marshallable](cc: CC): Vertex = { 25 | val fromCC = implicitly[Marshallable[CC]].fromCC(cc) 26 | fromCC.properties.foreach { case (k, v) => setProperty(Key[Any](k), v) } 27 | vertex 28 | } 29 | 30 | override def removeProperty(key: Key[_]): Vertex = 31 | removeProperty(key, Cardinality.single) 32 | 33 | def removeProperty(key: Key[_], cardinality: Cardinality): Vertex = { 34 | cardinality match { 35 | case Cardinality.single => 36 | val p = property(key) 37 | if (p.isPresent) p.remove 38 | case Cardinality.list | Cardinality.set => 39 | vertex.properties(key.name).asScala.foreach { p: VertexProperty[_] => 40 | p.remove 41 | } 42 | } 43 | vertex 44 | } 45 | 46 | override def removeProperties(keys: Key[_]*): Vertex = { 47 | keys.foreach(removeProperty) 48 | vertex 49 | } 50 | 51 | def out() = start().out() 52 | 53 | def out(labels: String*) = start().out(labels: _*) 54 | 55 | def outE() = start().outE() 56 | 57 | def outE(labels: String*) = start().outE(labels: _*) 58 | 59 | def in() = start().in() 60 | 61 | def in(labels: String*) = start().in(labels: _*) 62 | 63 | def inE() = start().inE() 64 | 65 | def inE(labels: String*) = start().inE(labels: _*) 66 | 67 | def both() = start().both() 68 | 69 | def both(labels: String*) = start().both(labels: _*) 70 | 71 | def bothE() = start().bothE() 72 | 73 | def bothE(labels: String*) = start().bothE(labels: _*) 74 | 75 | /** `implicit ScalaGraph` required for configuration, e.g. when using remote graph */ 76 | def addEdge(label: String, inVertex: Vertex, properties: KeyValue[_]*)(implicit graph: ScalaGraph): Edge = 77 | graph.traversal.V(vertex).addE(label, properties: _*).to(inVertex).head() 78 | 79 | def addEdge[CC <: Product: Marshallable](inVertex: Vertex, cc: CC): Edge = { 80 | val fromCC = implicitly[Marshallable[CC]].fromCC(cc) 81 | val idParam = fromCC.id.toSeq.flatMap(List(T.id, _)) 82 | val params = fromCC.properties.flatMap(pair => Seq(pair._1, pair._2.asInstanceOf[AnyRef])) 83 | vertex.addEdge(fromCC.label, inVertex.vertex, idParam ++ params: _*) 84 | } 85 | 86 | /** `implicit ScalaGraph` required for configuration, e.g. when using remote graph */ 87 | def <--(se: SemiEdge)(implicit graph: ScalaGraph): Edge = 88 | se.from.asScala().addEdge(se.label, vertex, se.properties: _*) 89 | 90 | /** `implicit ScalaGraph` required for configuration, e.g. when using remote graph */ 91 | def <--(de: SemiDoubleEdge)(implicit graph: ScalaGraph): (Edge, Edge) = 92 | addEdge(de.label, de.right, de.properties: _*) -> de.right.asScala() 93 | .addEdge(de.label, vertex, de.properties: _*) 94 | 95 | def ---(label: String): SemiEdge = SemiEdge(vertex, label) 96 | 97 | def ---(label: String, properties: KeyValue[_]*): SemiEdge = 98 | SemiEdge(vertex, label, properties: _*) 99 | 100 | def ---(label: String, properties: Map[String, Any]): SemiEdge = 101 | SemiEdge(vertex, label, properties.map { 102 | case (key, value) => Key[Any](key) -> value 103 | }.toSeq: _*) 104 | 105 | def ---[CC <: Product: Marshallable](cc: CC): SemiEdge = { 106 | val fromCC = implicitly[Marshallable[CC]].fromCC(cc) 107 | SemiEdge(vertex, fromCC.label, fromCC.properties.map { r => 108 | Key[Any](r._1) -> r._2 109 | }.toSeq: _*) 110 | } 111 | 112 | def vertices(direction: Direction, edgeLabels: String*): util.Iterator[Vertex] = 113 | vertex.vertices(direction, edgeLabels: _*) 114 | 115 | def edges(direction: Direction, edgeLabels: String*): util.Iterator[Edge] = 116 | vertex.edges(direction, edgeLabels: _*) 117 | 118 | def property[A](cardinality: Cardinality, 119 | key: Key[A], 120 | value: A, 121 | keyValues: AnyRef*): VertexProperty[A] = 122 | vertex.property(cardinality, key.name, value, keyValues: _*) 123 | 124 | /** convenience function for `property` which normally requires to pass in key/value pairs as varargs */ 125 | def setPropertyList[A <: AnyRef](key: String, values: List[A]): VertexProperty[A] = { 126 | removeProperty(Key[A](key), Cardinality.list) 127 | values 128 | .map { value => 129 | vertex.property(Cardinality.list, key, value) 130 | } 131 | .lastOption 132 | .getOrElse(VertexProperty.empty[A]) 133 | } 134 | /* TODO: looks like there's a bug in tinkerpop - passing multiple values with the same key doesn't result in all those values being set: 135 | values match { 136 | case Nil => 137 | removeProperty(key) 138 | VertexProperty.empty[A] 139 | case headValue :: tailValues => 140 | val varargs = tailValues.flatMap(value => List(key.name, value)) 141 | vertex.property(Cardinality.list, key.name, headValue, varargs: _*) 142 | } 143 | */ 144 | 145 | def setPropertyList[A <: AnyRef](key: Key[A], values: List[A]): VertexProperty[A] = 146 | setPropertyList(key.name, values) 147 | 148 | override def properties[A: DefaultsToAny]: Stream[VertexProperty[A]] = 149 | vertex.properties[A](keys.map(_.name).toSeq: _*).asScala.toStream 150 | 151 | override def properties[A: DefaultsToAny](wantedKeys: String*): Stream[VertexProperty[A]] = 152 | vertex.properties[A](wantedKeys: _*).asScala.toStream 153 | } 154 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/Schema.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | // a type safe key for a property of vertices or edges 4 | case class Key[A](name: String) { 5 | def ->(value: A): KeyValue[A] = KeyValue(this, value) 6 | 7 | def of(value: A): KeyValue[A] = KeyValue(this, value) 8 | } 9 | 10 | case class KeyValue[A](key: Key[A], value: A) 11 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/SelectAllStep.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.tinkerpop.gremlin.process.traversal.{Path, Traversal} 4 | import org.apache.tinkerpop.gremlin.process.traversal.Traverser.Admin 5 | import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent 6 | import org.apache.tinkerpop.gremlin.process.traversal.step.map.ScalarMapStep 7 | import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement 8 | 9 | import scala.collection.JavaConverters._ 10 | import shapeless._ 11 | import shapeless.ops.hlist._ 12 | 13 | class SelectAllStep[S, Labels <: HList, LabelsTuple](traversal: Traversal[_, _])( 14 | implicit tupler: Tupler.Aux[Labels, LabelsTuple]) 15 | extends ScalarMapStep[S, LabelsTuple](traversal.asAdmin) 16 | with TraversalParent { 17 | 18 | override def getRequirements = Set(TraverserRequirement.PATH).asJava 19 | 20 | protected def map(traverser: Admin[S]): LabelsTuple = { 21 | val labels: Labels = toHList(toList(traverser.path)) 22 | tupler(labels) 23 | } 24 | 25 | def toList(path: Path): List[Any] = { 26 | val labels = path.labels 27 | def hasUserLabel(i: Int) = !labels.get(i).isEmpty 28 | 29 | (0 until path.size).filter(hasUserLabel).map(path.get[Any]).toList 30 | } 31 | 32 | private def toHList[T <: HList](path: List[_]): T = 33 | if (path.isEmpty) 34 | HNil.asInstanceOf[T] 35 | else 36 | (path.head :: toHList[IsHCons[T]#T](path.tail)).asInstanceOf[T] 37 | } 38 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/SemiEdge.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | case class SemiEdge(from: Vertex, label: String, properties: KeyValue[_]*) { 4 | 5 | /** `implicit ScalaGraph` required for configuration, e.g. when using remote graph */ 6 | def -->(to: Vertex)(implicit graph: ScalaGraph) = 7 | from.asScala().addEdge(label, to, properties: _*) 8 | } 9 | 10 | case class SemiDoubleEdge(right: Vertex, label: String, properties: KeyValue[_]*) 11 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/StepLabel.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import java.util.UUID.randomUUID 4 | import java.util.{Map => JMap} 5 | import scala.annotation.implicitNotFound 6 | import shapeless._ 7 | import shapeless.poly._ 8 | 9 | // type safety for labelled steps 10 | case class StepLabel[A](name: String = randomUUID.toString) 11 | 12 | object StepLabel { 13 | 14 | object GetLabelName extends (StepLabel ~>> String) { 15 | def apply[B](label: StepLabel[B]) = label.name 16 | } 17 | 18 | object combineLabelWithValue extends Poly2 { 19 | implicit def atLabel[A, L <: HList] = 20 | at[StepLabel[A], (L, JMap[String, Any])] { 21 | case (label, (acc, values)) => 22 | (values.get(label.name).asInstanceOf[A] :: acc, values) 23 | } 24 | } 25 | 26 | trait ExtractLabelType[A] { 27 | type Out 28 | } 29 | 30 | object ExtractLabelType extends LowPriorityExtractLabelTypeImplicits { 31 | @implicitNotFound( 32 | "Unable to find implicit for extracting LabelType of StepLabel `${A}`. " 33 | + "We probably need to add an implicit def to `LowPriorityExtractLabelTypeImplicits`") 34 | type Aux[A, Out0] = ExtractLabelType[A] { type Out = Out0 } 35 | } 36 | 37 | trait LowPriorityExtractLabelTypeImplicits { 38 | implicit def forSingle[A] = new ExtractLabelType[StepLabel[A]] { 39 | type Out = A 40 | } 41 | 42 | implicit def forHNil = new ExtractLabelType[HNil] { type Out = HNil } 43 | 44 | implicit def forHList[H, T <: HList, HOut, TOut <: HList]( 45 | implicit hExtractLabelType: ExtractLabelType.Aux[H, HOut], 46 | tExtractLabelType: ExtractLabelType.Aux[T, TOut]) 47 | : ExtractLabelType.Aux[H :: T, HOut :: TOut] = 48 | new ExtractLabelType[H :: T] { type Out = HOut :: TOut } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/TraversalSource.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import java.util.function.{BinaryOperator, Supplier, UnaryOperator} 4 | import org.apache.commons.configuration2.Configuration 5 | import org.apache.tinkerpop.gremlin.process.remote.RemoteConnection 6 | import org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource.traversal 7 | import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource 8 | import shapeless.HNil 9 | 10 | object TraversalSource { 11 | def apply(graph: Graph): TraversalSource = 12 | TraversalSource(graph.traversal()) 13 | } 14 | 15 | case class TraversalSource(underlying: GraphTraversalSource) { 16 | def graph: Graph = underlying.getGraph 17 | 18 | def addV(): GremlinScala.Aux[Vertex, HNil] = 19 | GremlinScala[Vertex, HNil](underlying.addV()) 20 | 21 | def addV(label: String): GremlinScala.Aux[Vertex, HNil] = 22 | GremlinScala[Vertex, HNil](underlying.addV(label)) 23 | 24 | def addE(label: String): GremlinScala.Aux[Edge, HNil] = 25 | GremlinScala[Edge, HNil](underlying.addE(label)) 26 | 27 | def inject[S](starts: S*): GremlinScala.Aux[S, HNil] = 28 | GremlinScala[S, HNil](underlying.inject(starts: _*)) 29 | 30 | // start traversal with all vertices 31 | def V(): GremlinScala.Aux[Vertex, HNil] = 32 | GremlinScala[Vertex, HNil](underlying.V()) 33 | 34 | // start traversal with all edges 35 | def E(): GremlinScala.Aux[Edge, HNil] = 36 | GremlinScala[Edge, HNil](underlying.E()) 37 | 38 | // start traversal with some vertices identified by given ids 39 | def V(vertexIds: Any*): GremlinScala.Aux[Vertex, HNil] = 40 | GremlinScala[Vertex, HNil](underlying.V(vertexIds.asInstanceOf[Seq[AnyRef]]: _*)) 41 | 42 | // start traversal with some edges identified by given ids 43 | def E(edgeIds: Any*): GremlinScala.Aux[Edge, HNil] = 44 | GremlinScala[Edge, HNil](underlying.E(edgeIds.asInstanceOf[Seq[AnyRef]]: _*)) 45 | 46 | def withSack[A](initialValue: A): TraversalSource = 47 | withSack(() => initialValue) 48 | 49 | def withSack[A](initialValue: () => A) = 50 | TraversalSource(underlying.withSack(initialValue: Supplier[A])) 51 | 52 | def withSack[A](initialValue: A, splitOperator: A => A): TraversalSource = 53 | withSack(() => initialValue, splitOperator) 54 | 55 | def withSack[A](initialValue: () => A, splitOperator: A => A) = 56 | TraversalSource(underlying.withSack(initialValue: Supplier[A], splitOperator: UnaryOperator[A])) 57 | 58 | def withSack[A](initialValue: A, mergeOperator: (A, A) => A): TraversalSource = 59 | withSack(() => initialValue, mergeOperator) 60 | 61 | def withSack[A](initialValue: () => A, mergeOperator: (A, A) => A) = 62 | TraversalSource( 63 | underlying.withSack(initialValue: Supplier[A], mergeOperator: BinaryOperator[A])) 64 | 65 | def withSack[A](initialValue: A, 66 | splitOperator: A => A, 67 | mergeOperator: (A, A) => A): TraversalSource = 68 | withSack(() => initialValue, splitOperator, mergeOperator) 69 | 70 | def withSack[A](initialValue: () => A, splitOperator: A => A, mergeOperator: (A, A) => A) = 71 | TraversalSource( 72 | underlying.withSack(initialValue: Supplier[A], 73 | splitOperator: UnaryOperator[A], 74 | mergeOperator: BinaryOperator[A])) 75 | 76 | def withSideEffect[A](key: String, initialValue: A) = 77 | TraversalSource(underlying.withSideEffect(key, initialValue)) 78 | 79 | def withSideEffect[A](key: String, initialValue: () => A) = 80 | TraversalSource(underlying.withSideEffect(key, initialValue: Supplier[A])) 81 | 82 | def withSideEffect[A](key: String, initialValue: A, reducer: (A, A) => A) = 83 | TraversalSource(underlying.withSideEffect(key, initialValue, reducer: BinaryOperator[A])) 84 | 85 | def withSideEffect[A](key: String, initialValue: () => A, reducer: (A, A) => A) = 86 | TraversalSource( 87 | underlying.withSideEffect(key, initialValue: Supplier[A], reducer: BinaryOperator[A])) 88 | 89 | def withRemote(configFile: String): TraversalSource = 90 | TraversalSource(traversal().withRemote(configFile)) 91 | 92 | def withRemote(configuration: Configuration): TraversalSource = 93 | TraversalSource(traversal().withRemote(configuration)) 94 | 95 | def withRemote(connection: RemoteConnection): TraversalSource = 96 | TraversalSource(traversal().withRemote(connection)) 97 | } 98 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/UnionTraversals.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import java.util.{List => JList} 4 | import shapeless.{::, HList, HNil} 5 | import shapeless.ops.hlist.Prepend 6 | 7 | /** helper class to construct a typed union step */ 8 | class UnionTraversals[Start, Ends <: HList]( 9 | val travsUntyped: Seq[GremlinScala.Aux[Start, HNil] => GremlinScala[_]]) { 10 | 11 | def join[End](trav: GremlinScala.Aux[Start, HNil] => GremlinScala[End])( 12 | implicit p: Prepend[Ends, JList[End] :: HNil]): UnionTraversals[Start, p.Out] = 13 | new UnionTraversals[Start, p.Out](travsUntyped :+ trav) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/dsl/AnonymousVertex.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala.dsl 2 | 3 | import gremlin.scala._ 4 | import java.util.{Iterator => JIterator} 5 | import scala.collection.JavaConverters._ 6 | 7 | object AnonymousVertex { 8 | import org.apache.tinkerpop.gremlin.structure._ 9 | 10 | def apply[CC <: Product](cc: CC)(implicit marshaller: Marshallable[CC], grph: Graph): Vertex = { 11 | val fromCC = marshaller.fromCC(cc) 12 | new Vertex { 13 | val graph = grph 14 | val id = fromCC.id 15 | val label = fromCC.label 16 | def remove(): Unit = ??? 17 | def addEdge(label: String, inVertex: Vertex, keyValues: Object*): Edge = 18 | ??? 19 | def edges(direction: Direction, edgeLabels: String*): JIterator[Edge] = 20 | ??? 21 | def property[V](cardinality: VertexProperty.Cardinality, 22 | key: String, 23 | value: V, 24 | keyValues: Object*): VertexProperty[V] = ??? 25 | def vertices(direction: Direction, edgeLabels: String*): JIterator[Vertex] = ??? 26 | def properties[V](keys: String*): JIterator[VertexProperty[V]] = { 27 | val requestedKeys: Set[String] = keys.toSet 28 | val x: Iterable[VertexProperty[V]] = fromCC.properties.collect { 29 | case (ccKey, ccValue) if requestedKeys.contains(ccKey) => 30 | new VertexProperty[V] { 31 | // Members declared in org.apache.tinkerpop.gremlin.structure.Element 32 | def id(): Object = ??? 33 | def property[V](x$1: String, x$2: V): Property[V] = ??? 34 | def remove(): Unit = ??? 35 | 36 | // Members declared in org.apache.tinkerpop.gremlin.structure.Property 37 | def isPresent(): Boolean = true 38 | def key(): String = ccKey 39 | def value(): V = ccValue.asInstanceOf[V] 40 | 41 | // Members declared in org.apache.tinkerpop.gremlin.structure.VertexProperty 42 | def element(): Vertex = ??? 43 | def properties[U](x$1: String*): JIterator[Property[U]] = ??? 44 | } 45 | } 46 | x.iterator.asJava 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/dsl/Constructor.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala.dsl 2 | 3 | import gremlin.scala._ 4 | import shapeless._ 5 | import shapeless.ops.hlist.Tupler 6 | import shapeless.ops.product.ToHList 7 | 8 | trait Constructor[DomainType, Labels <: HList] { 9 | type GraphType 10 | type StepsType 11 | def apply(raw: GremlinScala[GraphType]): StepsType 12 | } 13 | 14 | object Constructor extends LowPriorityConstructorImplicits { 15 | type Aux[DomainType, Labels <: HList, GraphTypeOut, StepsTypeOut] = 16 | Constructor[DomainType, Labels] { 17 | type GraphType = GraphTypeOut 18 | type StepsType = StepsTypeOut 19 | } 20 | } 21 | 22 | trait LowPriorityConstructorImplicits extends LowestPriorityConstructorImplicits { 23 | 24 | implicit def forSimpleType[A, Labels <: HList](implicit converter: Converter.Aux[A, A]) = 25 | new Constructor[A, Labels] { 26 | type GraphType = A 27 | type StepsType = Steps[A, A, Labels] 28 | def apply(raw: GremlinScala[GraphType]) = new Steps[A, A, Labels](raw) 29 | } 30 | 31 | def forDomainNode[DomainType <: DomainRoot, 32 | Labels <: HList, 33 | StepsTypeOut <: NodeSteps[DomainType, Labels]]( 34 | constr: GremlinScala[Vertex] => StepsTypeOut) = 35 | new Constructor[DomainType, Labels] { 36 | type GraphType = Vertex 37 | type StepsType = StepsTypeOut 38 | 39 | def apply(raw: GremlinScala[GraphType]): StepsTypeOut = constr(raw) 40 | } 41 | 42 | implicit def forList[A, AGraphType, Labels <: HList, AStepsType]( 43 | implicit aConverter: Converter.Aux[A, AGraphType]) = 44 | new Constructor[List[A], Labels] { 45 | type GraphType = List[AGraphType] 46 | type StepsType = Steps[List[A], List[AGraphType], Labels] 47 | def apply(raw: GremlinScala[GraphType]) = 48 | new Steps[List[A], List[AGraphType], Labels](raw) 49 | } 50 | 51 | implicit def forSet[A, AGraphType, Labels <: HList, AStepsType]( 52 | implicit aConverter: Converter.Aux[A, AGraphType]) = 53 | new Constructor[Set[A], Labels] { 54 | type GraphType = Set[AGraphType] 55 | type StepsType = Steps[Set[A], Set[AGraphType], Labels] 56 | def apply(raw: GremlinScala[GraphType]) = 57 | new Steps[Set[A], Set[AGraphType], Labels](raw) 58 | } 59 | 60 | implicit val forHNil = new Constructor[HNil, HNil] { 61 | type GraphType = HNil 62 | type StepsType = Steps[HNil, HNil, HNil] 63 | def apply(raw: GremlinScala[HNil]) = new Steps[HNil, HNil, HNil](raw) 64 | } 65 | 66 | implicit def forHList[H, 67 | HGraphType, 68 | Labels <: HList, 69 | HStepsType, 70 | T <: HList, 71 | TGraphType <: HList, 72 | TStepsType](implicit 73 | hConstr: Constructor.Aux[H, Labels, HGraphType, HStepsType], 74 | tConstr: Constructor.Aux[T, Labels, TGraphType, TStepsType], 75 | converter: Converter.Aux[H :: T, HGraphType :: TGraphType]) = 76 | new Constructor[H :: T, Labels] { 77 | type GraphType = HGraphType :: TGraphType 78 | type StepsType = Steps[H :: T, HGraphType :: TGraphType, Labels] 79 | def apply(raw: GremlinScala[GraphType]): StepsType = 80 | new Steps[H :: T, HGraphType :: TGraphType, Labels](raw) 81 | } 82 | } 83 | 84 | trait LowestPriorityConstructorImplicits { 85 | // for all Products, e.g. tuples, case classes etc 86 | implicit def forGeneric[ 87 | T, 88 | Repr <: HList, 89 | GraphTypeHList <: HList, 90 | GraphTypeTuple <: Product, 91 | Labels <: HList, 92 | StepsType0 <: StepsRoot, 93 | EndDomainHList <: HList, 94 | EndDomainTuple <: Product 95 | ](implicit 96 | gen: Generic.Aux[T, Repr], 97 | constr: Constructor.Aux[Repr, Labels, GraphTypeHList, StepsType0], 98 | graphTypeTupler: Tupler.Aux[GraphTypeHList, GraphTypeTuple], 99 | eq: StepsType0#EndDomain0 =:= EndDomainHList, 100 | tupler: Tupler.Aux[EndDomainHList, EndDomainTuple], 101 | converter: Converter.Aux[T, GraphTypeTuple]) = 102 | new Constructor[T, Labels] { 103 | type GraphType = GraphTypeTuple 104 | type StepsType = Steps[T, GraphType, Labels] 105 | def apply(raw: GremlinScala[GraphType]): StepsType = 106 | new Steps[T, GraphType, Labels](raw) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/dsl/Converter.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala.dsl 2 | 3 | import gremlin.scala._ 4 | import shapeless._ 5 | import shapeless.ops.hlist.Tupler 6 | import shapeless.ops.product.ToHList 7 | 8 | trait Converter[DomainType] { 9 | type GraphType 10 | def toGraph(domainType: DomainType): GraphType 11 | def toDomain(graphType: GraphType): DomainType 12 | } 13 | 14 | object Converter extends LowPriorityConverterImplicits { 15 | type Aux[DomainType, Out0] = Converter[DomainType] { type GraphType = Out0 } 16 | } 17 | 18 | trait LowPriorityConverterImplicits extends LowestPriorityConverterImplicits { 19 | /* need to explicitly create these for the base types, otherwise it there would 20 | * be ambiguous implicits (given Converter.forDomainNode) */ 21 | implicit def forUnit = identityConverter[Unit] 22 | implicit val forString = identityConverter[String] 23 | implicit val forInt = identityConverter[Int] 24 | implicit val forLong = identityConverter[Long] 25 | implicit val forDouble = identityConverter[Double] 26 | implicit val forFloat = identityConverter[Float] 27 | implicit val forBoolean = identityConverter[Boolean] 28 | implicit val forInteger = identityConverter[Integer] 29 | implicit val forJLong = identityConverter[java.lang.Long] 30 | implicit val forJDouble = identityConverter[java.lang.Double] 31 | implicit val forJFloat = identityConverter[java.lang.Float] 32 | implicit val forJBoolean = identityConverter[java.lang.Boolean] 33 | def identityConverter[A] = new Converter[A] { 34 | type GraphType = A 35 | def toGraph(value: A) = value 36 | def toDomain(value: A) = value 37 | } 38 | 39 | implicit def forDomainNode[DomainType <: DomainRoot]( 40 | implicit marshaller: Marshallable[DomainType], 41 | graph: Graph) = new Converter[DomainType] { 42 | type GraphType = Vertex 43 | def toDomain(v: Vertex): DomainType = marshaller.toCC(v) 44 | def toGraph(dt: DomainType): Vertex = AnonymousVertex(dt) 45 | } 46 | 47 | implicit def forList[A, AGraphType](implicit aConverter: Converter.Aux[A, AGraphType]) = 48 | new Converter[List[A]] { 49 | type GraphType = List[AGraphType] 50 | def toDomain(aGraphs: List[AGraphType]): List[A] = 51 | aGraphs.map(aConverter.toDomain) 52 | def toGraph(as: List[A]): List[AGraphType] = as.map(aConverter.toGraph) 53 | } 54 | 55 | implicit def forSet[A, AGraphType](implicit aConverter: Converter.Aux[A, AGraphType]) = 56 | new Converter[Set[A]] { 57 | type GraphType = Set[AGraphType] 58 | def toDomain(aGraphs: Set[AGraphType]): Set[A] = 59 | aGraphs.map(aConverter.toDomain) 60 | def toGraph(as: Set[A]): Set[AGraphType] = as.map(aConverter.toGraph) 61 | } 62 | 63 | implicit val forHNil = new Converter[HNil] { 64 | type GraphType = HNil 65 | def toGraph(value: HNil) = HNil 66 | def toDomain(value: GraphType) = HNil 67 | } 68 | 69 | implicit def forHList[H, HGraphType, T <: HList, TGraphType <: HList]( 70 | implicit 71 | hConverter: Converter.Aux[H, HGraphType], 72 | tConverter: Converter.Aux[T, TGraphType]): Converter.Aux[H :: T, HGraphType :: TGraphType] = 73 | new Converter[H :: T] { 74 | type GraphType = HGraphType :: TGraphType 75 | 76 | def toGraph(values: H :: T): GraphType = values match { 77 | case h :: t => hConverter.toGraph(h) :: tConverter.toGraph(t) 78 | } 79 | 80 | def toDomain(values: GraphType): H :: T = values match { 81 | case h :: t => hConverter.toDomain(h) :: tConverter.toDomain(t) 82 | } 83 | } 84 | } 85 | 86 | trait LowestPriorityConverterImplicits { 87 | // for all Products, e.g. tuples, case classes etc 88 | implicit def forGeneric[T, Repr <: HList, GraphType <: HList, GraphTypeTuple <: Product]( 89 | implicit 90 | gen: Generic.Aux[T, Repr], 91 | converter: Converter.Aux[Repr, GraphType], 92 | tupler: Tupler.Aux[GraphType, GraphTypeTuple], 93 | toHList: ToHList.Aux[GraphTypeTuple, GraphType]): Converter.Aux[T, GraphTypeTuple] = 94 | new Converter[T] { 95 | type GraphType = GraphTypeTuple 96 | 97 | def toGraph(value: T): GraphTypeTuple = 98 | tupler(converter.toGraph(gen.to(value))) 99 | 100 | def toDomain(value: GraphTypeTuple): T = 101 | gen.from(converter.toDomain(toHList(value))) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/dsl/NodeSteps.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala.dsl 2 | 3 | import gremlin.scala._ 4 | import java.util.{Map => JMap} 5 | import scala.collection.mutable 6 | import shapeless.HList 7 | 8 | /* Root class for all your vertex based DSL steps 9 | * TODO: add support for using Edge instead of Vertex? 10 | */ 11 | class NodeSteps[EndDomain <: DomainRoot, Labels <: HList](override val raw: GremlinScala[Vertex])( 12 | implicit marshaller: Marshallable[EndDomain]) 13 | extends Steps[EndDomain, Vertex, Labels](raw)( 14 | Converter.forDomainNode[EndDomain](marshaller, raw.traversal.asAdmin.getGraph.get)) { 15 | 16 | /* follow the incoming edges of the given type as long as possible */ 17 | def walkIn(edgeType: String): GremlinScala[Vertex] = 18 | raw 19 | .repeat(_.in(edgeType)) 20 | .until(_.in(edgeType).count().is(P.eq(0))) 21 | 22 | /** Aggregate all objects at this point into the given collection, e.g. `mutable.ArrayBuffer.empty[EndDomain]` 23 | * Uses eager evaluation (as opposed to `store`() which lazily fills a collection) 24 | */ 25 | def aggregate(into: mutable.Buffer[EndDomain]): NodeSteps[EndDomain, Labels] = 26 | new NodeSteps[EndDomain, Labels]( 27 | raw.sideEffect { v: Vertex => 28 | into += v.toCC[EndDomain] 29 | } 30 | ) 31 | 32 | def filterOnEnd(predicate: EndDomain => Boolean): NodeSteps[EndDomain, Labels] = 33 | new NodeSteps[EndDomain, Labels]( 34 | raw.filterOnEnd { v: Vertex => 35 | predicate(v.toCC[EndDomain]) 36 | } 37 | ) 38 | 39 | /** filter by id */ 40 | def id(ids: AnyRef*): NodeSteps[EndDomain, Labels] = 41 | new NodeSteps[EndDomain, Labels](raw.hasId(ids: _*)) 42 | 43 | /** 44 | Extend the traversal with a side-effect step, where `fun` is a 45 | function that performs a side effect. The function `fun` can 46 | access the current traversal element via the variable `_`. 47 | */ 48 | def sideEffect(fun: EndDomain => Any): NodeSteps[EndDomain, Labels] = 49 | new NodeSteps[EndDomain, Labels](raw.sideEffect { v: Vertex => 50 | fun(v.toCC[EndDomain]) 51 | }) 52 | 53 | } 54 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/dsl/Steps.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala.dsl 2 | 3 | import gremlin.scala._ 4 | import gremlin.scala.StepLabel.{combineLabelWithValue, GetLabelName} 5 | import java.util.{Map => JMap} 6 | import java.util.stream.{Stream => JStream} 7 | import scala.collection.mutable 8 | import shapeless.{::, HList, HNil} 9 | import shapeless.ops.hlist.{IsHCons, Mapper, Prepend, RightFolder, ToTraversable, Tupler} 10 | import shapeless.ops.product.ToHList 11 | 12 | /** root type for all domain types */ 13 | trait DomainRoot extends Product { 14 | // type Underlying 15 | } 16 | 17 | /** just a helper trait for extracting type members for Steps */ 18 | trait StepsRoot { 19 | type EndDomain0 20 | type EndGraph0 21 | def raw: GremlinScala[EndGraph0] 22 | } 23 | 24 | class Steps[EndDomain, EndGraph, Labels <: HList](val raw: GremlinScala[EndGraph])( 25 | implicit val converter: Converter.Aux[EndDomain, EndGraph]) 26 | extends StepsRoot { 27 | type EndDomain0 = EndDomain 28 | type EndGraph0 = EndGraph 29 | 30 | /* executes traversal and converts results into cpg domain type */ 31 | def toList(): List[EndDomain] = raw.toList().map(converter.toDomain) 32 | def toStream(): JStream[EndDomain] = raw.toStream().map { end: EndGraph => converter.toDomain(end) } 33 | def toSet(): Set[EndDomain] = raw.toSet().map(converter.toDomain) 34 | def iterate(): Unit = raw.iterate() 35 | def exec(): Unit = iterate() 36 | 37 | /** 38 | Execute the traversal and convert it to a mutable buffer 39 | */ 40 | def toBuffer(): mutable.Buffer[EndDomain] = toList().toBuffer 41 | 42 | def head(): EndDomain = converter.toDomain(raw.head()) 43 | def headOption(): Option[EndDomain] = raw.headOption().map(converter.toDomain) 44 | def isDefined: Boolean = headOption().isDefined 45 | 46 | /** 47 | * shortcut for `toList` 48 | */ 49 | def l: List[EndDomain] = toList() 50 | 51 | /** 52 | Alias for `toStream` 53 | */ 54 | def s(): JStream[EndDomain] = toStream() 55 | 56 | /** 57 | * print the results to stdout 58 | */ 59 | def p(): List[String] = { 60 | l.map { 61 | case vertex: Vertex => { 62 | val label = vertex.label 63 | val id = vertex.id().toString 64 | val keyValPairs = vertex.valueMap.toList 65 | .filter(x => x._2.toString != "") 66 | .sortBy(_._1) 67 | .map(x => x._1 + ": " + x._2) 68 | s"($label,$id): " + keyValPairs.mkString(", ") 69 | } 70 | case elem => elem.toString 71 | } 72 | } 73 | 74 | def count(): Long = 75 | raw.count().head() 76 | 77 | override def clone() = new Steps[EndDomain, EndGraph, Labels](raw.clone()) 78 | 79 | def dedup(): Steps[EndDomain, EndGraph, Labels] = 80 | new Steps[EndDomain, EndGraph, Labels](raw.dedup()) 81 | 82 | /* access all gremlin-scala methods that don't modify the EndGraph type, e.g. `has` */ 83 | /* TODO: track/use NewLabelsGraph as given by `fun` */ 84 | def onRaw( 85 | fun: GremlinScala[EndGraph] => GremlinScala[EndGraph]): Steps[EndDomain, EndGraph, Labels] = 86 | new Steps[EndDomain, EndGraph, Labels](fun(raw)) 87 | 88 | /* TODO: track/use NewLabelsGraph as given by `fun` */ 89 | def map[NewEndDomain, NewEndGraph, NewSteps <: StepsRoot](fun: EndDomain => NewEndDomain)( 90 | implicit 91 | newConverter: Converter.Aux[NewEndDomain, NewEndGraph], 92 | constr: Constructor.Aux[NewEndDomain, Labels, NewEndGraph, NewSteps]): NewSteps = 93 | constr { 94 | raw.map { endGraph: EndGraph => 95 | newConverter.toGraph(fun(converter.toDomain(endGraph))) 96 | } 97 | } 98 | 99 | /* TODO: track/use NewLabelsGraph as given by `fun` */ 100 | def flatMap[NewSteps <: StepsRoot](fun: EndDomain => NewSteps)( 101 | implicit 102 | constr: Constructor.Aux[NewSteps#EndDomain0, Labels, NewSteps#EndGraph0, NewSteps], 103 | newConverter: Converter[NewSteps#EndDomain0] 104 | ): NewSteps = 105 | constr { 106 | raw.flatMap { endGraph: EndGraph => 107 | val newSteps: NewSteps = fun(converter.toDomain(endGraph)) 108 | newSteps.raw.asInstanceOf[GremlinScala[NewSteps#EndGraph0]] 109 | // not sure why I need the cast here - should be safe though 110 | } 111 | } 112 | 113 | def filter(predicate: Steps[EndDomain, EndGraph, Labels] => Steps[_, _, _]) 114 | : Steps[EndDomain, EndGraph, Labels] = { 115 | val rawWithFilter: GremlinScala[EndGraph] = 116 | raw.filter { gs => 117 | predicate( 118 | new Steps[EndDomain, EndGraph, Labels](gs) 119 | ).raw 120 | } 121 | new Steps[EndDomain, EndGraph, Labels](rawWithFilter) 122 | } 123 | 124 | def filterNot(predicate: Steps[EndDomain, EndGraph, Labels] => Steps[_, _, _]) 125 | : Steps[EndDomain, EndGraph, Labels] = { 126 | val rawWithFilter: GremlinScala[EndGraph] = 127 | raw.filterNot { gs => 128 | predicate( 129 | new Steps[EndDomain, EndGraph, Labels](gs) 130 | ).raw 131 | } 132 | new Steps[EndDomain, EndGraph, Labels](rawWithFilter) 133 | } 134 | 135 | // labels the current step and preserves the type - use together with `select` step 136 | def as[NewLabels <: HList](stepLabel: String)( 137 | implicit prependDomain: Prepend.Aux[Labels, EndDomain :: HNil, NewLabels]) 138 | : Steps[EndDomain, EndGraph, NewLabels] = 139 | new Steps[EndDomain, EndGraph, NewLabels]( 140 | raw.asInstanceOf[GremlinScala.Aux[EndGraph, HNil]].as(stepLabel)) 141 | 142 | def as[NewLabels <: HList](stepLabel: StepLabel[EndDomain])( 143 | implicit prependDomain: Prepend.Aux[Labels, EndDomain :: HNil, NewLabels]) 144 | : Steps[EndDomain, EndGraph, NewLabels] = 145 | new Steps[EndDomain, EndGraph, NewLabels]( 146 | raw.asInstanceOf[GremlinScala.Aux[EndGraph, HNil]].as(stepLabel.name)) 147 | 148 | // select all labels 149 | def select[LabelsGraph <: HList, LabelsGraphTuple, LabelsTuple]()( 150 | implicit 151 | conv1: Converter.Aux[Labels, LabelsGraph], 152 | tupler1: Tupler.Aux[LabelsGraph, LabelsGraphTuple], 153 | tupler2: Tupler.Aux[Labels, LabelsTuple], 154 | conv2: Converter.Aux[LabelsTuple, LabelsGraphTuple] 155 | ) = new Steps[LabelsTuple, LabelsGraphTuple, Labels]( 156 | raw.asInstanceOf[GremlinScala.Aux[EndGraph, LabelsGraph]].select() 157 | ) 158 | 159 | // select one specific label 160 | def select[Label, LabelGraph](label: StepLabel[Label])( 161 | implicit conv1: Converter.Aux[Label, LabelGraph]) = 162 | new Steps[Label, LabelGraph, Labels](raw.select(StepLabel[LabelGraph](label.name))) 163 | 164 | // select multiple specific labels 165 | def select[StepLabelsTuple <: Product, 166 | StepLabels <: HList, 167 | H0, 168 | T0 <: HList, 169 | SelectedTypes <: HList, 170 | SelectedTypesTuple <: Product, 171 | SelectedGraphTypesTuple <: Product, 172 | LabelNames <: HList, 173 | Z](stepLabelsTuple: StepLabelsTuple)( 174 | implicit toHList: ToHList.Aux[StepLabelsTuple, StepLabels], 175 | hasOne: IsHCons.Aux[StepLabels, H0, T0], 176 | hasTwo: IsHCons[T0], // witnesses that labels has > 1 elements 177 | extractLabelType: StepLabel.ExtractLabelType.Aux[StepLabels, SelectedTypes], 178 | tupler: Tupler.Aux[SelectedTypes, SelectedTypesTuple], 179 | conv: Converter.Aux[SelectedTypesTuple, SelectedGraphTypesTuple], 180 | stepLabelToString: Mapper.Aux[GetLabelName.type, StepLabels, LabelNames], 181 | trav: ToTraversable.Aux[LabelNames, List, String], 182 | folder: RightFolder.Aux[StepLabels, 183 | (HNil, JMap[String, Any]), 184 | combineLabelWithValue.type, 185 | (SelectedTypes, Z)] 186 | ): Steps[SelectedTypesTuple, SelectedGraphTypesTuple, Labels] = { 187 | val stepLabels: StepLabels = toHList(stepLabelsTuple) 188 | val labels: List[String] = stepLabels.map(GetLabelName).toList 189 | val label1 = labels.head 190 | val label2 = labels.tail.head 191 | val remainder = labels.tail.tail 192 | 193 | val selectTraversal = 194 | raw.traversal.select[Any](label1, label2, remainder: _*) 195 | val newRaw: GremlinScala[SelectedGraphTypesTuple] = 196 | GremlinScala(selectTraversal).map { selectValues => 197 | val resultTuple = stepLabels.foldRight((HNil: HNil, selectValues))(combineLabelWithValue) 198 | val values: SelectedTypes = resultTuple._1 199 | tupler(values) 200 | .asInstanceOf[SelectedGraphTypesTuple] //dirty but does the trick 201 | } 202 | 203 | new Steps[SelectedTypesTuple, SelectedGraphTypesTuple, Labels](newRaw) 204 | } 205 | 206 | /** 207 | Repeat the given traversal. This step can be combined with the until and emit steps to 208 | provide a termination and emit criteria. 209 | */ 210 | def repeat[NewEndDomain >: EndDomain]( 211 | repeatTraversal: Steps[EndDomain, EndGraph, HNil] => Steps[NewEndDomain, EndGraph, _])( 212 | implicit newConverter: Converter.Aux[NewEndDomain, EndGraph]) 213 | : Steps[NewEndDomain, EndGraph, Labels] = 214 | new Steps[NewEndDomain, EndGraph, Labels]( 215 | raw.repeat { rawTraversal => 216 | repeatTraversal( 217 | new Steps[EndDomain, EndGraph, HNil](rawTraversal) 218 | ).raw 219 | } 220 | ) 221 | 222 | /** 223 | Termination criteria for a repeat step. 224 | If used before the repeat step it as "while" characteristics. 225 | If used after the repeat step it as "do-while" characteristics 226 | */ 227 | def until(untilTraversal: Steps[EndDomain, EndGraph, HNil] => Steps[_, _, _]) 228 | : Steps[EndDomain, EndGraph, Labels] = 229 | new Steps[EndDomain, EndGraph, Labels]( 230 | raw.until { rawTraversal => 231 | untilTraversal( 232 | new Steps[EndDomain, EndGraph, HNil](rawTraversal) 233 | ).raw 234 | } 235 | ) 236 | 237 | /** 238 | * Modifier for repeat steps. Configure the amount of times the repeat traversal is 239 | * executed. 240 | */ 241 | def times(maxLoops: Int): Steps[EndDomain, EndGraph, Labels] = 242 | new Steps[EndDomain, EndGraph, Labels](raw.times(maxLoops)) 243 | 244 | /** 245 | Emit is used with the repeat step to emit the elements of the repeatTraversal after each 246 | iteration of the repeat loop. 247 | */ 248 | def emit(): Steps[EndDomain, EndGraph, Labels] = 249 | new Steps[EndDomain, EndGraph, Labels](raw.emit()) 250 | 251 | /** 252 | Emit is used with the repeat step to emit the elements of the repeatTraversal after each 253 | iteration of the repeat loop. 254 | The emitTraversal defines under which condition the elements are emitted. 255 | */ 256 | def emit(emitTraversal: Steps[EndDomain, EndGraph, HNil] => Steps[_, _, _]) 257 | : Steps[EndDomain, EndGraph, Labels] = 258 | new Steps[EndDomain, EndGraph, Labels]( 259 | raw.emit { rawTraversal => 260 | emitTraversal( 261 | new Steps[EndDomain, EndGraph, HNil](rawTraversal) 262 | ).raw 263 | } 264 | ) 265 | 266 | /** 267 | * The or step is a filter with multiple `or` related filter traversals. 268 | */ 269 | def or(orTraversals: (Steps[EndDomain, EndGraph, HNil] => Steps[_, _, _])*) 270 | : Steps[EndDomain, EndGraph, Labels] = { 271 | val rawOrTraversals = orTraversals.map { 272 | orTraversal => (rawTraversal: GremlinScala[EndGraph]) => 273 | orTraversal( 274 | new Steps[EndDomain, EndGraph, HNil]( 275 | rawTraversal.asInstanceOf[GremlinScala.Aux[EndGraph, HNil]]) 276 | ).raw 277 | } 278 | 279 | new Steps[EndDomain, EndGraph, Labels]( 280 | raw.or(rawOrTraversals: _*) 281 | ) 282 | } 283 | 284 | /** 285 | * The and step is a filter with multiple `and` related filter traversals. 286 | */ 287 | def and(andTraversals: (Steps[EndDomain, EndGraph, HNil] => Steps[_, _, _])*) 288 | : Steps[EndDomain, EndGraph, Labels] = { 289 | val rawAndTraversals = andTraversals.map { 290 | andTraversal => (rawTraversal: GremlinScala[EndGraph]) => 291 | andTraversal( 292 | new Steps[EndDomain, EndGraph, HNil]( 293 | rawTraversal.asInstanceOf[GremlinScala.Aux[EndGraph, HNil]]) 294 | ).raw 295 | } 296 | 297 | new Steps[EndDomain, EndGraph, Labels]( 298 | raw.and(rawAndTraversals: _*) 299 | ) 300 | } 301 | 302 | /** 303 | * Step that orders nodes according to f. 304 | * */ 305 | def orderBy[A](fun: EndDomain => A): Steps[EndDomain, EndGraph, Labels] = 306 | new Steps[EndDomain, EndGraph, Labels](raw.order(By { v: EndGraph => 307 | fun(converter.toDomain(v)) 308 | })) 309 | 310 | override def toString = s"${getClass.getSimpleName}($raw)" 311 | } 312 | -------------------------------------------------------------------------------- /gremlin-scala/src/main/scala/gremlin/scala/package.scala: -------------------------------------------------------------------------------- 1 | package gremlin 2 | 3 | import java.util.function.{ 4 | BiConsumer, 5 | BiPredicate, 6 | BiFunction, 7 | BinaryOperator, 8 | Consumer, 9 | Function => JFunction, 10 | Predicate => JPredicate, 11 | Supplier, 12 | UnaryOperator 13 | } 14 | import org.apache.tinkerpop.gremlin.process.traversal 15 | import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal 16 | import org.apache.tinkerpop.gremlin.structure 17 | import shapeless._ 18 | import shapeless.ops.hlist.IsHCons 19 | import shapeless.ops.hlist.{IsHCons, ToTraversable} 20 | import shapeless.ops.product.ToHList 21 | import shapeless.syntax.std.product.productOps 22 | import _root_.scala.language.implicitConversions 23 | 24 | package object scala { 25 | type Vertex = structure.Vertex 26 | type Edge = structure.Edge 27 | type Element = structure.Element 28 | type Graph = structure.Graph 29 | type Property[A] = structure.Property[A] 30 | type Traverser[A] = traversal.Traverser[A] 31 | type Label = String 32 | type P[A] = traversal.P[A] 33 | 34 | implicit class GraphAsScala[G <: Graph](g: G) { 35 | def asScala() = ScalaGraph(g) 36 | } 37 | 38 | implicit class GraphAsJava(g: ScalaGraph) { 39 | def asJava() = g.graph 40 | } 41 | 42 | implicit class EdgeAsScala(e: Edge) { 43 | def asScala() = ScalaEdge(e) 44 | } 45 | 46 | implicit class EdgeAsJava(e: ScalaEdge) { 47 | def asJava() = e.edge 48 | } 49 | 50 | implicit class VertexAsScala(e: Vertex) { 51 | def asScala() = ScalaVertex(e) 52 | } 53 | 54 | implicit class VertexAsJava(v: ScalaVertex) { 55 | def asJava() = v.vertex 56 | } 57 | 58 | implicit class PropertyOps[A](property: Property[A]) { 59 | def toOption: Option[A] = 60 | if (property.isPresent) Some(property.value) 61 | else None 62 | } 63 | 64 | // to create a new anonymous traversal, e.g. `__.outE` 65 | def __[A](): GremlinScala.Aux[A, HNil] = 66 | GremlinScala[A, HNil](org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.start[A]()) 67 | 68 | def __[A](starts: A*): GremlinScala.Aux[A, HNil] = 69 | GremlinScala[A, HNil]( 70 | org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__ 71 | .__[A](starts: _*)) 72 | 73 | implicit def asScalaVertex(v: Vertex): ScalaVertex = ScalaVertex(v) 74 | 75 | implicit def asScalaEdge(e: Edge): ScalaEdge = ScalaEdge(e) 76 | 77 | implicit def asScalaGraph(g: Graph): ScalaGraph = ScalaGraph(g) 78 | 79 | implicit def asGremlinScala[A](traversal: GraphTraversal[_, A]): GremlinScala.Aux[A, HNil] = 80 | GremlinScala[A, HNil](traversal) 81 | 82 | implicit def toSupplier[A](f: () => A): Supplier[A] = new Supplier[A] { 83 | override def get(): A = f() 84 | } 85 | 86 | implicit def toConsumer[A](f: A => Unit): Consumer[A] = new Consumer[A] { 87 | override def accept(a: A): Unit = f(a) 88 | } 89 | 90 | implicit def toJavaFunction[A, B](f: A => B): JFunction[A, B] = 91 | new JFunction[A, B] { 92 | override def apply(a: A): B = f(a) 93 | } 94 | 95 | implicit def toJavaUnaryOperator[A](f: A => A): UnaryOperator[A] = 96 | new UnaryOperator[A] { 97 | override def apply(a: A): A = f(a) 98 | } 99 | 100 | implicit def toJavaBinaryOperator[A](f: (A, A) => A): BinaryOperator[A] = 101 | new BinaryOperator[A] { 102 | override def apply(a1: A, a2: A): A = f(a1, a2) 103 | } 104 | 105 | implicit def toJavaBiFunction[A, B, C](f: (A, B) => C): BiFunction[A, B, C] = 106 | new BiFunction[A, B, C] { 107 | override def apply(a: A, b: B): C = f(a, b) 108 | } 109 | 110 | implicit def toJavaBiConsumer[A, B](f: (A, B) => Unit): BiConsumer[A, B] = 111 | new BiConsumer[A, B] { 112 | override def accept(a: A, b: B): Unit = f(a, b) 113 | } 114 | 115 | implicit def toJavaPredicate[A](f: A => Boolean): JPredicate[A] = 116 | new JPredicate[A] { 117 | override def test(a: A): Boolean = f(a) 118 | } 119 | 120 | implicit def toJavaBiPredicate[A, B](predicate: (A, B) => Boolean): BiPredicate[A, B] = 121 | new BiPredicate[A, B] { 122 | def test(a: A, b: B) = predicate(a, b) 123 | } 124 | 125 | implicit def liftTraverser[A, B](fun: A => B): Traverser[A] => B = 126 | (t: Traverser[A]) => fun(t.get) 127 | 128 | // Marshalling implicits 129 | implicit class GremlinScalaVertexFunctions(val gs: GremlinScala[Vertex]) { 130 | 131 | /** 132 | * Load a vertex values into a case class 133 | */ 134 | def toCC[CC <: Product: Marshallable] = gs.map(_.toCC[CC]) 135 | } 136 | 137 | implicit class GremlinScalaEdgeFunctions(val gs: GremlinScala[Edge]) { 138 | 139 | /** 140 | * Load a edge values into a case class 141 | */ 142 | def toCC[CC <: Product: Marshallable] = gs.map(_.toCC[CC]) 143 | } 144 | 145 | // Arrow syntax implicits 146 | implicit class SemiEdgeFunctions(label: Label) { 147 | def ---(from: Vertex) = SemiEdge(from, label) 148 | 149 | def -->(right: Vertex) = SemiDoubleEdge(right, label) 150 | } 151 | 152 | implicit class SemiEdgeProductFunctions[ 153 | LabelAndValuesAsTuple <: Product, 154 | LabelAndValues <: HList, 155 | Lbl <: String, 156 | KeyValues <: HList 157 | ](labelAndValuesAsTuple: LabelAndValuesAsTuple)( 158 | implicit toHList: ToHList.Aux[LabelAndValuesAsTuple, LabelAndValues], 159 | startsWithLabel: IsHCons.Aux[LabelAndValues, Lbl, KeyValues], // first element has to be a Label 160 | keyValueToList: ToTraversable.Aux[KeyValues, List, KeyValue[_]] // all other elements have to be KeyValue[_] 161 | ) { 162 | lazy val labelAndValues = labelAndValuesAsTuple.toHList 163 | lazy val label: String = labelAndValues.head 164 | lazy val keyValues: KeyValues = labelAndValues.tail 165 | lazy val properties: List[KeyValue[_]] = keyValues.toList 166 | 167 | def ---(from: Vertex) = SemiEdge(from, label, properties: _*) 168 | def -->(right: Vertex) = SemiDoubleEdge(right, label, properties: _*) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /gremlin-scala/src/test/resources/dummy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpollmeier/gremlin-scala/b68ca4e1cc6b050c0e98966af8c08c17ad984156/gremlin-scala/src/test/resources/dummy -------------------------------------------------------------------------------- /gremlin-scala/src/test/scala/gremlin/scala/.gitignore: -------------------------------------------------------------------------------- 1 | /Playground.scala 2 | -------------------------------------------------------------------------------- /gremlin-scala/src/test/scala/gremlin/scala/AlgorithmSpec.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph 4 | import org.scalatest.wordspec.AnyWordSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | /* demo common algorithms */ 8 | class AlgorithmSpec extends AnyWordSpec with Matchers { 9 | 10 | "directed acyclic graphs".can { 11 | "be detected" in { 12 | // to start with, there's no cycles 13 | implicit val graph = TinkerGraph.open.asScala() 14 | val vA = graph + "a" 15 | val vB = graph + "b" 16 | val vC = graph + "c" 17 | val vD = graph + "d" 18 | vA --- "next" --> vB 19 | vA --- "next" --> vD 20 | vC --- "next" --> vA 21 | vC --- "next" --> vD 22 | 23 | isCyclic(graph) shouldBe false 24 | 25 | //make it cyclic 26 | vB --- "next" --> vC 27 | isCyclic(graph) shouldBe true 28 | } 29 | 30 | def isCyclic(graph: ScalaGraph): Boolean = { 31 | val paths = graph.V() 32 | .as("a") 33 | .repeat(_.out().simplePath()) 34 | .emit() 35 | .where(_.out().as("a")) 36 | .toList() 37 | 38 | paths.nonEmpty 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /gremlin-scala/src/test/scala/gremlin/scala/ArrowSyntaxSpec.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph 4 | import org.scalatest.wordspec.AnyWordSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class ArrowSyntaxSpec extends AnyWordSpec with Matchers { 8 | 9 | "A --> B creates an edge".which { 10 | 11 | "has a label" in new Fixture { 12 | paris --- Eurostar --> london 13 | 14 | paris.out(Eurostar).head() shouldBe london 15 | } 16 | 17 | "has a label and one property" in new Fixture { 18 | paris --- (Eurostar, Name -> "alpha") --> london 19 | 20 | paris.out(Eurostar).head() shouldBe london 21 | paris.outE(Eurostar).value(Name).head() shouldBe "alpha" 22 | } 23 | 24 | "has a label and multiple properties" in new Fixture { 25 | paris --- (Eurostar, Name -> "alpha", Length -> 100) --> london 26 | 27 | paris.out(Eurostar).head() shouldBe london 28 | paris.outE(Eurostar).value(Name).head() shouldBe "alpha" 29 | paris.outE(Eurostar).value(Length).head() shouldBe 100 30 | } 31 | 32 | "has a label and multiple properties as Map " in new Fixture { 33 | paris --- (Eurostar, properties) --> london 34 | paris.out(Eurostar).head() shouldBe london 35 | paris.outE(Eurostar).value(Name).head() shouldBe "alpha" 36 | paris.outE(Eurostar).value(Length).head() shouldBe 100 37 | } 38 | } 39 | 40 | "A <-- B creates an edge".which { 41 | "has a label" in new Fixture { 42 | paris <-- Eurostar --- london 43 | london.out(Eurostar).head() shouldBe paris 44 | } 45 | 46 | "has a label and one property" in new Fixture { 47 | paris <-- (Eurostar, Name -> "alpha") --- london 48 | paris.in(Eurostar).head() shouldBe london 49 | paris.inE(Eurostar).value(Name).head() shouldBe "alpha" 50 | } 51 | 52 | "has a label and multiple properties" in new Fixture { 53 | paris <-- (Eurostar, Name -> "alpha", Length -> 100) --- london 54 | 55 | paris.in(Eurostar).head() shouldBe london 56 | paris.inE(Eurostar).value(Name).head() shouldBe "alpha" 57 | paris.inE(Eurostar).value(Length).head() shouldBe 100 58 | } 59 | 60 | /* "has a label and multiple properties as Map" in new Fixture { 61 | paris <-- (Eurostar, properties) --- london 62 | 63 | paris.in(Eurostar).head shouldBe london 64 | paris.inE(Eurostar).value(Name).head shouldBe "alpha" 65 | paris.inE(Eurostar).value(Length).head shouldBe 100 66 | }*/ 67 | } 68 | 69 | "A <--> B create edges".which { 70 | "have labels" in new Fixture { 71 | paris <-- Eurostar --> london 72 | 73 | paris.out(Eurostar).head() shouldBe london 74 | london.out(Eurostar).head() shouldBe paris 75 | } 76 | 77 | "have labels and one property" in new Fixture { 78 | paris <-- (Eurostar, Name -> "alpha") --> london 79 | 80 | paris.out(Eurostar).head() shouldBe london 81 | paris.outE(Eurostar).value(Name).head() shouldBe "alpha" 82 | paris.in(Eurostar).head() shouldBe london 83 | paris.inE(Eurostar).value(Name).head() shouldBe "alpha" 84 | } 85 | 86 | "have labels and multiple properties" in new Fixture { 87 | paris <-- (Eurostar, Name -> "alpha", Length -> 100) --> london 88 | 89 | paris.out(Eurostar).head() shouldBe london 90 | paris.outE(Eurostar).value(Name).head() shouldBe "alpha" 91 | paris.outE(Eurostar).value(Length).head() shouldBe 100 92 | paris.in(Eurostar).head() shouldBe london 93 | paris.inE(Eurostar).value(Name).head() shouldBe "alpha" 94 | paris.inE(Eurostar).value(Length).head() shouldBe 100 95 | } 96 | 97 | /* "have labels and multiple properties as Map" in new Fixture { 98 | paris <-- (Eurostar, properties) --> london 99 | 100 | paris.out(Eurostar).head shouldBe london 101 | paris.outE(Eurostar).value(Name).head shouldBe "alpha" 102 | paris.outE(Eurostar).value(Length).head shouldBe 100 103 | paris.in(Eurostar).head shouldBe london 104 | paris.inE(Eurostar).value(Name).head shouldBe "alpha" 105 | paris.inE(Eurostar).value(Length).head shouldBe 100 106 | }*/ 107 | } 108 | 109 | // TODO: case class support 110 | // "adding edge with case class" in { 111 | // val graph = TinkerGraph.open.asScala 112 | 113 | // val paris = graph.addVertex("Paris") 114 | // val london = graph.addVertex("London") 115 | 116 | // val e = paris --- CCWithLabelAndId( 117 | // "some string", 118 | // Int.MaxValue, 119 | // Long.MaxValue, 120 | // Some("option type"), 121 | // Seq("test1", "test2"), 122 | // Map("key1" -> "value1", "key2" -> "value2"), 123 | // NestedClass("nested") 124 | // ) --> london 125 | 126 | // e.inVertex shouldBe london 127 | // e.outVertex shouldBe paris 128 | // } 129 | 130 | // "adding bidirectional edge with case class" in { 131 | // val graph = TinkerGraph.open.asScala 132 | 133 | // val paris = graph.addVertex("Paris") 134 | // val london = graph.addVertex("London") 135 | 136 | // val (e0, e1) = paris <-- CCWithLabel( 137 | // "some string", 138 | // Long.MaxValue, 139 | // Some("option type"), 140 | // Seq("test1", "test2"), 141 | // Map("key1" -> "value1", "key2" -> "value2"), 142 | // NestedClass("nested") 143 | // ) --> london 144 | 145 | // e0.inVertex shouldBe london 146 | // e0.outVertex shouldBe paris 147 | 148 | // e1.inVertex shouldBe paris 149 | // e1.outVertex shouldBe london 150 | // } 151 | 152 | // "adding left edge with case class" in { 153 | // val graph = TinkerGraph.open.asScala 154 | 155 | // val paris = graph.addVertex("Paris") 156 | // val london = graph.addVertex("London") 157 | 158 | // val e = paris <-- CCWithLabelAndId( 159 | // "some string", 160 | // Int.MaxValue, 161 | // Long.MaxValue, 162 | // Some("option type"), 163 | // Seq("test1", "test2"), 164 | // Map("key1" -> "value1", "key2" -> "value2"), 165 | // NestedClass("nested") 166 | // ) --- london 167 | 168 | // e.inVertex shouldBe paris 169 | // e.outVertex shouldBe london 170 | // } 171 | 172 | trait Fixture { 173 | implicit val graph = TinkerGraph.open.asScala() 174 | val paris: Vertex = graph + "Paris" 175 | val london = graph + "London" 176 | 177 | val Eurostar = "eurostar" //edge label 178 | 179 | val Name: Key[String] = Key[String]("name") 180 | val Length: Key[Int] = Key[Int]("length") 181 | 182 | val properties: Map[String, Any] = 183 | List(("name", "alpha"), ("length", 100)).toMap 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /gremlin-scala/src/test/scala/gremlin/scala/ElementSpec.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph 4 | import org.apache.tinkerpop.gremlin.structure.T 5 | import org.apache.tinkerpop.gremlin.structure.VertexProperty.Cardinality 6 | import scala.collection.JavaConverters._ 7 | import TestGraph._ 8 | 9 | // TODO: rewrite using new type safe steps 10 | class ElementSpec extends TestBase { 11 | 12 | describe("properties") { 13 | it("gets properties") { 14 | v1.keys shouldBe Set(Key("name"), Key("age")) 15 | v1.property(Name).value shouldBe "marko" 16 | v1.property(DoesNotExist).isPresent shouldBe false 17 | v1.valueMap shouldBe Map("name" -> "marko", "age" -> 29) 18 | v1.valueMap("name", "age") shouldBe Map("name" -> "marko", "age" -> 29) 19 | v1.properties("name", "age").length shouldBe 2 20 | v1.properties.length shouldBe 2 21 | 22 | e7.keys shouldBe Set(Key("weight")) 23 | e7.property(Weight).value shouldBe 0.5 24 | e7.property(DoesNotExist).isPresent shouldBe false 25 | e7.valueMap("weight") shouldBe Map("weight" -> 0.5) 26 | 27 | graph.V(1).properties(Age.name, Name.name).hasValue("marko").count().head() shouldBe 1 28 | graph.V(1).properties(Age.name, Name.name).hasValue(29).count().head() shouldBe 1 29 | graph.V(1).properties(Age.name, Name.name).hasValue(29, "marko").count().head() shouldBe 2 30 | graph 31 | .V(1) 32 | .properties(Age.name, Name.name) 33 | .hasValue("marko", 29: Integer) 34 | .count() 35 | .head() shouldBe 2 36 | } 37 | 38 | it("maps properties to scala.Option") { 39 | v1.property(Name).toOption should be(Some("marko")) 40 | e7.property(Weight).toOption shouldBe Some(0.5) 41 | } 42 | 43 | it("sets a property") { 44 | v1.setProperty(TestProperty, "updated") 45 | v1.property(TestProperty).value shouldBe "updated" 46 | 47 | e7.setProperty(TestProperty, "updated") 48 | e7.property(TestProperty).value shouldBe "updated" 49 | } 50 | 51 | /** adapted from http://tinkerpop.apache.org/docs/current/reference/#vertex-properties 52 | * TODO: `properties` should take `Key` as well 53 | */ 54 | it("sets a property with multiple values") { 55 | // tp3 standard way 56 | val v = graph.addVertex((Name.name -> "marko"), (Name.name -> "marko a. rodriguez")) 57 | graph.V(v).properties(Name.name).count().head() shouldBe 2 58 | 59 | // add one more 60 | v.property(Cardinality.list, Name.name, "m. a. rodriguez") 61 | graph.V(v).properties(Name.name).count().head() shouldBe 3 62 | 63 | // can filter on property values as well 64 | graph.V(v).properties(Name.name).hasValue("marko").count().head() shouldBe 1 65 | 66 | // gremlin-scala style 67 | v.setPropertyList(TestProperty, List("one", "two", "three")) 68 | graph.V(v).properties(TestProperty.name).count().head() shouldBe 3 69 | 70 | // can override 71 | v.setPropertyList(TestProperty, List("three", "four")) 72 | graph.V(v).properties(TestProperty.name).count().head() shouldBe 2 73 | } 74 | 75 | it("removes a property") { 76 | v1.setProperty(TestProperty, "updated") 77 | v1.removeProperty(TestProperty) 78 | v1.property(TestProperty).isPresent shouldBe false 79 | 80 | e7.setProperty(TestProperty, "updated") 81 | e7.removeProperty(TestProperty) 82 | e7.property(TestProperty).isPresent shouldBe false 83 | } 84 | } 85 | 86 | describe("values") { 87 | it("gets a value") { 88 | v1.value2(Name) shouldBe "marko" 89 | e7.value2(Weight) shouldBe 0.5 90 | } 91 | 92 | it("gets an optional value") { 93 | v1.valueOption(Name) shouldBe Some("marko") 94 | v1.valueOption(DoesNotExist) shouldBe None 95 | } 96 | 97 | it("sets an optional value") { 98 | v1.valueOption(TestProperty, None) 99 | v1.property(TestProperty).isPresent shouldBe false 100 | v1.valueOption(TestProperty, Some("test")) 101 | v1.property(TestProperty).value shouldBe "test" 102 | 103 | e7.valueOption(TestProperty, None) 104 | e7.property(TestProperty).isPresent shouldBe false 105 | e7.valueOption(TestProperty, Some("test")) 106 | e7.property(TestProperty).value shouldBe "test" 107 | } 108 | 109 | it("throws an exception if it doesn't exist") { 110 | intercept[IllegalStateException] { v1.value2(DoesNotExist) } 111 | } 112 | } 113 | 114 | describe("id, equality and hashCode") { 115 | it("has an id") { 116 | v1.id shouldBe 1 117 | e7.id shouldBe 7 118 | } 119 | 120 | it("equals") { 121 | v1 == v(1).asScala() shouldBe true 122 | v1 == v(2).asScala() shouldBe false 123 | } 124 | 125 | it("uses the right hashCodes") { 126 | v1.hashCode shouldBe v(1).asScala().hashCode 127 | v1.hashCode should not be v(2).asScala().hashCode 128 | 129 | Set(v1) contains v(1) shouldBe true 130 | Set(v1) contains v(2) shouldBe false 131 | } 132 | } 133 | 134 | describe("adding and removing elements") { 135 | 136 | it("adds a vertex") { 137 | val graph = TinkerGraph.open.asScala() 138 | val v1 = graph.addVertex() 139 | val v2 = graph.addVertex() 140 | v2.setProperty(TestProperty, "testValue") 141 | 142 | graph.V(v1.id).head() shouldBe v1 143 | graph.V(v2.id).head().property(TestProperty).value shouldBe "testValue" 144 | (graph.V().toList() should have).size(2) 145 | } 146 | 147 | it("adds a vertex with a given label") { 148 | val graph = TinkerGraph.open.asScala() 149 | val label1 = "label1" 150 | val label2 = "label2" 151 | val v1 = graph.addVertex(label1) 152 | val v2 = graph.addVertex(label2, Map(TestProperty.name -> "testValue")) 153 | 154 | graph.V().has(T.label, label1).head() shouldBe v1.vertex 155 | graph.V().has(T.label, label2).head() shouldBe v2.vertex 156 | graph.V().has(T.label, label2).value(TestProperty).head() shouldBe "testValue" 157 | } 158 | 159 | it("adds a vertex with a given label with syntactic sugar") { 160 | val graph = TinkerGraph.open.asScala() 161 | val label1 = "label1" 162 | val label2 = "label2" 163 | 164 | val v1 = graph + label1 165 | val v2 = graph + (label2, TestProperty -> "testValue") 166 | 167 | graph.V().hasLabel(label1).head() shouldBe v1.vertex 168 | graph.V().hasLabel(label2).head() shouldBe v2.vertex 169 | graph.V().hasLabel(label2).value(TestProperty).head() shouldBe "testValue" 170 | 171 | graph.asJava().close() 172 | } 173 | 174 | it("adds an edge") { 175 | val graph = TinkerGraph.open.asScala() 176 | val v1 = graph.addVertex() 177 | val v2 = graph.addVertex() 178 | 179 | val e = v1.addEdge("testLabel", v2) 180 | e.label shouldBe "testLabel" 181 | v1.outE().head() shouldBe e.edge 182 | v1.out("testLabel").head() shouldBe v2.vertex 183 | } 184 | 185 | it("adds an edge with additional properties") { 186 | implicit val graph = TinkerGraph.open.asScala() 187 | val v1 = graph.addVertex() 188 | val v2 = graph.addVertex() 189 | 190 | val e = v1.asScala().addEdge("testLabel", v2, TestProperty -> "testValue") 191 | e.label shouldBe "testLabel" 192 | e.value2(TestProperty) shouldBe "testValue" 193 | e.valueMap(TestProperty.name) shouldBe Map(TestProperty.name -> "testValue") 194 | v1.outE().head() shouldBe e.edge 195 | v1.out("testLabel").head() shouldBe v2.vertex 196 | } 197 | 198 | it("removes elements") { 199 | val graph = TinkerGraph.open.asScala() 200 | val v = graph.addVertex() 201 | v.remove() 202 | graph.V().toList() shouldBe empty 203 | } 204 | } 205 | 206 | def v1 = v(1).asScala() 207 | def e7 = e(7).asScala() 208 | val TestProperty = Key[String]("testProperty") 209 | } 210 | -------------------------------------------------------------------------------- /gremlin-scala/src/test/scala/gremlin/scala/FilterSpec.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.tinkerpop.gremlin.process.traversal.TextP 4 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerFactory 5 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph 6 | import TestGraph._ 7 | import org.scalatest.wordspec.AnyWordSpec 8 | import org.scalatest.matchers.should.Matchers 9 | 10 | class FilterSpec extends AnyWordSpec with Matchers { 11 | 12 | "filter" in new Fixture { 13 | graph 14 | .V() 15 | .filter(_.value(Age).is(P.gt(30))) 16 | .value(Name) 17 | .toSet() should be(Set("josh", "peter")) 18 | } 19 | 20 | "filterNot" in new Fixture { 21 | graph 22 | .V() 23 | .filterNot(_.value(Age).is(P.gt(30))) 24 | .value(Name) 25 | .toSet() should be(Set("lop", "marko", "vadas", "ripple")) 26 | } 27 | 28 | "filter on end type" in new Fixture { 29 | graph 30 | .V() 31 | .filterOnEnd(_.property(Age).orElse(0) > 30) 32 | .value(Name) 33 | .toSet() should be(Set("josh", "peter")) 34 | } 35 | 36 | "filter with TextP - e.g. contains, endsWith etc" in new Fixture { 37 | graph 38 | .V() 39 | .has(Name, TextP.containing("os")) 40 | .value(Name) 41 | .toSet() should be(Set("josh")) 42 | } 43 | 44 | "has" in new Fixture { 45 | graph.V().has(Age, 35).value(Name).toSet() shouldBe Set("peter") 46 | } 47 | 48 | "has - sugar" in new Fixture { 49 | implicit val g = TinkerGraph.open.asScala() 50 | g + ("software", Name -> "blueprints", Created -> 2010) 51 | 52 | g.V() 53 | .has(Name -> "blueprints") 54 | .head() <-- "dependsOn" --- (g + ("software", Name -> "gremlin", Created -> 2009)) 55 | g.V() 56 | .has(Name -> "gremlin") 57 | .head() <-- "dependsOn" --- (g + ("software", Name -> "gremlinScala")) 58 | g.V() 59 | .has(Name -> "gremlinScala") 60 | .head() <-- "createdBy" --- (g + ("person", Name -> "mpollmeier")) 61 | 62 | g.V().toList().size shouldBe 4 63 | g.V().hasLabel("software").toList().size shouldBe 3 64 | g.V().hasLabel("person").toList().size shouldBe 1 65 | 66 | g.E().toList().size shouldBe 3 67 | g.E().hasLabel("dependsOn").toList().size shouldBe 2 68 | g.E().hasLabel("createdBy").toList().size shouldBe 1 69 | 70 | g.asJava().close() 71 | } 72 | 73 | "hasNot" in new Fixture { 74 | graph.V().hasNot(Age, 35).value(Name).toSet() shouldBe Set( 75 | "lop", 76 | "marko", 77 | "josh", 78 | "vadas", 79 | "ripple" 80 | ) 81 | } 82 | 83 | "coin all" in new Fixture { 84 | graph.V().coin(1.0d).value(Name).toSet() shouldBe Set( 85 | "lop", 86 | "marko", 87 | "josh", 88 | "vadas", 89 | "ripple", 90 | "peter" 91 | ) 92 | } 93 | 94 | "coin nothing" in new Fixture { 95 | graph.V().coin(0.0d).value(Name).toSet() shouldBe Set() 96 | } 97 | 98 | "dedup success" in new Fixture { 99 | val a = StepLabel[Edge]() 100 | val b = StepLabel[Vertex]() 101 | 102 | graph 103 | .V() 104 | .outE() 105 | .as(a) 106 | .inV() 107 | .as(b) 108 | .select(a) 109 | .select(b) 110 | .order(By(Name)) 111 | .value(Name) 112 | .dedup() 113 | .toList() shouldBe List("josh", "lop", "ripple", "vadas") 114 | } 115 | 116 | "drop success" in new Fixture { 117 | graph.V().outE().drop().toSet() shouldBe Set() 118 | } 119 | 120 | "is usage" in new Fixture { 121 | graph.V().value(Age).is(P.lte(30)).toSet() shouldBe Set(27, 29) 122 | } 123 | 124 | "range success" in new Fixture { 125 | val markoVertexId = 1 126 | (graph 127 | .V(markoVertexId) 128 | .out("knows") 129 | .out("created") 130 | .range(0, 1) 131 | .value(Name) 132 | .toSet() should contain).oneOf("lop", "ripple") 133 | } 134 | 135 | "simple path" in new Fixture { 136 | val markoVertexId = 1 137 | graph 138 | .V(markoVertexId) 139 | .out("created") 140 | .in("created") 141 | .simplePath() 142 | .value(Name) 143 | .toSet() shouldBe Set( 144 | "josh", 145 | "peter" 146 | ) 147 | } 148 | 149 | "tail" in new Fixture { 150 | graph.V().value(Name).order().tail(2).toSet() shouldBe Set("ripple", "vadas") 151 | } 152 | 153 | "where" in new Fixture { 154 | val a = StepLabel[Vertex]() 155 | graph 156 | .V() 157 | .as(a) 158 | .out("created") 159 | .where(_.as(a).value(Name).is("josh")) 160 | .in("created") 161 | .value(Name) 162 | .toSet() shouldBe Set("marko", "josh", "peter") 163 | } 164 | 165 | trait Fixture { 166 | val graph = TinkerFactory.createClassic.asScala() 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /gremlin-scala/src/test/scala/gremlin/scala/GraphHelperSpec.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph 4 | import org.scalatest.wordspec.AnyWordSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class GraphHelperSpec extends AnyWordSpec with Matchers { 8 | 9 | "deep clone graph" in { 10 | val original = TinkerGraph.open.asScala() 11 | val testProperty = Key[String]("testProperty") 12 | 13 | { 14 | implicit val graph: ScalaGraph = original 15 | val marko = original + "marko" 16 | val stephen = original + "stephen" 17 | marko --- "knows" --> stephen 18 | } 19 | 20 | { 21 | implicit val clone: ScalaGraph = GraphHelper.cloneElements(original, TinkerGraph.open.asScala()) 22 | val stephen = clone.V().hasLabel("stephen").head() 23 | val michael = clone + "michael" 24 | michael --- "knows" --> stephen 25 | clone.V().property(testProperty, "someValue").iterate() 26 | 27 | // original graph should be unchanged 28 | original.V().count().head() shouldBe 2 29 | original.E().count().head() shouldBe 1 30 | original.V().has(testProperty).count().head() shouldBe 0 31 | 32 | // cloned graph should contain old and new elements and properties 33 | clone.V().count().head() shouldBe 3 34 | clone.E().count().head() shouldBe 2 35 | clone.V().has(testProperty).count().head() shouldBe 3 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /gremlin-scala/src/test/scala/gremlin/scala/GraphSerialisationSpec.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import java.io.FileOutputStream 4 | import org.apache.tinkerpop.gremlin.structure.io.IoCore 5 | import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONVersion 6 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.{TinkerFactory, TinkerGraph} 7 | import org.scalatest.wordspec.AnyWordSpec 8 | import org.scalatest.matchers.should.Matchers 9 | 10 | class GraphSerialisationSpec extends AnyWordSpec with Matchers { 11 | 12 | "serialising from/to" should { 13 | 14 | "support graphML" in { 15 | val file = "target/tinkerpop-modern.graphml" 16 | graph.io(IoCore.graphml).writeGraph(file) 17 | 18 | val newGraph = TinkerGraph.open 19 | newGraph.io(IoCore.graphml).readGraph(file) 20 | newGraph.V().count().head() shouldBe 6 21 | newGraph.E().count().head() shouldBe 6 22 | } 23 | 24 | "support graphson" in { 25 | val file = "target/tinkerpop-modern.graphson.json" 26 | graph.io(IoCore.graphson).writeGraph(file) 27 | 28 | val newGraph = TinkerGraph.open 29 | newGraph.io(IoCore.graphson).readGraph(file) 30 | newGraph.V().count().head() shouldBe 6 31 | newGraph.E().count().head() shouldBe 6 32 | } 33 | 34 | "support graphson v2" in { 35 | val file = "target/tinkerpop-modern.graphson2.json" 36 | val mapper = graph 37 | .io(IoCore.graphson) 38 | .mapper 39 | .normalize(true) 40 | .version(GraphSONVersion.V2_0) 41 | .create 42 | graph 43 | .io(IoCore.graphson) 44 | .writer 45 | .mapper(mapper) 46 | .create 47 | .writeGraph(new FileOutputStream(file), graph) 48 | 49 | val newGraph = TinkerGraph.open 50 | newGraph.io(IoCore.graphson).readGraph(file) 51 | newGraph.V().count().head() shouldBe 6 52 | newGraph.E().count().head() shouldBe 6 53 | } 54 | 55 | "support gryo/kryo" in { 56 | val file = "target/tinkerpop-modern.gryo" 57 | graph.io(IoCore.gryo).writeGraph(file) 58 | 59 | val newGraph = TinkerGraph.open 60 | newGraph.io(IoCore.gryo).readGraph(file) 61 | newGraph.V().count().head() shouldBe 6 62 | newGraph.E().count().head() shouldBe 6 63 | } 64 | } 65 | 66 | def graph = TinkerFactory.createModern 67 | } 68 | -------------------------------------------------------------------------------- /gremlin-scala/src/test/scala/gremlin/scala/LogicalSpec.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph 4 | import org.scalatest.wordspec.AnyWordSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class LogicalSpec extends AnyWordSpec with Matchers { 8 | 9 | "choose" should { 10 | "provide simple version for if/else semantic" in new Fixture { 11 | graph.V() 12 | .choose( 13 | _.value(Age).is(P.gt(30)), 14 | onTrue = _.value(Height), 15 | onFalse = _.value(Shoesize) 16 | ) 17 | .toSet() shouldBe Set(190, 18 | 176, // Michael and Steffi are >30 - take their height 19 | 5) // Karlotta is <=30 - take her shoesize 20 | } 21 | 22 | "provide if/elseif/else semantic" in new Fixture { 23 | // note: this is only if/elseif/else semantic because the PickToken is enforced to be unique 24 | graph.V() 25 | .choose( 26 | on = _.value(Age), 27 | BranchCase(34, _.value(Height)), 28 | BranchCase(32, _.value(Shoesize)), 29 | BranchOtherwise(_.value(YearOfBirth)) 30 | ) 31 | .toSet() shouldBe Set(190, // Michael is 34 - take his height 32 | 41, //Steffi is 32 - take her shoesize 33 | 2015) // Karlotta is case `Otherwise` - take her year of birth 34 | } 35 | } 36 | 37 | "coalesce provides if/elseif/else semantics" in new Fixture { 38 | graph.V() 39 | .value(Age) 40 | .coalesce( 41 | _.is(P.lt(31)).constant("young"), 42 | _.is(P.lt(34)).constant("old"), 43 | _.constant("very old") 44 | ) 45 | .toList() shouldBe List("very old", "old", "young") 46 | } 47 | 48 | "branch" should { 49 | "execute all cases that match" in new Fixture { 50 | graph.V() 51 | .branch( 52 | on = _.value(Age), 53 | BranchCase(34, _.value(Height)), 54 | BranchCase(32, _.value(Shoesize)), 55 | BranchCase(1, _.value(YearOfBirth)) 56 | ) 57 | .toSet() shouldBe Set(190, // Michael is 34 - take his height 58 | 41, //Steffi is 32 - take her shoesize 59 | 2015) // Karlotta is 1 - take her year of birth 60 | } 61 | 62 | "allow to use `matchAll` semantics" in new Fixture { 63 | graph.V() 64 | .branch( 65 | on = _.value(Age), 66 | BranchCase(34, _.value(Height)), 67 | BranchMatchAll(_.value(YearOfBirth)) 68 | ) 69 | .toSet() shouldBe Set(190, 70 | 1983, // Michael's height (since he is 34) and year of birth 71 | 1984, //Steffi's year of birth 72 | 2015) // Karlotta's year of birth 73 | } 74 | } 75 | 76 | "and step returns results if all conditions are met" in new Fixture { 77 | graph.V() 78 | .and( 79 | _.label().is(Person), 80 | _.out().has(Name -> "Karlotta") 81 | ) 82 | .value(Name) 83 | .toSet() shouldBe Set("Michael", "Steffi") 84 | } 85 | 86 | "or step returns results if at least one condition is met" in new Fixture { 87 | graph.V() 88 | .or( 89 | _.label().is("does not exist"), 90 | _.has(Age -> 34) 91 | ) 92 | .value(Name) 93 | .toSet() shouldBe Set("Michael") 94 | } 95 | 96 | "exists" should { 97 | "return true if one or more elements found" in new Fixture { 98 | graph.V().exists() shouldBe true 99 | } 100 | 101 | "return false if no elements found" in new Fixture { 102 | graph.V().filter(_.has(Key("nonExistingProperty"))).exists() shouldBe false 103 | } 104 | } 105 | 106 | trait Fixture { 107 | implicit val graph = TinkerGraph.open.asScala() 108 | 109 | val Person = "person" 110 | val Name = Key[String]("name") 111 | val Age = Key[Int]("age") 112 | val Height = Key[Int]("height") 113 | val Shoesize = Key[Int]("shoesize") 114 | val YearOfBirth = Key[Int]("yearOfBirth") 115 | val StreetNumber = Key[Int]("streetNumber") 116 | val parentOf = "parentOf" 117 | val marriedTo = "marriedTo" 118 | 119 | val michael = graph + (Person, Name -> "Michael", Age -> 34, Height -> 190, Shoesize -> 44, YearOfBirth -> 1983, StreetNumber -> 3) 120 | val steffi = graph + (Person, Name -> "Steffi", Age -> 32, Height -> 176, Shoesize -> 41, YearOfBirth -> 1984, StreetNumber -> 3) 121 | val karlotta = graph + (Person, Name -> "Karlotta", Age -> 1, Height -> 90, Shoesize -> 5, YearOfBirth -> 2015, StreetNumber -> 3) 122 | michael <-- marriedTo --> steffi 123 | michael --- parentOf --> karlotta 124 | steffi --- parentOf --> karlotta 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /gremlin-scala/src/test/scala/gremlin/scala/MonadSpec.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph 4 | import org.scalatest.wordspec.AnyWordSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class MonadSpec extends AnyWordSpec with Matchers { 8 | 9 | "obeys the monad laws" in { 10 | // based on examples in http://devth.com/2015/monad-laws-in-scala/ 11 | 12 | val graph = TinkerGraph.open.asScala() 13 | 14 | def f(x: Int): GremlinScala[Int] = if (x < 10) __[Int]() else __(x * 2) 15 | def g(x: Int): GremlinScala[Int] = if (x > 50) __(x + 1) else __[Int]() 16 | 17 | withClue("left identity") { 18 | val a = 30 19 | val lhs = __(a).flatMap(f).head() 20 | val rhs = f(a).head() 21 | lhs shouldBe 60 22 | lhs shouldBe rhs 23 | } 24 | 25 | withClue("right identity") { 26 | def m = __(30) 27 | val lhs = m.flatMap { x: Int => 28 | __(x) 29 | }.head() 30 | lhs shouldBe 30 31 | val rhs = m.head() 32 | lhs shouldBe rhs 33 | } 34 | 35 | withClue("associativity") { 36 | def m = __(30) 37 | val lhs = m.flatMap(f).flatMap(g).head() 38 | lhs shouldBe 61 39 | 40 | val rhs = m.flatMap(x => f(x).flatMap(g)).head() 41 | rhs shouldBe 61 42 | 43 | lhs shouldBe rhs 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /gremlin-scala/src/test/scala/gremlin/scala/PSpec.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.tinkerpop.gremlin.process.traversal.{P => JavaP} 4 | import org.scalatest.wordspec.AnyWordSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | /* test cases copied from TP3 PTest.java */ 8 | class PSpec extends AnyWordSpec with Matchers { 9 | 10 | List( 11 | TestCase(P.eq(0), 0, true), 12 | TestCase(P.eq(0), 1, false), 13 | TestCase(P.neq(0), 0, false), 14 | TestCase(P.neq(0), 1, true), 15 | TestCase(P.gt(0), -1, false), 16 | TestCase(P.gt(0), 0, false), 17 | TestCase(P.gt(0), 1, true), 18 | TestCase(P.lt(0), -1, true), 19 | TestCase(P.lt(0), 0, false), 20 | TestCase(P.lt(0), 1, false), 21 | TestCase(P.gte(0), -1, false), 22 | TestCase(P.gte(0), 0, true), 23 | TestCase(P.gte(0), 1, true), 24 | TestCase(P.lte(0), -1, true), 25 | TestCase(P.lte(0), 0, true), 26 | TestCase(P.lte(0), 1, false), 27 | TestCase(P.between(1, 10), 0, false), 28 | TestCase(P.between(1, 10), 1, true), 29 | TestCase(P.between(1, 10), 9, true), 30 | TestCase(P.between(1, 10), 10, false), 31 | TestCase(P.inside(1, 10), 0, false), 32 | TestCase(P.inside(1, 10), 1, false), 33 | TestCase(P.inside(1, 10), 9, true), 34 | TestCase(P.inside(1, 10), 10, false), 35 | TestCase(P.outside(1, 10), 0, true), 36 | TestCase(P.outside(1, 10), 1, false), 37 | TestCase(P.outside(1, 10), 9, false), 38 | TestCase(P.outside(1, 10), 10, false), 39 | TestCase(P.within(Set(1, 2, 3)), 0, false), 40 | TestCase(P.within(Set(1, 2, 3)), 1, true), 41 | TestCase(P.within(Set(1, 2, 3)), 10, false), 42 | TestCase(P.without(Set(1, 2, 3)), 0, true), 43 | TestCase(P.without(Set(1, 2, 3)), 1, false), 44 | TestCase(P.without(Set(1, 2, 3)), 10, true) 45 | ).map { testCase => 46 | s"$testCase" in { 47 | testCase.predicate.test(testCase.value) shouldBe testCase.expected 48 | } 49 | } 50 | 51 | List( 52 | TestCase(P.between("m", "n").and(P.neq("marko")), "marko", false), 53 | TestCase(P.between("m", "n").and(P.neq("marko")), "matthias", true), 54 | TestCase(P.between("m", "n").or(P.eq("daniel")), "marko", true), 55 | TestCase(P.between("m", "n").or(P.eq("daniel")), "daniel", true), 56 | TestCase(P.between("m", "n").or(P.eq("daniel")), "stephen", false) 57 | ).map { testCase => 58 | s"$testCase" in { 59 | testCase.predicate.test(testCase.value) shouldBe testCase.expected 60 | } 61 | } 62 | 63 | case class TestCase[A](predicate: JavaP[A], value: A, expected: Boolean) 64 | } 65 | -------------------------------------------------------------------------------- /gremlin-scala/src/test/scala/gremlin/scala/ProjectSpec.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerFactory 4 | import org.scalatest.wordspec.AnyWordSpec 5 | import org.scalatest.matchers.should.Matchers 6 | import shapeless.syntax.std.tuple._ 7 | 8 | class ProjectSpec extends AnyWordSpec with Matchers { 9 | 10 | "projecting by two traversals" in { 11 | val result: (java.lang.Long, java.lang.Long) = 12 | graph.V() 13 | .has(name.of("marko")) 14 | .project(_(By(__().outE().count())).and(By(__().inE().count()))) 15 | .head() 16 | 17 | result shouldBe (3, 0) 18 | } 19 | 20 | "projecting by property and traversal" in { 21 | val result: List[(String, java.lang.Long)] = 22 | graph.V() 23 | .out("created") 24 | .project(_(By(name)).and(By(__().in("created").count()))) 25 | .toList() 26 | 27 | result shouldBe List( 28 | ("lop", 3), 29 | ("lop", 3), 30 | ("lop", 3), 31 | ("ripple", 1) 32 | ) 33 | } 34 | 35 | def graph: ScalaGraph = TinkerFactory.createModern.asScala() 36 | val name = Key[String]("name") 37 | 38 | } 39 | -------------------------------------------------------------------------------- /gremlin-scala/src/test/scala/gremlin/scala/SchemaSpec.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph 4 | import org.scalatest.wordspec.AnyWordSpec 5 | import org.scalatest.matchers.should.Matchers 6 | import shapeless.test.illTyped 7 | 8 | class SchemaSpec extends AnyWordSpec with Matchers { 9 | 10 | "a schema with defined Keys".can { 11 | val Software = "software" 12 | val Person = "person" 13 | val Paris = "Paris" 14 | val London = "London" 15 | val EuroStar = "eurostar" 16 | object Name extends Key[String]("name") 17 | object Created extends Key[Int]("created") 18 | object Type extends Key[String]("type") 19 | object Weight extends Key[Int]("weight") 20 | 21 | "create vertices" in { 22 | implicit val graph = TinkerGraph.open.asScala() 23 | 24 | val v0 = graph + (Software, Name -> "blueprints", Created -> 2010) 25 | val v1 = graph + (Software, Created -> 2009, Name -> "gremlin") 26 | val v2 = graph + (Software, Name -> "gremlinScala") 27 | val v3 = graph + (Person, Name -> "mpollmeier") 28 | 29 | graph.V().toList().size shouldBe 4 30 | graph.V().hasLabel(Software).toList().size shouldBe 3 31 | graph.V().hasLabel(Person).toList().size shouldBe 1 32 | 33 | graph.V().has(Name).toList().size shouldBe 4 34 | graph.V().has(Created).toList().size shouldBe 2 35 | 36 | graph.asJava().close() 37 | } 38 | 39 | "read type safe properties" when { 40 | "using `value`" should { 41 | "support vertices" in new Fixture { 42 | val name = paris.value2(Name) 43 | val someString: String = name 44 | illTyped { //to ensure that there is no implicit conversion to make the above work 45 | """ 46 | val i: Integer = paris.value2(Name) 47 | """ 48 | } 49 | } 50 | 51 | "support edges" in new Fixture { 52 | val distance = rail.value2(Distance) 53 | val someInt: Int = distance 54 | illTyped { //to ensure that there is no implicit conversion to make the above work 55 | """ 56 | val i: String = v.value2(Distance) 57 | """ 58 | } 59 | } 60 | 61 | "support traversal" in new Fixture { 62 | val name = paris.out(EuroStar).value(Name).head() 63 | val someString: String = name //no implicit conversion, it already is a String 64 | 65 | val distance = paris.outE(EuroStar).value(Distance).head() 66 | val someInt: Int = distance //no implicit conversion, it already is an Int 67 | } 68 | } 69 | 70 | "using `property`" should { 71 | "support vertices" in new Fixture { 72 | val name = paris.property(Name) 73 | val someString: Property[String] = name 74 | illTyped { //to ensure that there is no implicit conversion to make the above work 75 | """ 76 | val i: Property[Integer] = paris.property(Name) 77 | """ 78 | } 79 | } 80 | 81 | "support edges" in new Fixture { 82 | val distance = rail.property(Distance) 83 | val someInt: Property[Int] = distance //no implicit conversion, it already is an Int 84 | illTyped { //to ensure that there is no implicit conversion to make the above work 85 | """ 86 | val i: Property[String] = v.property(Distance) 87 | """ 88 | } 89 | } 90 | } 91 | 92 | trait Fixture { 93 | val City = "city" 94 | object Name extends Key[String]("name") 95 | object Population extends Key[Int]("population") 96 | object Distance extends Key[Int]("distance") 97 | 98 | implicit val graph = TinkerGraph.open.asScala() 99 | val paris = graph + (City, Name -> "paris") 100 | val london = graph + (City, Name -> "london") 101 | val rail = paris --- (EuroStar, Distance -> 495) --> london 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /gremlin-scala/src/test/scala/gremlin/scala/SelectSpec.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerFactory 4 | import org.scalatest.wordspec.AnyWordSpec 5 | import org.scalatest.matchers.should.Matchers 6 | import shapeless.{::, HNil} 7 | import java.util.{Map => JMap} 8 | import scala.collection.JavaConverters._ 9 | 10 | class SelectSpec extends AnyWordSpec with Matchers { 11 | def graph = TinkerFactory.createModern.asScala() 12 | 13 | "selecting all labelled steps" should { 14 | "support single label" in { 15 | val path: List[Tuple1[Edge]] = 16 | graph.V(1).outE().as("a").select().toList() 17 | 18 | path(0)._1 shouldBe graph.E(9).head() 19 | path(1)._1 shouldBe graph.E(7).head() 20 | path(2)._1 shouldBe graph.E(8).head() 21 | } 22 | 23 | "support multiple label" in { 24 | val path: List[(Vertex, Edge)] = 25 | graph.V(1).as("a").outE().as("b").select().toList() 26 | 27 | val v1 = graph.V(1).head() 28 | path(0) shouldBe ((v1, graph.E(9).head())) 29 | path(1) shouldBe ((v1, graph.E(7).head())) 30 | path(2) shouldBe ((v1, graph.E(8).head())) 31 | } 32 | 33 | "works without labelled steps" in { 34 | val path: List[Unit] = 35 | graph.V(1).outE().inV().select().toList() 36 | 37 | path(0) shouldBe (()) 38 | path(1) shouldBe (()) 39 | path(2) shouldBe (()) 40 | } 41 | } 42 | 43 | "selecting one or more specific labels" should { 44 | val a = StepLabel[Vertex]() 45 | val b = StepLabel[Edge]() 46 | val c = StepLabel[Double]() 47 | 48 | val v1 = graph.V(1).head() 49 | val e9 = graph.E(9).head() 50 | 51 | def newTraversal: GremlinScala.Aux[Double, Vertex :: Edge :: Double :: HNil] = 52 | graph.V(1).as(a).outE("created").as(b).value(TestGraph.Weight).as(c) 53 | 54 | "derive types for a simple as/select" in { 55 | val traversal = newTraversal 56 | val result: Vertex = 57 | traversal.select(a).head() 58 | 59 | result shouldBe v1 60 | } 61 | 62 | "derive types for as/select with two labels" in { 63 | val traversal = newTraversal 64 | val result: (Vertex, Edge) = 65 | traversal.select((a, b)).head() 66 | 67 | result shouldBe ((v1, e9)) 68 | } 69 | 70 | "derive types for as/select with three labels" in { 71 | val traversal = newTraversal 72 | val result: (Vertex, Edge, Double) = 73 | traversal.select((a, b, c)).head() 74 | 75 | result shouldBe ((v1, e9, 0.4)) 76 | } 77 | } 78 | 79 | "resets labels on ReducingBarrier steps" should { 80 | "work for `mean`" in { 81 | graph 82 | .V() 83 | .as("a") 84 | .outE("created") 85 | .value(TestGraph.Weight) 86 | .mean() 87 | .as("b") 88 | .select() 89 | .head() 90 | ._1 shouldBe 0.49999999999999994 91 | } 92 | 93 | "work for `count`" in { 94 | graph 95 | .V() 96 | .as("a") 97 | .out("created") 98 | .count() 99 | .as("b") 100 | .select() 101 | .head() 102 | ._1 shouldBe 4 103 | } 104 | 105 | "work for `max`" in { 106 | graph 107 | .V() 108 | .as("a") 109 | .outE("created") 110 | .value(TestGraph.Weight) 111 | .max() 112 | .as("b") 113 | .select() 114 | .head() 115 | ._1 shouldBe 1.0 116 | } 117 | 118 | "work for `min`" in { 119 | graph 120 | .V() 121 | .as("a") 122 | .outE("created") 123 | .value(TestGraph.Weight) 124 | .min() 125 | .as("b") 126 | .select() 127 | .head() 128 | ._1 shouldBe 0.2 129 | } 130 | 131 | "work for `sum`" in { 132 | val sum = graph 133 | .V() 134 | .as("a") 135 | .outE("created") 136 | .value(TestGraph.Weight) 137 | .sum() 138 | .as("b") 139 | .select() 140 | .head() 141 | ._1 142 | 143 | (sum: Double) shouldBe 2d +- 0.1d 144 | } 145 | } 146 | 147 | "select column" should { 148 | "extract keys from map" in { 149 | val result: collection.Set[String] = graph 150 | .V() 151 | .hasLabel("software") 152 | .group(By(__().value(Key[String]("name"))), By(__().in("created"))) 153 | .selectKeys 154 | .head() 155 | .asScala 156 | result shouldBe Set("ripple", "lop") 157 | } 158 | 159 | "extract keys from map entry" in { 160 | val result: List[String] = graph 161 | .V() 162 | .hasLabel("software") 163 | .group(By(__().value(Key[String]("name"))), By(__().in("created"))) 164 | .unfold[JMap.Entry[String, Vertex]]() 165 | .selectKeys 166 | .toList() 167 | result shouldBe List("ripple", "lop") 168 | } 169 | 170 | "extract keys from path" in { 171 | val result: Seq[collection.Set[String]] = graph 172 | .V(1) 173 | .as("a") 174 | .outE() 175 | .as("b") 176 | .path() 177 | .selectKeys 178 | .head() 179 | .asScala 180 | .toList 181 | .map(_.asScala) 182 | result shouldBe List(Set("a"), Set("b")) 183 | } 184 | 185 | "extract values from map" in { 186 | val result: Iterable[String] = graph 187 | .V() 188 | .hasLabel("software") 189 | .group(By(Key[String]("lang")), By(Key[String]("name"))) 190 | .selectValues 191 | .head() 192 | .asScala 193 | .flatMap(_.asScala) 194 | result shouldBe List("lop", "ripple") 195 | } 196 | 197 | "extract values from map entry" in { 198 | val result: List[String] = graph 199 | .V() 200 | .hasLabel("software") 201 | .group(By(__().value(Key[String]("name"))), 202 | By(__().in("created").value(Key[String]("name")))) 203 | .unfold[JMap.Entry[String, String]]() 204 | .selectValues 205 | .toList() 206 | 207 | result shouldBe List("josh", "marko") 208 | } 209 | 210 | "extract values from path" in { 211 | val result: java.util.List[Any] = graph 212 | .V(1) 213 | .as("a") 214 | .outE() 215 | .as("b") 216 | .path() 217 | .selectValues 218 | .head() 219 | result.get(0).toString shouldBe "v[1]" 220 | result.get(1).toString shouldBe "e[9][1-created->3]" 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /gremlin-scala/src/test/scala/gremlin/scala/TestBase.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerFactory 4 | import org.scalatest.funspec.AnyFunSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | trait TestGraph { 8 | implicit val graph = TinkerFactory.createClassic.asScala() 9 | def v(i: Int) = graph.V(i: Integer).head() 10 | def e(i: Int) = graph.E(i: Integer).head() 11 | 12 | def print(gs: GremlinScala[_]) = println(gs.toList()) 13 | } 14 | 15 | object TestGraph { 16 | val Name = Key[String]("name") 17 | val Age = Key[Int]("age") 18 | val Created = Key[Int]("created") 19 | val Location = Key[String]("location") 20 | val Weight = Key[Double]("weight") 21 | val DoesNotExist = Key[Any]("doesnt_exist") 22 | 23 | @label("person") 24 | case class Person(name: String, age: Int) 25 | 26 | @label("software") 27 | case class Software(name: String, lang: String) 28 | } 29 | 30 | trait TestBase extends AnyFunSpec with Matchers with TestGraph { 31 | implicit class Properties[A](set: Iterable[Property[A]]) { 32 | def unroll(): Iterable[A] = set.map(_.value) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gremlin-scala/src/test/scala/gremlin/scala/TraversalStrategySpec.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import java.util 4 | import java.util.concurrent.CompletableFuture 5 | 6 | import org.apache.tinkerpop.gremlin.process.remote.RemoteConnection 7 | import org.apache.tinkerpop.gremlin.process.remote.traversal.{ 8 | AbstractRemoteTraversal, 9 | DefaultRemoteTraverser, 10 | RemoteTraversal 11 | } 12 | import org.apache.tinkerpop.gremlin.structure.{Vertex => TVertex} 13 | import org.apache.tinkerpop.gremlin.process.traversal.{Bytecode, Step, TraversalSideEffects} 14 | import org.apache.tinkerpop.gremlin.process.traversal.Traverser.Admin 15 | import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph 16 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerFactory 17 | import org.scalamock.scalatest.MockFactory 18 | import org.scalatest.wordspec.AnyWordSpec 19 | import org.scalatest.matchers.should.Matchers 20 | 21 | import scala.concurrent.Await 22 | import scala.concurrent.duration.{FiniteDuration, MILLISECONDS} 23 | import scala.util.Random 24 | 25 | class TraversalStrategySpec extends AnyWordSpec with Matchers with MockFactory { 26 | 27 | "sack step".can { 28 | 29 | /** http://tinkerpop.apache.org/docs/current/reference/#sack-step */ 30 | "carry simple value" when { 31 | "using constant for initial sack" in new Fixture { 32 | graph.configure(_.withSack(1d)).V().sack().toList() shouldBe List(1d, 1d, 1d, 1d, 1d, 1d) 33 | } 34 | 35 | "using function for initial sack" in new Fixture { 36 | graph.configure(_.withSack(() => 1d)).V().sack().toList() shouldBe List(1d, 1d, 1d, 1d, 1d, 1d) 37 | 38 | val randomValues = 39 | graph.configure(_.withSack(() => Random.nextDouble())).V().sack().toList() 40 | randomValues.toSet.size shouldBe 6 41 | } 42 | } 43 | 44 | "transform the sack on the go" in new Fixture { 45 | val result = graph 46 | .configure(_.withSack(1d)) 47 | .V() 48 | .repeat { 49 | _.outE().sack { (curr: Double, edge) => 50 | curr * edge.value2(Weight) 51 | }.inV() 52 | } 53 | .times(2) 54 | .sack() 55 | .toSet() 56 | 57 | result shouldBe Set(1d, 0.4d) 58 | } 59 | 60 | "be modulated with by operator" when { 61 | "modulating by property" in new Fixture { 62 | val result = graph 63 | .configure(_.withSack(1d)) 64 | .V(1) 65 | .outE() 66 | .sack(multiply, By(Weight)) 67 | .inV() 68 | .sack() 69 | .toSet() 70 | 71 | result shouldBe Set(0.4d, 0.5d, 1d) 72 | } 73 | 74 | "modulating by traversal" in new Fixture { 75 | val result = graph 76 | .configure(_.withSack(1d)) 77 | .V(1) 78 | .outE() 79 | .sack(multiply, By(__().value(Weight))) 80 | .inV() 81 | .sack() 82 | .toSet() 83 | 84 | result shouldBe Set(0.4d, 0.5d, 1d) 85 | } 86 | def multiply(a: Double, b: Double): Double = a * b 87 | } 88 | 89 | "use provided split operator when cloning sack" in new Fixture { 90 | var counter = 0 91 | val identityWithCounterIncrease = { value: Double => 92 | counter += 1 93 | value 94 | } 95 | 96 | graph 97 | .configure(_.withSack(1d, splitOperator = identityWithCounterIncrease)) 98 | .V() 99 | .out() 100 | .toList() 101 | counter shouldBe 6 102 | } 103 | 104 | "use provided merge operator when bulking sack" in new Fixture { 105 | val sum = (f1: Double, f2: Double) => f1 + f2 106 | graph 107 | .configure(_.withSack(1d, mergeOperator = sum)) 108 | .V(1) 109 | .out("knows") 110 | .in("knows") 111 | .sack() 112 | .toList() shouldBe List(2d, 2d) 113 | // without `sum` would be List(1d, 1d) 114 | } 115 | 116 | "be configured when starting from an element" when { 117 | "on a vertex" in new Fixture { 118 | val v1: Vertex = graph.V(1).head() 119 | v1.start(_.withSack(1d)).outE(Knows).sack().toList() shouldBe List(1d, 1d) 120 | } 121 | 122 | "on an edge" in new Fixture { 123 | val e7: Edge = graph.E(7).head() 124 | e7.start(_.withSack(1d)).outV().outE(Knows).sack().toList() shouldBe List(1d, 1d) 125 | } 126 | } 127 | } 128 | 129 | "withRemote" should { 130 | "use RemoteConnection" in new RemoteGraphFixture { 131 | // Execute a traversal with provided vertices 132 | val result = remoteGraph.V().toList() 133 | result shouldEqual mockVertices 134 | } 135 | 136 | "support promise" in new RemoteGraphFixture { 137 | // Execute a traversal using promise with the provided vertices 138 | val future = remoteGraph.V().promise() 139 | val result = Await.result(future, FiniteDuration(1000, MILLISECONDS)) 140 | result shouldEqual mockVertices 141 | } 142 | } 143 | 144 | trait Fixture { 145 | val graph = TinkerFactory.createModern.asScala() 146 | val Name = Key[String]("name") 147 | val Lang = Key[String]("lang") 148 | val Weight = Key[Double]("weight") 149 | val Knows = "knows" 150 | } 151 | 152 | trait RemoteGraphFixture { 153 | val graph = EmptyGraph.instance().asScala() 154 | 155 | // Stub out a remote connection that responds to g.V() with 2 vertices 156 | val connection = stub[RemoteConnection] 157 | val remoteGraph = graph.configure(_.withRemote(connection)) 158 | 159 | // effectively a g.V() bytecode 160 | val expectedBytecode: Bytecode = new Bytecode() 161 | expectedBytecode.addStep("V") 162 | 163 | // data to return 164 | val mockVertices = List[TVertex]( 165 | DetachedVertex(id = 1: Integer, label = "person"), 166 | DetachedVertex(id = 2: Integer, label = "person") 167 | ) 168 | 169 | // Create a future that completes immediately and provides a remote traversal providing vertices 170 | val vertexResult = new CompletableFuture[RemoteTraversal[_ <: Any, TVertex]] 171 | val traversal = new AbstractRemoteTraversal[Int, TVertex] { 172 | val it = mockVertices.iterator 173 | 174 | override def nextTraverser: Admin[TVertex] = 175 | new DefaultRemoteTraverser[Vertex](it.next(), 1) 176 | 177 | override def getSideEffects: TraversalSideEffects = 178 | null // not necessary for this test 179 | 180 | override def next(): TVertex = nextTraverser().get() 181 | 182 | override def hasNext: Boolean = it.hasNext 183 | 184 | override def getSteps: util.List[Step[_, _]] = super.getSteps 185 | } 186 | vertexResult.complete(traversal) 187 | 188 | // when expected byte code provided, return vertex result. 189 | (connection.submitAsync[TVertex] _ when expectedBytecode).returns(vertexResult) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /gremlin-scala/src/test/scala/gremlin/scala/dsl/DslSpec.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala.dsl 2 | 3 | import gremlin.scala._ 4 | import java.util.{Map => JMap} 5 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerFactory 6 | import org.scalatest.wordspec.AnyWordSpec 7 | import org.scalatest.matchers.should.Matchers 8 | import scala.collection.mutable 9 | import scala.collection.JavaConverters._ 10 | import shapeless._ 11 | 12 | class DslSpec extends AnyWordSpec with Matchers { 13 | import TestDomain._ 14 | 15 | "finds all persons" in { 16 | val personSteps = PersonSteps(TinkerFactory.createModern) 17 | personSteps.toSet() shouldBe Set( 18 | Person(Some(1), "marko", 29), 19 | Person(Some(2), "vadas", 27), 20 | Person(Some(4), "josh", 32), 21 | Person(Some(6), "peter", 35) 22 | ) 23 | } 24 | 25 | "label with `as` and typesafe `select` of domain types" should { 26 | 27 | "select all labelled steps by default" in { 28 | implicit val graph = TinkerFactory.createModern 29 | 30 | val personAndSoftware: List[(Person, Software)] = 31 | PersonSteps(graph) 32 | .as("person") 33 | .created 34 | .as("software") 35 | .select() 36 | .toList() 37 | (personAndSoftware should have).size(4) 38 | 39 | val softwareByCreator: Map[String, Software] = personAndSoftware.map { 40 | case (person, software) => (person.name, software) 41 | }.toMap 42 | softwareByCreator("marko") shouldBe Software("lop", "java") 43 | } 44 | 45 | "allow to select one labelled step only" in { 46 | implicit val graph = TinkerFactory.createModern 47 | val labelPerson = StepLabel[Person]("p") 48 | val labelSoftware = StepLabel[Software]("s") 49 | 50 | val personAndSoftware: Set[Software] = 51 | PersonSteps(graph) 52 | .as(labelPerson) 53 | .created 54 | .as(labelSoftware) 55 | .select(labelSoftware) 56 | .toSet() 57 | personAndSoftware shouldBe Set(Software("lop", "java"), Software("ripple", "java")) 58 | } 59 | 60 | "allow to select multiple labelled steps" in { 61 | implicit val graph = TinkerFactory.createModern 62 | val labelPerson = StepLabel[Person]("p") 63 | val labelSoftware = StepLabel[Software]("s") 64 | 65 | val personAndSoftware: List[(Software, Person)] = 66 | PersonSteps(graph) 67 | .as(labelPerson) 68 | .created 69 | .as(labelSoftware) 70 | .select((labelSoftware, labelPerson)) 71 | .toList() 72 | (personAndSoftware should have).size(4) 73 | 74 | val softwareByCreator: Map[String, Software] = personAndSoftware.map { 75 | case (software, person) => (person.name, software) 76 | }.toMap 77 | softwareByCreator("marko") shouldBe Software("lop", "java") 78 | } 79 | } 80 | 81 | "finds combination of person/software in for comprehension" in { 82 | implicit val graph = TinkerFactory.createModern 83 | 84 | val traversal = for { 85 | person <- PersonSteps(graph) 86 | software <- person.created 87 | } yield (person.name, software) 88 | 89 | val tuples = traversal.toSet() shouldBe Set( 90 | ("marko", Software("lop", "java")), 91 | ("josh", Software("lop", "java")), 92 | ("peter", Software("lop", "java")), 93 | ("josh", Software("ripple", "java")) 94 | ) 95 | } 96 | 97 | "filter with traversal on domain type" when { 98 | "domain type is a case class" in { 99 | val ripples = PersonSteps(TinkerFactory.createModern) 100 | .filter(_.created.isRipple) 101 | 102 | ripples.toList() shouldBe List( 103 | Person(Some(4), "josh", 32) 104 | ) 105 | } 106 | } 107 | 108 | "filterNot with traversal on domain type" in { 109 | val notRipple = PersonSteps(TinkerFactory.createModern) 110 | .filterNot(_.created.isRipple) 111 | 112 | notRipple.toList().size shouldBe 3 113 | } 114 | 115 | "filter on domain type" in { 116 | val markos: List[Person] = 117 | PersonSteps(TinkerFactory.createModern) 118 | .filterOnEnd(_.name == "marko") 119 | .toList() 120 | 121 | markos.size shouldBe 1 122 | } 123 | 124 | "aggregate intermediary results into a collection" in { 125 | val allPersons = mutable.ArrayBuffer.empty[Person] 126 | val markos: List[Person] = 127 | PersonSteps(TinkerFactory.createModern) 128 | .aggregate(allPersons) 129 | .filterOnEnd(_.name == "marko") 130 | .toList() 131 | 132 | markos.size shouldBe 1 133 | allPersons.size should be > 1 134 | } 135 | 136 | "allow side effects" in { 137 | var i = 0 138 | PersonSteps(TinkerFactory.createModern).sideEffect(_ => i = i + 1).iterate() 139 | i shouldBe 4 140 | } 141 | 142 | "deduplicates" in { 143 | val results: List[Person] = 144 | PersonSteps(TinkerFactory.createModern).created.createdBy.dedup().toList() 145 | results.size shouldBe 3 146 | } 147 | 148 | "allows to use underlying gremlin-scala steps" in { 149 | val steps: PersonSteps[_] = 150 | PersonSteps(TinkerFactory.createModern) 151 | .onRaw(_.hasId(1: Integer)) 152 | steps.toList().size shouldBe 1 153 | } 154 | 155 | "traverses from person to software" in { 156 | val personSteps = 157 | PersonSteps(TinkerFactory.createModern) 158 | .onRaw(_.hasId(1: Integer)) 159 | 160 | personSteps.created.toSet() shouldBe Set(Software("lop", "java")) 161 | } 162 | 163 | "supports collections in map/flatMap" when { 164 | implicit val graph = TinkerFactory.createModern 165 | def personSteps = PersonSteps(graph) 166 | 167 | "using List" in { 168 | val query = personSteps.map { person => 169 | (person.name, person.created.toList()) 170 | } 171 | 172 | val results: List[(String, List[Software])] = query.toList() 173 | results.size shouldBe 4 174 | } 175 | 176 | "using Set" in { 177 | val query = personSteps.map { person => 178 | (person.name, person.created.toSet()) 179 | } 180 | 181 | val results: List[(String, Set[Software])] = query.toList() 182 | results.size shouldBe 4 183 | } 184 | } 185 | 186 | "allows to be cloned" in { 187 | val graph = TinkerFactory.createModern 188 | def personSteps = PersonSteps(graph) 189 | 190 | val query = personSteps.hasName("marko") 191 | val queryCloned = query.clone() 192 | query.toList().size shouldBe 1 193 | queryCloned.toList().size shouldBe 1 194 | } 195 | } 196 | 197 | object TestDomain { 198 | @label("person") case class Person(@id id: Option[Integer], name: String, age: Integer) 199 | extends DomainRoot 200 | @label("software") case class Software(name: String, lang: String) extends DomainRoot 201 | 202 | object PersonSteps { 203 | def apply(graph: Graph) = new PersonSteps[HNil](graph.V().hasLabel[Person]()) 204 | } 205 | class PersonSteps[Labels <: HList](override val raw: GremlinScala[Vertex]) 206 | extends NodeSteps[Person, Labels](raw) { 207 | 208 | def created = new SoftwareSteps[Labels](raw.out("created")) 209 | 210 | def name = 211 | new Steps[String, String, Labels](raw.map(_.value[String]("name"))) 212 | 213 | def hasName(name: String) = 214 | new PersonSteps[Labels](raw.has(Key("name") -> name)) 215 | } 216 | 217 | class SoftwareSteps[Labels <: HList](override val raw: GremlinScala[Vertex]) 218 | extends NodeSteps[Software, Labels](raw) { 219 | 220 | def createdBy = new PersonSteps[Labels](raw.in("created")) 221 | 222 | def isRipple = new SoftwareSteps[Labels](raw.has(Key("name") -> "ripple")) 223 | } 224 | 225 | implicit def toPersonSteps[Labels <: HList]( 226 | steps: Steps[Person, Vertex, Labels]): PersonSteps[Labels] = 227 | new PersonSteps[Labels](steps.raw) 228 | 229 | implicit def personStepsConstructor[Labels <: HList] 230 | : Constructor.Aux[Person, Labels, Vertex, PersonSteps[Labels]] = 231 | Constructor.forDomainNode[Person, Labels, PersonSteps[Labels]](new PersonSteps[Labels](_)) 232 | 233 | implicit def softwareStepsConstructor[Labels <: HList] 234 | : Constructor.Aux[Software, Labels, Vertex, SoftwareSteps[Labels]] = 235 | Constructor.forDomainNode[Software, Labels, SoftwareSteps[Labels]](new SoftwareSteps[Labels](_)) 236 | 237 | implicit def liftPerson(person: Person)(implicit graph: Graph): PersonSteps[HNil] = 238 | new PersonSteps[HNil](graph.asScala().V(person.id.get)) 239 | } 240 | -------------------------------------------------------------------------------- /gremlin-scala/src/test/scala/gremlin/scala/marshallable/MarshallableSpec.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala.marshallable 2 | // specifically moved this to a separate package to verify `gremlin.scala.` imports 3 | 4 | import gremlin.scala.{ 5 | asScalaEdge, 6 | asScalaVertex, 7 | id, 8 | label, 9 | nullable, 10 | underlying, 11 | Edge, 12 | Element, 13 | GraphAsScala, 14 | Marshallable, 15 | Vertex 16 | } 17 | import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph 18 | import org.scalatest.wordspec.AnyWordSpec 19 | import org.scalatest.matchers.should.Matchers 20 | import scala.collection.JavaConverters._ 21 | import shapeless.test.illTyped 22 | 23 | case class CCSimple(s: String, i: Int) 24 | 25 | case class MyValueClass(value: Int) extends AnyVal 26 | case class CCWithValueClass(s: String, i: MyValueClass) 27 | case class CCWithOption(i: Int, s: Option[String]) 28 | case class CCWithOptionValueClass(s: String, i: Option[MyValueClass]) 29 | case class CCWithOptionAnyVal(x: Option[Int], y: Option[Long]) 30 | case class CCWithList(s: String, ss: List[String], is: List[Int], ds: List[Double]) 31 | case class CCWithSet(s: String, ss: Set[String]) 32 | case class CCWithNullable(i: Int, @nullable maybeNull: String) 33 | 34 | case class CCWithOptionId(s: String, @id id: Option[Int]) 35 | case class CCWithOptionIdNested(s: String, @id id: Option[Int], i: MyValueClass) 36 | case class CCWithNonOptionalIdShouldFail(@id id: Int) 37 | 38 | case class MyIdValueClass(value: Long) extends AnyVal 39 | case class CCWithValueClassId(s: String, @id id: Option[MyIdValueClass]) 40 | 41 | case class CCWithUnderlyingVertex(@underlying underlying: Option[Vertex], s: String) 42 | case class CCWithNonOptionalUnderlyingShouldFail(@underlying underlying: Vertex) 43 | 44 | @label("label_a") 45 | case class CCWithLabel(s: String) 46 | 47 | @label("the_label") 48 | case class ComplexCC( 49 | s: String, 50 | l: Long, 51 | o: Option[String], 52 | seq: Seq[String], 53 | map: Map[String, String], 54 | nested: NestedClass 55 | ) { def randomDef = ??? } 56 | 57 | case class NestedClass(s: String) 58 | 59 | class NoneCaseClass(s: String) 60 | 61 | class MarshallableSpec extends AnyWordSpec with Matchers { 62 | 63 | "marshals / unmarshals case classes".which { 64 | 65 | "only have simple members" in new Fixture { 66 | val cc = CCSimple("text", 12) 67 | val v = graph + cc 68 | 69 | val vl = graph.V(v.id).head() 70 | vl.label shouldBe cc.getClass.getSimpleName 71 | vl.valueMap should contain("s" -> cc.s) 72 | vl.valueMap should contain("i" -> cc.i) 73 | } 74 | 75 | "contain options" should { 76 | "map `Some[A]` to `A`" in new Fixture { 77 | val ccWithOptionSome = 78 | CCWithOption(Int.MaxValue, Some("optional value")) 79 | val v = graph + ccWithOptionSome 80 | v.toCC[CCWithOption] shouldBe ccWithOptionSome 81 | 82 | val vl = graph.V(v.id).head() 83 | vl.value[String]("s") shouldBe ccWithOptionSome.s.get 84 | } 85 | 86 | "map `None` to `null`" in new Fixture { 87 | val ccWithOptionNone = CCWithOption(Int.MaxValue, None) 88 | val v = graph + ccWithOptionNone 89 | v.toCC[CCWithOption] shouldBe ccWithOptionNone 90 | 91 | val vl = graph.V(v.id).head() 92 | vl.keys should not contain "s" //None should be mapped to `null` 93 | } 94 | 95 | "handle an optional AnyVal" in new Fixture { 96 | val ccWithOptionAnyVal = CCWithOptionAnyVal(Some(1), None) 97 | val v = graph + ccWithOptionAnyVal 98 | v.toCC[CCWithOptionAnyVal] 99 | 100 | val vl = graph.V(v.id).head() 101 | vl.value[Int]("x") shouldBe ccWithOptionAnyVal.x.get 102 | vl.keys should not contain "y" 103 | } 104 | 105 | // Background: if we marshal Option types, the graph db needs to understand scala.Option, 106 | // which wouldn't make any sense. So we rather translate it to `null` if it's `None`. 107 | // https://github.com/mpollmeier/gremlin-scala/issues/98 108 | } 109 | 110 | "contain value classes" should { 111 | "unwrap a plain value class" in new Fixture { 112 | val cc = CCWithValueClass("some text", MyValueClass(42)) 113 | val v = graph + cc 114 | 115 | val vl = graph.V(v.id).head() 116 | vl.label shouldBe cc.getClass.getSimpleName 117 | vl.valueMap should contain("s" -> cc.s) 118 | vl.valueMap should contain("i" -> cc.i.value) 119 | vl.toCC[CCWithValueClass] shouldBe cc 120 | } 121 | 122 | "unwrap an optional value class" in new Fixture { 123 | val cc = CCWithOptionValueClass("some text", Some(MyValueClass(42))) 124 | val v = graph + cc 125 | 126 | val vl = graph.V(v.id).head() 127 | vl.label shouldBe cc.getClass.getSimpleName 128 | vl.valueMap should contain("s" -> cc.s) 129 | vl.valueMap should contain("i" -> cc.i.get.value) 130 | vl.toCC[CCWithOptionValueClass] shouldBe cc 131 | } 132 | 133 | "handle None value class" in new Fixture { 134 | val cc = CCWithOptionValueClass("some text", None) 135 | val v = graph + cc 136 | 137 | val vl = graph.V(v.id).head() 138 | vl.label shouldBe cc.getClass.getSimpleName 139 | vl.valueMap should contain("s" -> cc.s) 140 | vl.valueMap.keySet should not contain ("i") 141 | vl.toCC[CCWithOptionValueClass] shouldBe cc 142 | } 143 | 144 | "handle value class ID" in new Fixture { 145 | val cc = CCWithValueClassId(s = "some text", id = None) 146 | val v = graph + cc 147 | val underlyingId = v.id().asInstanceOf[Long] 148 | v.toCC[CCWithValueClassId].id shouldBe Some(MyIdValueClass(underlyingId)) 149 | } 150 | } 151 | 152 | "handle List members" in new Fixture { 153 | val cc = CCWithList(s = "foo", ss = List("one", "two"), is = List(1, 2), ds = Nil) 154 | 155 | val v = graph + cc 156 | v.toCC[CCWithList] shouldBe cc 157 | 158 | val vl = graph.V(v.id).head() 159 | val properties = vl.properties[String]("ss").asScala.toList 160 | properties.size shouldBe 2 161 | properties.map(_.value) shouldBe List("one", "two") 162 | } 163 | 164 | "handle Set members" in new Fixture { 165 | val cc = CCWithSet(s = "foo", ss = Set("bar", "baz")) 166 | 167 | val v = graph + cc 168 | v.toCC[CCWithSet] shouldBe cc 169 | 170 | val vl = graph.V(v.id).head() 171 | val properties = vl.properties[String]("ss").asScala.toList 172 | properties.size shouldBe 2 173 | properties.map(_.value) shouldBe List("bar", "baz") 174 | } 175 | 176 | "allows members to be `null` if annotated with `@nullable`" in new Fixture { 177 | val cc = CCWithNullable(i = 42, maybeNull = null) 178 | 179 | val v = graph + cc 180 | v.toCC[CCWithNullable] shouldBe cc 181 | } 182 | 183 | "define their custom marshaller" in new Fixture { 184 | val ccWithOptionNone = CCWithOption(Int.MaxValue, None) 185 | 186 | implicit val marshaller = new Marshallable[CCWithOption] { 187 | import gremlin.scala.PropertyOps 188 | def fromCC(cc: CCWithOption) = 189 | FromCC(None, "CCWithOption", List("i" -> cc.i, "s" -> cc.s.getOrElse("undefined"))) 190 | 191 | def toCC(element: Element): CCWithOption = 192 | CCWithOption(i = element.value[Int]("i"), s = element.property[String]("s").toOption) 193 | } 194 | 195 | val v = graph + ccWithOptionNone 196 | v.toCC[CCWithOption](marshaller) shouldBe CCWithOption(ccWithOptionNone.i, Some("undefined")) 197 | } 198 | 199 | "combination of many things" in new Fixture { 200 | val cc = ComplexCC( 201 | "some string", 202 | Long.MaxValue, 203 | Some("option type"), 204 | Seq("test1", "test2"), 205 | Map("key1" -> "value1", "key2" -> "value2"), 206 | NestedClass("nested") 207 | ) 208 | 209 | val v = graph + cc 210 | 211 | val vl = graph.V(v.id).head() 212 | vl.label shouldBe "the_label" 213 | vl.valueMap should contain("s" -> cc.s) 214 | vl.valueMap should contain("l" -> cc.l) 215 | vl.valueMap should contain("o" -> cc.o.get) 216 | vl.valueMap should contain("seq" -> cc.seq) 217 | vl.valueMap should contain("map" -> cc.map) 218 | vl.valueMap should contain("nested" -> cc.nested) 219 | } 220 | 221 | "have an Option @id annotation" in new Fixture { 222 | val cc = CCWithOptionId(s = "text", id = Some(12)) 223 | val v = graph + cc 224 | 225 | v.toCC[CCWithOptionId].s shouldBe cc.s 226 | 227 | val vl = graph.V(v.id).head() 228 | vl.label shouldBe cc.getClass.getSimpleName 229 | vl.valueMap should contain("s" -> cc.s) 230 | } 231 | 232 | "fails compilation for non-option @id annotation" in new Fixture { 233 | // id must be assigned by graph (in the context of Marshallable) 234 | illTyped { 235 | """ 236 | val cc = CCWithNonOptionalIdShouldFail(12) 237 | graph + cc 238 | """ 239 | } 240 | } 241 | 242 | "have @underlying vertex" in new Fixture { 243 | val cc = CCWithUnderlyingVertex( 244 | underlying = None, //not known yet, not part of graph yet 245 | "some string" 246 | ) 247 | 248 | val vertex = graph + cc 249 | val ccFromVertex = vertex.toCC[CCWithUnderlyingVertex] 250 | ccFromVertex.s shouldBe cc.s 251 | ccFromVertex.underlying shouldBe Symbol("defined") 252 | 253 | graph.V(ccFromVertex.underlying.get.id).value[String]("s").head() shouldBe cc.s 254 | } 255 | 256 | "fails compilation for non-option @underlying annotation" in new Fixture { 257 | illTyped { 258 | """ 259 | val cc = CCWithNonOptionalUnderlyingShouldFail(null) 260 | graph + cc 261 | """ 262 | } 263 | } 264 | } 265 | 266 | "find vertices by label" in new Fixture { 267 | val ccSimple = CCSimple("a string", 42) 268 | val ccWithOption = CCWithOption(52, Some("other string")) 269 | val ccWithLabel = CCWithLabel("s") 270 | 271 | graph + ccSimple 272 | graph + ccWithOption 273 | graph + ccWithLabel 274 | 275 | graph.V().count().head() shouldBe 3 276 | 277 | val ccSimpleVertices = graph.V().hasLabel[CCSimple]().toList() 278 | (ccSimpleVertices should have).size(1) 279 | ccSimpleVertices.head.toCC[CCSimple] shouldBe ccSimple 280 | 281 | val ccWithLabelVertices = graph.V().hasLabel[CCWithLabel]().toList() 282 | (ccWithLabelVertices should have).size(1) 283 | ccWithLabelVertices.head.toCC[CCWithLabel] shouldBe ccWithLabel 284 | } 285 | 286 | "add edges using case-class".which { 287 | "have no id-annotation" in new CCEdgeAddFixture { 288 | val ccEdgeWithLabelInitial = CCWithLabel("edge-property") 289 | 290 | ccVertexFrom.addEdge(ccVertexTo, ccEdgeWithLabelInitial).toCC[CCWithLabel] 291 | 292 | val ccEdgesWithLabel = graph.E().hasLabel[CCWithLabel]().toList() 293 | (ccEdgesWithLabel should have).size(1) 294 | ccEdgesWithLabel.head.toCC[CCWithLabel] shouldBe ccEdgeWithLabelInitial 295 | } 296 | 297 | "have id-annotation None" in new CCEdgeAddFixture { 298 | val ccEdgeWithOptionIdNoneInitial = CCWithOptionId("edge-property", None) 299 | 300 | val ccEdgeWithOptionIdNone = ccVertexFrom 301 | .addEdge(ccVertexTo, ccEdgeWithOptionIdNoneInitial) 302 | .toCC[CCWithOptionId] 303 | ccEdgeWithOptionIdNone.id should not be empty 304 | 305 | val ccEdgesWithOptionIdNone = graph.E().hasLabel[CCWithOptionId]().toList() 306 | (ccEdgesWithOptionIdNone should have).size(1) 307 | ccEdgesWithOptionIdNone.head 308 | .toCC[CCWithOptionId] shouldBe ccEdgeWithOptionIdNone 309 | } 310 | 311 | "have id-annotation Some(123)" in new CCEdgeAddFixture { 312 | val ccEdgeWithOptionIdSomeInitial = 313 | CCWithOptionId("edge-property", Some(123)) 314 | 315 | val ccEdgeWithOptionIdSome = ccVertexFrom 316 | .addEdge(ccVertexTo, ccEdgeWithOptionIdSomeInitial) 317 | .toCC[CCWithOptionId] 318 | ccEdgeWithOptionIdSome.id shouldBe ccEdgeWithOptionIdSomeInitial.id 319 | 320 | val ccEdgesWithOptionIdSome = graph.E().hasLabel[CCWithOptionId]().toList() 321 | (ccEdgesWithOptionIdSome should have).size(1) 322 | ccEdgesWithOptionIdSome.head 323 | .toCC[CCWithOptionId] shouldBe ccEdgeWithOptionIdSome 324 | } 325 | } 326 | 327 | "edge" should { 328 | "update using a case-class template" in new CCEdgeUpdateFixture { 329 | graph 330 | .E(ccWithIdSet.id.get) 331 | .head() 332 | .updateWith(ccUpdate) 333 | .toCC[CC] shouldBe ccUpdate 334 | graph.E(ccWithIdSet.id.get).head().toCC[CC] shouldBe ccUpdate 335 | } 336 | 337 | "update as a case class" in new CCEdgeUpdateFixture { 338 | graph 339 | .E(ccWithIdSet.id.get) 340 | .head() 341 | .updateAs[CC](_.copy(s = ccUpdate.s, i = ccUpdate.i)) 342 | .toCC[CC] shouldBe ccUpdate 343 | graph.E(ccWithIdSet.id.get).head().toCC[CC] shouldBe ccUpdate 344 | } 345 | } 346 | 347 | "vertex" should { 348 | "update using a case-class template" in new CCVertexUpdateFixture { 349 | graph 350 | .V(ccWithIdSet.id.get) 351 | .head() 352 | .updateWith(ccUpdate) 353 | .toCC[CC] shouldBe ccUpdate 354 | graph.V(ccWithIdSet.id.get).head().toCC[CC] shouldBe ccUpdate 355 | } 356 | 357 | "update as a case class" in new CCVertexUpdateFixture { 358 | graph 359 | .V(ccWithIdSet.id.get) 360 | .head() 361 | .updateAs[CC](_.copy(s = ccUpdate.s, i = ccUpdate.i)) 362 | .toCC[CC] shouldBe ccUpdate 363 | graph.V(ccWithIdSet.id.get).head().toCC[CC] shouldBe ccUpdate 364 | } 365 | } 366 | 367 | "marshals a the end step of a traversal" in new Fixture { 368 | val cc1 = CCSimple("text one", 1) 369 | val cc2 = CCSimple("text two", 2) 370 | graph + cc1 371 | graph + cc2 372 | 373 | val results: Set[CCSimple] = graph.V().toCC[CCSimple].toSet() 374 | results shouldBe Set(cc1, cc2) 375 | } 376 | 377 | case class EmptyCC() 378 | "handle case class without elements" in new Fixture { 379 | val m = implicitly[Marshallable[EmptyCC]] 380 | } 381 | 382 | trait Fixture { 383 | val graph = TinkerGraph.open.asScala() 384 | } 385 | 386 | trait CCUpdateFixture[E <: Element] extends Fixture { 387 | type CC = CCWithOptionIdNested 388 | val ccInitial = CCWithOptionIdNested("string", None, MyValueClass(42)) 389 | 390 | def ccWithIdSet: CC 391 | lazy val ccUpdate = ccWithIdSet.copy(s = "otherString", i = MyValueClass(7)) 392 | } 393 | 394 | trait CCVertexUpdateFixture extends CCUpdateFixture[Vertex] { 395 | val ccWithIdSet = (graph + ccInitial).toCC[CC] 396 | } 397 | 398 | trait CCEdgeAddFixture extends Fixture { 399 | val ccVertexFrom = graph + "fromLabel" 400 | val ccVertexTo = graph + "toLabel" 401 | } 402 | 403 | trait CCEdgeUpdateFixture extends CCUpdateFixture[Edge] { 404 | private val testVertex = graph + "Huh" 405 | val ccWithIdSet = testVertex.addEdge(testVertex, ccInitial).toCC[CC] 406 | } 407 | 408 | "can't persist a none product type (none case class or tuple)" in { 409 | illTyped { 410 | """ 411 | val graph = TinkerGraph.open.asScala() 412 | graph + new NoneCaseClass("test") 413 | """ 414 | } 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /macros/src/main/scala/gremlin/scala/Annotations.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import scala.annotation.{compileTimeOnly, StaticAnnotation} 4 | import scala.annotation.meta._ 5 | 6 | /** the underlying graph element, typically a vertex or edge */ 7 | @getter @beanGetter 8 | class underlying extends StaticAnnotation 9 | 10 | /** the id of the underlying graph element */ 11 | @getter @beanGetter 12 | class id extends StaticAnnotation 13 | 14 | /** annotate a non-option member with `@nullable` to allow `null` values. 15 | * by default this would throw an error on deserialization 16 | * a.k.a. license to shoot yourself in the foot */ 17 | @getter @beanGetter 18 | class nullable extends StaticAnnotation 19 | 20 | class label(val label: String = "") extends StaticAnnotation 21 | -------------------------------------------------------------------------------- /macros/src/main/scala/gremlin/scala/Marshallable.scala: -------------------------------------------------------------------------------- 1 | package gremlin.scala 2 | 3 | import org.apache.tinkerpop.gremlin.structure.Graph.Hidden 4 | import org.apache.tinkerpop.gremlin.structure.Element 5 | import scala.language.experimental.macros 6 | import scala.reflect.macros.blackbox 7 | 8 | trait Marshallable[CC <: Product] { 9 | type Id = AnyRef 10 | type Label = String 11 | case class FromCC(id: Option[Id], label: Label, properties: List[(String, Any)]) 12 | 13 | def fromCC(cc: CC): FromCC 14 | def toCC(element: Element): CC 15 | } 16 | 17 | object Marshallable { 18 | implicit def materializeMappable[CC <: Product]: Marshallable[CC] = 19 | macro materializeMappableImpl[CC] 20 | 21 | def materializeMappableImpl[CC <: Product: c.WeakTypeTag]( 22 | c: blackbox.Context): c.Expr[Marshallable[CC]] = { 23 | import c.universe._ 24 | val tpe = weakTypeOf[CC] 25 | val companion = tpe.typeSymbol.companion 26 | 27 | val (idParam, fromCCParams, toCCParams) = tpe.decls 28 | .foldLeft[(Tree, Seq[Tree], Seq[Tree])]((q"_root_.scala.None", Seq.empty, Seq.empty)) { 29 | case ((_idParam, _fromCCParams, _toCCParams), field: MethodSymbol) 30 | if field.isCaseAccessor => 31 | val name = field.name 32 | val decoded = name.decodedName.toString 33 | val returnType = field.returnType 34 | 35 | def handleStandardProperty(nullable: Boolean) = { 36 | // check if the property is a value class and try to extract everything we need to unwrap it 37 | val treesForValueClass = for { 38 | valueName <- valueGetter(returnType) 39 | if returnType <:< typeOf[AnyVal] 40 | wrappedType <- wrappedTypeMaybe(returnType) 41 | } yield { // ValueClass property 42 | val valueClassCompanion = returnType.typeSymbol.companion 43 | (_idParam, 44 | _fromCCParams :+ q"_root_.scala.collection.immutable.List($decoded -> cc.$name.$valueName)", 45 | _toCCParams :+ q"$valueClassCompanion(element.value[$wrappedType]($decoded)).asInstanceOf[$returnType]") 46 | } 47 | treesForValueClass.getOrElse { //normal property 48 | val toCCParams = 49 | if (!nullable) { 50 | q"element.value[$returnType]($decoded)" 51 | } else { 52 | // for people who like to shoot themselves in the foot 53 | q"new _root_.gremlin.scala.PropertyOps(element.property[$returnType]($decoded)).toOption.orNull" 54 | } 55 | (_idParam, 56 | _fromCCParams :+ q"_root_.scala.collection.immutable.List($decoded -> cc.$name)", 57 | _toCCParams :+ toCCParams) 58 | } 59 | } 60 | 61 | def handleOptionProperty = { 62 | // check if the property is an Option[AnyVal] and try to extract everything we need to unwrap it 63 | returnType.typeArgs.headOption match { 64 | case Some(innerAnyValClassType) if innerAnyValClassType <:< typeOf[AnyVal] => 65 | valueGetter(innerAnyValClassType) match { 66 | case Some(wrappedValueGetter) => //Option[ValueClass] 67 | val valueClassCompanion = innerAnyValClassType.typeSymbol.companion 68 | (_idParam, 69 | _fromCCParams :+ 70 | q""" 71 | _root_.scala.collection.immutable.List( 72 | cc.$name.map{ x => $decoded -> x.${wrappedValueGetter.name} }.getOrElse($decoded -> null) 73 | ) 74 | """, 75 | _toCCParams :+ 76 | q""" 77 | new _root_.gremlin.scala.PropertyOps(element.property($decoded)) 78 | .toOption 79 | .map($valueClassCompanion.apply) 80 | .asInstanceOf[$returnType] 81 | """) 82 | case None => // Option[AnyVal] 83 | (_idParam, 84 | _fromCCParams :+ 85 | q""" 86 | _root_.scala.collection.immutable.List( 87 | cc.$name.map{ x => $decoded -> x }.getOrElse($decoded -> null) 88 | ) 89 | """, 90 | _toCCParams :+ 91 | q""" 92 | new _root_.gremlin.scala.PropertyOps(element.property($decoded)) 93 | .toOption 94 | .asInstanceOf[$returnType] 95 | """) 96 | } 97 | 98 | case _ => // normal option property 99 | (_idParam, 100 | _fromCCParams :+ q"_root_.scala.collection.immutable.List($decoded -> cc.$name.orNull)", 101 | _toCCParams :+ 102 | q""" 103 | new _root_.gremlin.scala.PropertyOps(element.property($decoded)) 104 | .toOption 105 | .asInstanceOf[$returnType] 106 | """) 107 | } 108 | } 109 | 110 | def handleListProperty = { 111 | (_idParam, 112 | _fromCCParams :+ q"cc.$name.map { x => $decoded -> x }", 113 | _toCCParams :+ 114 | q""" 115 | element.properties($decoded) 116 | .asScala 117 | .toList 118 | .map(_.value) 119 | .asInstanceOf[$returnType] 120 | """) 121 | // element.properties[String]("ss").asScala.toList.map(_.value), 122 | // _toCCParams :+ q"element.value[$returnType]($decoded)") 123 | } 124 | 125 | def handleSetProperty = { 126 | (_idParam, 127 | _fromCCParams :+ q"cc.$name.toList.map { x => $decoded -> x }", 128 | _toCCParams :+ 129 | q""" 130 | element.properties($decoded) 131 | .asScala 132 | .map(_.value) 133 | .toSet 134 | .asInstanceOf[$returnType] 135 | """) 136 | } 137 | 138 | def valueGetter(tpe: Type): Option[MethodSymbol] = 139 | tpe.decls.sorted 140 | .filter(_.isMethod) 141 | .map(_.asMethod) 142 | .takeWhile(!_.isConstructor) 143 | .find(_.paramLists == Nil /* nullary */ ) 144 | 145 | def valueClassConstructor(tpe: Type): Option[MethodSymbol] = 146 | tpe.companion.decls.find(_.name.toString == "apply") match { 147 | case Some(m: MethodSymbol) => Some(m) 148 | case _ => None 149 | } 150 | 151 | def wrappedTypeMaybe(tpe: Type): Option[Type] = 152 | util 153 | .Try(valueClassConstructor(tpe).get.paramLists.head.head.typeSignature) 154 | .toOption 155 | 156 | def handleId = { 157 | assert( 158 | returnType.typeSymbol == weakTypeOf[Option[_]].typeSymbol, 159 | "@id parameter *must* be of type `Option[A]`. In the context of " + 160 | "Marshallable, we have to let the graph assign an id" 161 | ) 162 | 163 | val valueClassParams = for { 164 | valueClass <- returnType.typeArgs.headOption if valueClass <:< typeOf[AnyVal] 165 | wrappedValueGetter <- valueGetter(valueClass) 166 | } yield { 167 | val valueClassCompanion = valueClass.typeSymbol.companion 168 | (q"cc.$name.map(_.${wrappedValueGetter.name}).asInstanceOf[_root_.scala.Option[AnyRef]]", 169 | _fromCCParams, 170 | _toCCParams :+ q"_root_.scala.Option(element.id.asInstanceOf[${wrappedValueGetter.returnType}]).map($valueClassCompanion.apply)") 171 | } 172 | 173 | valueClassParams.getOrElse( 174 | (q"cc.$name.asInstanceOf[_root_.scala.Option[AnyRef]]", 175 | _fromCCParams, 176 | _toCCParams :+ q"_root_.scala.Option(element.id).asInstanceOf[$returnType]")) 177 | } 178 | 179 | def handleUnderlying = { 180 | assert( 181 | returnType.typeSymbol == weakTypeOf[Option[_]].typeSymbol, 182 | "@underlying parameter *must* be of type `Option[A]`, since" + 183 | " it can only be defined after it has been added to the graph" 184 | ) 185 | (q"cc.$name.asInstanceOf[_root_.scala.Option[AnyRef]]", 186 | _fromCCParams, 187 | _toCCParams :+ q"_root_.scala.Option(element).asInstanceOf[$returnType]") 188 | } 189 | 190 | // main control flow 191 | if (field.annotations.map(_.tree.tpe) contains weakTypeOf[id]) { 192 | handleId // @id 193 | } else if (field.annotations.map(_.tree.tpe) contains weakTypeOf[underlying]) { 194 | handleUnderlying // @underlying 195 | } else { // normal property member 196 | assert(!Hidden.isHidden(decoded), 197 | s"The parameter name $decoded can't be used in the persistable case class $tpe") 198 | if (returnType.typeSymbol == weakTypeOf[Option[_]].typeSymbol) { 199 | handleOptionProperty 200 | } else if (returnType.typeSymbol == weakTypeOf[List[_]].typeSymbol) { 201 | handleListProperty 202 | } else if (returnType.typeSymbol == weakTypeOf[Set[_]].typeSymbol) { 203 | handleSetProperty 204 | } else { 205 | handleStandardProperty( 206 | nullable = field.annotations.map(_.tree.tpe).contains(weakTypeOf[nullable])) 207 | } 208 | } 209 | 210 | case (params, _) => params 211 | } 212 | 213 | val label = tpe.typeSymbol.asClass.annotations 214 | .find(_.tree.tpe =:= weakTypeOf[label]) 215 | .map { annotation => 216 | val label = annotation.tree.children.tail.head 217 | q"""$label""" 218 | } 219 | .getOrElse(q"cc.getClass.getSimpleName") 220 | 221 | val ret = c.Expr[Marshallable[CC]] { 222 | q""" 223 | new _root_.gremlin.scala.Marshallable[$tpe] { 224 | import _root_.scala.collection.JavaConverters._ 225 | def fromCC(cc: $tpe) = 226 | this.FromCC( 227 | $idParam, 228 | $label, 229 | _root_.scala.collection.immutable.List[_root_.scala.collection.immutable.List[(_root_.scala.Predef.String,_root_.scala.Any)]](..$fromCCParams) 230 | .flatten 231 | .filter { kv => 232 | _root_.scala.Option(kv._2).isDefined 233 | } 234 | ) 235 | def toCC(element: _root_.gremlin.scala.Element): $tpe = $companion(..$toCCParams) 236 | } 237 | """ 238 | } 239 | // if (tpe.toString.contains("CCWithNullable")) println(ret) 240 | ret 241 | } 242 | 243 | } 244 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.6.2 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("io.shiftleft" % "sbt-ci-release-early" % "2.0.35") 2 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") 3 | --------------------------------------------------------------------------------