All Downloads are FREE. Search and download functionalities are using the official Maven repository.

overflowdb.codegen.CodeGen.scala Maven / Gradle / Ivy

package overflowdb.codegen

import better.files._
import java.lang.System.lineSeparator
import overflowdb.codegen.CodeGen.ConstantContext
import overflowdb.schema.EdgeType.Cardinality
import overflowdb.schema.Property.ValueType
import overflowdb.schema._
import scala.collection.mutable

/** Generates a domain model for OverflowDb traversals for a given domain-specific schema. */
class CodeGen(schema: Schema) {
  import Helpers._
  val basePackage = schema.basePackage
  val nodesPackage = s"$basePackage.nodes"
  val edgesPackage = s"$basePackage.edges"
  val traversalsPackage = s"$basePackage.traversal"

  private var enableScalafmt = true
  private var scalafmtConfig: Option[File] = None

  def disableScalafmt: this.type = {
    enableScalafmt = false
    this
  }

  /** replace entire default scalafmt config (from Formatter.defaultScalafmtConfig) with custom config */
  def withScalafmtConfig(file: java.io.File): this.type = {
    this.scalafmtConfig = Option(file.toScala)
    this
  }

  def run(outputDir: java.io.File): Seq[java.io.File] = {
    warnForDuplicatePropertyDefinitions()
    val _outputDir = outputDir.toScala
    val results =
      writeStarters(_outputDir) ++
      writeConstants(_outputDir) ++
      writeEdgeFiles(_outputDir) ++
      writeNodeFiles(_outputDir) ++
      writeNodeTraversalFiles(_outputDir) :+
      writeNewNodeFile(_outputDir)
    println(s"generated ${results.size} files in ${_outputDir}")

    if (enableScalafmt) {
      val scalaSourceFiles = results.filter(_.extension == Some(".scala"))
      Formatter.run(scalaSourceFiles, scalafmtConfig)
    }
    results.map(_.toJava)
  }

  /* to provide feedback for potential schema optimisation: no need to redefine properties if they are already
   * defined in one of the parents */
  protected def warnForDuplicatePropertyDefinitions() = {
    val warnings = for {
      nodeType <- schema.allNodeTypes
      property <- nodeType.propertiesWithoutInheritance
      baseType <- nodeType.extendzRecursively
      if baseType.propertiesWithoutInheritance.contains(property) && !schema.noWarnList.contains((nodeType, property))
    } yield s"[info]: $nodeType wouldn't need to have property `${property.name}` added explicitly - $baseType already brings it in"

    if (warnings.size > 0) println(s"${warnings.size} warnings found:")
    warnings.sorted.foreach(println)
  }

  protected def writeStarters(outputDir: File): Seq[File] = {
    val results = mutable.Buffer.empty[File]
    val baseDir = outputDir / basePackage.replaceAll("\\.", "/")
    baseDir.createDirectories()
    val domainShortName = schema.domainShortName

    def registerAdditionalSearchPackages: String = {
      schema.additionalTraversalsPackages.map { packageName =>
        s""".registerAdditionalSearchPackage("$packageName")"""
      }.mkString("")
    }

    val domainMain = baseDir.createChild(s"$domainShortName.scala").write(
      s"""package $basePackage
         |
         |import java.nio.file.{Path, Paths}
         |import overflowdb.traversal.help.{DocSearchPackages, TraversalHelp}
         |import overflowdb.{Config, Graph}
         |import scala.jdk.javaapi.CollectionConverters.asJava
         |
         |object $domainShortName {
         |  implicit val defaultDocSearchPackage: DocSearchPackages = DocSearchPackages(getClass.getPackage.getName)
         |
         |  /**
         |    * Syntactic sugar for `new $domainShortName(graph)`.
         |    * Usage:
         |    *   `$domainShortName(graph)` or simply `$domainShortName` if you have an `implicit Graph` in scope
         |    */
         |  def apply(implicit graph: Graph) = new $domainShortName(graph)
         |
         |  def empty: $domainShortName =
         |    new $domainShortName(emptyGraph)
         |
         |  /**
         |    * Instantiate $domainShortName with storage.
         |    * If the storage file already exists, it will load (a subset of) the data into memory. Otherwise it will create an empty $domainShortName.
         |    * In either case, configuring storage means that OverflowDb will be stored to disk on shutdown (`close`).
         |    * I.e. if you want to preserve state between sessions, just use this method to instantiate the $domainShortName and ensure to properly `close` it at the end.
         |    * @param path to the storage file, e.g. /home/user1/overflowdb.bin
         |    */
         |  def withStorage(path: Path): $domainShortName =
         |    withConfig(Config.withoutOverflow.withStorageLocation(path))
         |
         |  def withStorage(path: String): $domainShortName =
         |    withStorage(Paths.get(path))
         |
         |  def withConfig(config: overflowdb.Config): $domainShortName =
         |    new $domainShortName(
         |      Graph.open(config, nodes.Factories.allAsJava, edges.Factories.allAsJava, convertPropertyForPersistence))
         |
         |  def emptyGraph: Graph =
         |    Graph.open(Config.withoutOverflow, nodes.Factories.allAsJava, edges.Factories.allAsJava, convertPropertyForPersistence)
         |
         |  def convertPropertyForPersistence(property: Any): Any =
         |    property match {
         |      case arraySeq: scala.collection.immutable.ArraySeq[_] => arraySeq.unsafeArray
         |      case coll: IterableOnce[Any] => asJava(coll.iterator.toArray)
         |      case other => other
         |    }
         |
         |}
         |
         |
         |/**
         |  * Domain-specific wrapper for graph, starting point for traversals.
         |  * @param graph the underlying graph. An empty graph is created if this parameter is omitted.
         |  */
         |class $domainShortName(private val _graph: Graph = $domainShortName.emptyGraph) extends AutoCloseable {
         |  def graph: Graph = _graph
         |
         |  def help(implicit searchPackageNames: DocSearchPackages): String =
         |    new TraversalHelp(searchPackageNames).forTraversalSources
         |
         |  override def close(): Unit =
         |    graph.close
         |
         |  override def toString(): String =
         |    String.format("$domainShortName (%s)", graph)
         |}
         |
         |""".stripMargin
    )
    results.append(domainMain)

    results.toSeq
  }

  protected def writeConstants(outputDir: File): Seq[File] = {
    val results = mutable.Buffer.empty[File]
    val baseDir = outputDir / basePackage.replaceAll("\\.", "/")
    baseDir.createDirectories()

    def writeConstantsFile(className: String, constants: Seq[ConstantContext]): Unit = {
      val constantsSource = constants.map { constant =>
        val documentation = constant.documentation.filter(_.nonEmpty).map(comment => s"""/** $comment */""").getOrElse("")
        s"""$documentation
           |${constant.source}
           |""".stripMargin
      }.mkString(lineSeparator)
      val allConstantsSetType = if (constantsSource.contains("PropertyKey")) "PropertyKey" else "String"
      val allConstantsBody = constants.map { constant =>
        s"add(${constant.name});"
      }.mkString(lineSeparator)
      val allConstantsSet =
        s"""
           |public static Set<$allConstantsSetType> ALL = new HashSet<$allConstantsSetType>() {{
           |$allConstantsBody
           |}};
           |""".stripMargin
      val file = baseDir.createChild(s"$className.java").write(
        s"""package $basePackage;
           |
           |import overflowdb.*;
           |
           |import java.util.Collection;
           |import java.util.HashSet;
           |import java.util.Set;
           |
           |public class $className {
           |
           |$constantsSource
           |$allConstantsSet
           |}""".stripMargin
      )
      results.append(file)
    }

    writeConstantsFile("PropertyNames", schema.properties.map { property =>
      ConstantContext(property.name, s"""public static final String ${property.name} = "${property.name}";""", property.comment)
    })
    writeConstantsFile("NodeTypes", schema.nodeTypes.map { nodeType =>
      ConstantContext(nodeType.name, s"""public static final String ${nodeType.name} = "${nodeType.name}";""", nodeType.comment)
    })
    writeConstantsFile("EdgeTypes", schema.edgeTypes.map { edgeType =>
      ConstantContext(edgeType.name, s"""public static final String ${edgeType.name} = "${edgeType.name}";""", edgeType.comment)
    })
    schema.constantsByCategory.foreach { case (category, constants) =>
      writeConstantsFile(category, constants.map { constant =>
        ConstantContext(constant.name, s"""public static final String ${constant.name} = "${constant.value}";""", constant.comment)
      })
    }

    writeConstantsFile("Properties", schema.properties.map { property =>
      val src = {
        val valueType = typeFor(property)
        val cardinality = property.cardinality
        import Property.Cardinality
        val completeType = cardinality match {
          case Cardinality.One(_) => valueType
          case Cardinality.ZeroOrOne => valueType
          case Cardinality.List => s"scala.collection.IndexedSeq<$valueType>"
        }
        s"""public static final overflowdb.PropertyKey<$completeType> ${property.name} = new overflowdb.PropertyKey<>("${property.name}");"""
      }
      ConstantContext(property.name, src, property.comment)
    })

    results.toSeq
  }

  protected def writeEdgeFiles(outputDir: File): Seq[File] = {
    val staticHeader =
      s"""package $edgesPackage
         |
         |import overflowdb._
         |import scala.jdk.CollectionConverters._
         |""".stripMargin

    val packageObject = {
      val factories = {
        val edgeFactories = schema.edgeTypes.map(edgeType => edgeType.className + ".factory").mkString(", ")
        s"""object Factories {
           |  lazy val all: Seq[EdgeFactory[_]] = Seq($edgeFactories)
           |  lazy val allAsJava: java.util.List[EdgeFactory[_]] = all.asJava
           |}
           |""".stripMargin
      }

      s"""$staticHeader
         |$propertyErrorRegisterImpl
         |$factories
         |""".stripMargin
    }

    def generateEdgeSource(edgeType: EdgeType, properties: Seq[Property[_]]) = {
      val edgeClassName = edgeType.className

      val propertyNames = properties.map(_.className)

      val propertyNameDefs = properties.map { p =>
        s"""val ${p.className} = "${p.name}" """
      }.mkString(lineSeparator)

      val propertyDefinitions = properties.map { p =>
        propertyKeyDef(p.name, typeFor(p), p.cardinality)
      }.mkString(lineSeparator)

      val companionObject =
        s"""object $edgeClassName {
           |  val Label = "${edgeType.name}"
           |
           |  object PropertyNames {
           |    $propertyNameDefs
           |    val all: Set[String] = Set(${propertyNames.mkString(", ")})
           |    val allAsJava: java.util.Set[String] = all.asJava
           |  }
           |
           |  object Properties {
           |    $propertyDefinitions
           |  }
           |
           |  object PropertyDefaults {
           |    ${propertyDefaultCases(properties)}
           |  }
           |
           |  val layoutInformation = new EdgeLayoutInformation(Label, PropertyNames.allAsJava)
           |
           |  val factory = new EdgeFactory[$edgeClassName] {
           |    override val forLabel = $edgeClassName.Label
           |
           |    override def createEdge(graph: Graph, outNode: NodeRef[NodeDb], inNode: NodeRef[NodeDb]) =
           |      new $edgeClassName(graph, outNode, inNode)
           |  }
           |}
           |""".stripMargin

      def propertyBasedFieldAccessors(properties: Seq[Property[_]]): String = {
        import Property.Cardinality
        properties.map { property =>
          val name = property.name
          val nameCamelCase = camelCase(name)
          val tpe = getCompleteType(property)

          property.cardinality match {
            case Cardinality.One(_) =>
              s"""def $nameCamelCase: $tpe = property("$name").asInstanceOf[$tpe]"""
            case Cardinality.ZeroOrOne =>
              s"""def $nameCamelCase: $tpe = Option(property("$name")).asInstanceOf[$tpe]""".stripMargin
            case Cardinality.List =>
              val returnType = s"IndexedSeq[${typeFor(property)}]"
              s"""def $nameCamelCase: $tpe = {
                 |  property("$name") match {
                 |    case null => collection.immutable.ArraySeq.empty
                 |    case arr: Array[_] if arr.isEmpty => collection.immutable.ArraySeq.empty
                 |    case arr: Array[_] => scala.collection.immutable.ArraySeq.unsafeWrapArray(arr).asInstanceOf[$returnType]
                 |    case iterable: IterableOnce[_] => iterable.iterator.to(IndexedSeq).asInstanceOf[$returnType]
                 |    case jList: java.util.List[_] => jList.asScala.to(IndexedSeq).asInstanceOf[$returnType]
                 |  }
                 |}""".stripMargin
          }
        }.mkString(lineSeparator)
      }

      val propertyDefaultValues = propertyDefaultValueImpl(s"$edgeClassName.PropertyDefaults", properties)
      val classImpl =
        s"""class $edgeClassName(_graph: Graph, _outNode: NodeRef[NodeDb], _inNode: NodeRef[NodeDb])
           |extends Edge(_graph, $edgeClassName.Label, _outNode, _inNode, $edgeClassName.PropertyNames.allAsJava) {
           |
           |  ${propertyBasedFieldAccessors(properties)}
           |
           |  $propertyDefaultValues
           |
           |}
           |""".stripMargin

      s"""$staticHeader
         |$companionObject
         |$classImpl
         |""".stripMargin
    }

    val baseDir = outputDir / edgesPackage.replaceAll("\\.", "/")
    if (baseDir.exists) baseDir.delete()
    baseDir.createDirectories()
    val pkgObjFile = baseDir.createChild("package.scala").write(packageObject)
    val edgeTypeFiles = schema.edgeTypes.map { edge =>
      val src = generateEdgeSource(edge, edge.properties)
      val srcFile = edge.className + ".scala"
      baseDir.createChild(srcFile).write(src)
    }
    pkgObjFile +: edgeTypeFiles
  }

  protected def neighborAccessorNameForEdge(edge: EdgeType, direction: Direction.Value): String =
    camelCase(edge.name + "_" + direction)

  protected def writeNodeFiles(outputDir: File): Seq[File] = {
    val rootTypeImpl = {
      val genericNeighborAccessors = for {
        direction <- Direction.all
        edgeType <- schema.edgeTypes
        accessor = neighborAccessorNameForEdge(edgeType, direction)
      } yield s"def _$accessor: java.util.Iterator[StoredNode] = { java.util.Collections.emptyIterator() }"

      val markerTraits =
        schema.allNodeTypes
          .flatMap(_.markerTraits)
          .distinct
          .map { case MarkerTrait(name) => s"trait $name" }
          .sorted
          .mkString(lineSeparator)

      val keyBasedTraits =
        schema.nodeProperties.map { property =>
          val camelCaseName = camelCase(property.name)
          val tpe = getCompleteType(property)
          val traitName = s"Has${property.className}"
          s"""trait $traitName {
             |  def $camelCaseName: $tpe
             |}
             |""".stripMargin

        }.mkString(lineSeparator)

      val factories = {
        val nodeFactories =
          schema.nodeTypes.map(nodeType => nodeType.className + ".factory").mkString(", ")
        s"""object Factories {
           |  lazy val all: Seq[NodeFactory[_]] = Seq($nodeFactories)
           |  lazy val allAsJava: java.util.List[NodeFactory[_]] = all.asJava
           |}
           |""".stripMargin
      }
      val reChars = "[](){}*+&|?.,\\\\$"
      s"""package $nodesPackage
         |
         |import overflowdb._
         |import scala.jdk.CollectionConverters._
         |
         |$propertyErrorRegisterImpl
         |
         |object Misc {
         |  val reChars = "$reChars"
         |  def isRegex(pattern: String): Boolean = pattern.exists(reChars.contains(_))
         |}
         |
         |/** Abstract supertype for overflowdb.Node and NewNode */
         |trait AbstractNode extends overflowdb.NodeOrDetachedNode {
         |  def label: String
         |}
         |
         |/* A node that is stored inside an Graph (rather than e.g. DiffGraph) */
         |trait StoredNode extends Node with AbstractNode with Product {
         |  /* underlying Node in the graph.
         |   * since this is a StoredNode, this is always set */
         |  def underlying: Node = this
         |
         |  /** labels of product elements, used e.g. for pretty-printing */
         |  def productElementLabel(n: Int): String
         |
         |  /* all properties plus label and id */
         |  def toMap: Map[String, Any] = {
         |    val map = propertiesMap()
         |    map.put("_label", label)
         |    map.put("_id", id: java.lang.Long)
         |    map.asScala.toMap
         |  }
         |
         |  /*Sets fields from newNode*/
         |  def fromNewNode(newNode: NewNode, mapping: NewNode => StoredNode):Unit = ???
         |
         |  ${genericNeighborAccessors.mkString(lineSeparator)}
         |}
         |
         |  $keyBasedTraits
         |  $markerTraits
         |
         |  $factories
         |""".stripMargin
    }

    val staticHeader =
      s"""package $nodesPackage
         |
         |import overflowdb._
         |import scala.jdk.CollectionConverters._
         |""".stripMargin

    def generateNodeBaseTypeSource(nodeBaseType: NodeBaseType): String = {
      val className = nodeBaseType.className
      val properties = nodeBaseType.properties

      val mixinsForPropertyAccessorsReadOnly = nodeBaseType.properties.map { property =>
        s"with Has${property.className}"
      }.mkString(" ")

      val mixinsForBaseTypes = nodeBaseType.extendz.map { baseTrait =>
        s"with ${baseTrait.className}"
      }.mkString(" ")

      val mixinForBaseTypesNew = nodeBaseType.extendz.map { baseTrait =>
        s"with ${baseTrait.className}New"
      }.mkString(" ")

      val mixinsForBaseTypes2 = nodeBaseType.extendz.map { baseTrait =>
        s"with ${baseTrait.className}Base"
      }.mkString(" ")

      val mixinsForMarkerTraits = nodeBaseType.markerTraits.map { case MarkerTrait(name) =>
        s"with $name"
      }.mkString(" ")

      def abstractEdgeAccessors(nodeBaseType: NodeBaseType, direction: Direction.Value) = {
        nodeBaseType.edges(direction).groupBy(_.viaEdge).map { case (edge, neighbors) =>
          val edgeAccessorName = neighborAccessorNameForEdge(edge, direction)
          /** TODO bring this back, but not as direct accessors on the type, but via extension methods
            * context: in complex schema hierarchies, type inheritance between base nodes
            * and nodes can lead to very convoluted accessor types. E.g. in TestSchema03c in the integration tests:
            *
            * AbstractNode1.edge1In: Traversal[AbstractNode1]
            * Node1.edge1In:         Traversal[AbstractNode1]
            * Node2.edge1In:         Traversal[NodeExt]
            *
            * which is technically correct based on the schema, but the JVM doesn't allow this because
            * both Node1 and Node2 extend AbstractNode, and therefor Node2.edge1In doesn't compile
            */
//          val neighborNodesType = {
//            val subtypesWithSameEdgeAndDirection =
//              nodeBaseType.subtypes(schema.allNodeTypes.toSet)
//                .flatMap(_.edges(direction).filter(_.viaEdge == edge))
//
//            val relevantNeighbors = (neighbors ++ subtypesWithSameEdgeAndDirection).map(_.neighbor).toSet
//            deriveCommonRootType(relevantNeighbors)
//          }
          val neighborNodesType = "_ <: StoredNode"
          val genericEdgeAccessor = s"def $edgeAccessorName: overflowdb.traversal.Traversal[$neighborNodesType]"

          val specificNodeAccessors = neighbors.flatMap { adjacentNode =>
            val neighbor = adjacentNode.neighbor
            val entireNodeHierarchy: Set[AbstractNodeType] = neighbor.subtypes(schema.allNodeTypes.toSet) ++ (neighbor.extendzRecursively :+ neighbor)
            entireNodeHierarchy.map { neighbor =>
              val accessorName = adjacentNode.customStepName.getOrElse(
                s"_${camelCase(neighbor.name)}Via${edge.className.capitalize}${camelCaseCaps(direction.toString)}"
              )
              val accessorImpl0 = s"$edgeAccessorName.collectAll[${neighbor.className}]"
              val cardinality = adjacentNode.cardinality
              val accessorImpl1 = cardinality match {
                case EdgeType.Cardinality.One =>
                  s"""try { $accessorImpl0.next() } catch {
                     |  case e: java.util.NoSuchElementException =>
                     |    throw new overflowdb.SchemaViolationException("$direction edge with label ${adjacentNode.viaEdge.name} to an adjacent ${neighbor.name} is mandatory, but not defined for this ${nodeBaseType.name} node with id=" + id, e)
                     |}""".stripMargin
                case EdgeType.Cardinality.ZeroOrOne => s"$accessorImpl0.nextOption()"
                case _ => accessorImpl0
              }

              s"""/** ${adjacentNode.customStepDoc.getOrElse("")}
                 |  * Traverse to ${neighbor.name} via ${adjacentNode.viaEdge.name} $direction edge.
                 |  */ ${docAnnotationMaybe(adjacentNode.customStepDoc)}
                 |def $accessorName: ${fullScalaType(neighbor, cardinality)} =
                 |  $accessorImpl1
                 |  """.stripMargin
            }
          }.distinct.mkString(lineSeparator)

          s"""$genericEdgeAccessor
             |
             |$specificNodeAccessors""".stripMargin
        }.mkString(lineSeparator)
      }

      val companionObject = {
        val propertyNames = nodeBaseType.properties.map(_.name)
        val propertyNameDefs = propertyNames.map { name =>
          s"""val ${camelCaseCaps(name)} = "$name" """
        }.mkString(lineSeparator)

        val propertyDefinitions = properties.map { p =>
          propertyKeyDef(p.name, typeFor(p), p.cardinality)
        }.mkString(lineSeparator)

        val Seq(outEdgeNames, inEdgeNames) =
          Seq(nodeBaseType.outEdges, nodeBaseType.inEdges).map { edges =>
            edges.map(_.viaEdge.name).sorted.map(quote).mkString(",")
          }

        s"""object $className {
           |  object PropertyNames {
           |    $propertyNameDefs
           |    val all: Set[String] = Set(${propertyNames.map(camelCaseCaps).mkString(", ")})
           |  }
           |
           |  object Properties {
           |    $propertyDefinitions
           |  }
           |
           |  object PropertyDefaults {
           |    ${propertyDefaultCases(properties)}
           |  }
           |
           |  object Edges {
           |    val Out: Array[String] = Array($outEdgeNames)
           |    val In: Array[String] = Array($inEdgeNames)
           |  }
           |
           |}""".stripMargin
      }

      val newNodePropertySetters = nodeBaseType.properties.map { property =>
        val camelCaseName = camelCase(property.name)
        val tpe = getCompleteType(property)
        s"def ${camelCaseName}_=(value: $tpe): Unit"
      }.mkString(lineSeparator)

      s"""package $nodesPackage
         |
         |$companionObject
         |
         |trait ${className}Base extends AbstractNode
         |$mixinsForPropertyAccessorsReadOnly
         |$mixinsForBaseTypes2
         |$mixinsForMarkerTraits
         |
         |trait ${className}New extends NewNode $mixinForBaseTypesNew $mixinsForPropertyAccessorsReadOnly {
         |  $newNodePropertySetters
         |}
         |
         |trait $className extends StoredNode with ${className}Base
         |$mixinsForBaseTypes {
         |${abstractEdgeAccessors(nodeBaseType, Direction.OUT)}
         |${abstractEdgeAccessors(nodeBaseType, Direction.IN)}
         |}""".stripMargin
    }

    def generateNodeSource(nodeType: NodeType) = {
      val properties = nodeType.properties

      val propertyNames = (properties.map(_.name) ++ nodeType.containedNodes.map(_.localName)).distinct
      val propertyNameDefs = propertyNames.map { name =>
        s"""val ${camelCaseCaps(name)} = "$name" """
      }.mkString(lineSeparator)

      val propertyDefs = properties.map { p =>
        propertyKeyDef(p.name, typeFor(p), p.cardinality)
      }.mkString(lineSeparator)

      val propertyDefsForContainedNodes = nodeType.containedNodes.map { containedNode =>
        propertyKeyDef(containedNode.localName, containedNode.classNameForStoredNode, containedNode.cardinality)
      }.mkString(lineSeparator)

      val (neighborOutInfos, neighborInInfos) = {
        /** the offsetPos determines the index into the adjacent nodes array of a given node type
          * assigning numbers here must follow the same way as in NodeLayoutInformation, i.e. starting at 0,
          * first assign ids to the outEdges based on their order in the list, and then the same for inEdges */
        var _currOffsetPos = -1
        def nextOffsetPos: Int = { _currOffsetPos += 1; _currOffsetPos }

        case class AjacentNodeWithInheritanceStatus(adjacentNode: AdjacentNode, isInherited: Boolean)

        /** For all adjacent nodes, figure out if they are inherited or not.
          *  Later on, we will create edge accessors for all inherited neighbors, but only create the node accessors
          *  on the base types (if they are defined there). Note: they may even be inherited with a different cardinality */
        def adjacentNodesWithInheritanceStatus(adjacentNodes: AbstractNodeType => Seq[AdjacentNode]): Seq[AjacentNodeWithInheritanceStatus] = {
          val inherited = nodeType.extendzRecursively
            .flatMap(adjacentNodes)
            .map(AjacentNodeWithInheritanceStatus(_, true))

          // only edge and neighbor node matter, not the cardinality
          val inheritedLookup: Set[(EdgeType, AbstractNodeType)] =
            inherited.map(_.adjacentNode).map { adjacentNode => (adjacentNode.viaEdge, adjacentNode.neighbor) }.toSet

          val direct = adjacentNodes(nodeType).map { adjacentNode =>
            val isInherited = inheritedLookup.contains((adjacentNode.viaEdge, adjacentNode.neighbor))
            AjacentNodeWithInheritanceStatus(adjacentNode, isInherited)
          }
          (direct ++ inherited).distinct
        }

        def createNeighborInfos(neighborContexts: Seq[AjacentNodeWithInheritanceStatus], direction: Direction.Value): Seq[NeighborInfoForEdge] = {
          neighborContexts.groupBy(_.adjacentNode.viaEdge).map { case (edge, neighborContexts) =>
            val neighborInfoForNodes = neighborContexts.map { case AjacentNodeWithInheritanceStatus(adjacentNode, isInherited) =>
              NeighborInfoForNode(adjacentNode.neighbor, edge, direction, adjacentNode.cardinality, isInherited, adjacentNode.customStepName, adjacentNode.customStepDoc)
            }
            NeighborInfoForEdge(edge, neighborInfoForNodes, nextOffsetPos)
          }.toSeq
        }

        val neighborOutInfos = createNeighborInfos(adjacentNodesWithInheritanceStatus(_.outEdges), Direction.OUT)
        val neighborInInfos = createNeighborInfos(adjacentNodesWithInheritanceStatus(_.inEdges), Direction.IN)
        (neighborOutInfos, neighborInInfos)
      }

      val neighborInfos: Seq[(NeighborInfoForEdge, Direction.Value)] =
        neighborOutInfos.map((_, Direction.OUT)) ++ neighborInInfos.map((_, Direction.IN))

      def toLayoutInformationEntry(neighborInfos: Seq[NeighborInfoForEdge]): String = {
        neighborInfos.sortBy(_.offsetPosition).map { neighborInfo =>
          val edgeClass = neighborInfo.edge.className
          s"$edgesPackage.$edgeClass.layoutInformation"
        }.mkString(s",$lineSeparator")
      }
      val List(outEdgeLayouts, inEdgeLayouts) = List(neighborOutInfos, neighborInInfos).map(toLayoutInformationEntry)

      val className = nodeType.className
      val classNameDb = nodeType.classNameDb

      val companionObject =
        s"""object $className {
           |  def apply(graph: Graph, id: Long) = new $className(graph, id)
           |
           |  val Label = "${nodeType.name}"
           |
           |  object PropertyNames {
           |    $propertyNameDefs
           |    val all: Set[String] = Set(${propertyNames.map(camelCaseCaps).mkString(", ")})
           |    val allAsJava: java.util.Set[String] = all.asJava
           |  }
           |
           |  object Properties {
           |    $propertyDefs
           |    $propertyDefsForContainedNodes
           |  }
           |
           |  object PropertyDefaults {
           |    ${propertyDefaultCases(properties)}
           |  }
           |
           |  val layoutInformation = new NodeLayoutInformation(
           |    Label,
           |    PropertyNames.allAsJava,
           |    List($outEdgeLayouts).asJava,
           |    List($inEdgeLayouts).asJava)
           |
           |
           |  object Edges {
           |    val Out: Array[String] = Array(${quoted(neighborOutInfos.map(_.edge.name).sorted).mkString(",")})
           |    val In: Array[String] = Array(${quoted(neighborInInfos.map(_.edge.name).sorted).mkString(",")})
           |  }
           |
           |  val factory = new NodeFactory[$classNameDb] {
           |    override val forLabel = $className.Label
           |
           |    override def createNode(ref: NodeRef[$classNameDb]) =
           |      new $classNameDb(ref.asInstanceOf[NodeRef[NodeDb]])
           |
           |    override def createNodeRef(graph: Graph, id: Long) = $className(graph, id)
           |  }
           |}
           |""".stripMargin

      val mixinsForExtendedNodes: String =
        nodeType.extendz.map { traitName =>
          s"with ${traitName.className}"
        }.mkString(" ")

      val mixinsForExtendedNodesBase: String =
        nodeType.extendz.map { traitName =>
          s"with ${traitName.className}Base"
        }.mkString(" ")

      val mixinsForMarkerTraits: String =
        nodeType.markerTraits.map { case MarkerTrait(name) =>
          s"with $name"
        }.mkString(" ")

      val propertyBasedTraits = properties.map(p => s"with Has${p.className}").mkString(" ")

      val propertiesMapImpl = {
        import Property.Cardinality
        val putKeysImpl = properties.map { key =>
          val memberName = camelCase(key.name)
          key.cardinality match {
            case Cardinality.One(_) =>
              s"""properties.put("${key.name}", $memberName)"""
            case Cardinality.ZeroOrOne =>
              s"""$memberName.map { value => properties.put("${key.name}", value) }"""
            case Cardinality.List =>
              s"""if (this._$memberName != null && this._$memberName.nonEmpty) { properties.put("${key.name}", $memberName) }"""
          }
        }.mkString(lineSeparator)

        val putContainedNodesImpl = {
          nodeType.containedNodes.map { cnt =>
            val memberName = cnt.localName
            cnt.cardinality match {
              case Cardinality.One(_) =>
                s"""properties.put("$memberName", this._$memberName)"""
              case Cardinality.ZeroOrOne =>
                s"""$memberName.map { value => properties.put("$memberName", value) }"""
              case Cardinality.List =>
                s"""if (this._$memberName != null && this._$memberName.nonEmpty) { properties.put("$memberName", this.$memberName) }"""
            }
          }
        }.mkString(lineSeparator)

        s"""{
           |  val properties = new java.util.HashMap[String, Any]
           |  $putKeysImpl
           |  $putContainedNodesImpl
           |  properties
           |}""".stripMargin
      }

      val propertiesMapForStorageImpl = {
        import Property.Cardinality
        val putKeysImpl = properties.map { key =>
          val memberName = camelCase(key.name)
          key.cardinality match {
            case Cardinality.One(default) =>
              val isDefaultValueImpl = defaultValueCheckImpl(memberName, default)
              s"""if (!($isDefaultValueImpl)) { properties.put("${key.name}", $memberName) }"""
            case Cardinality.ZeroOrOne =>
              s"""$memberName.map { value => properties.put("${key.name}", value) }"""
            case Cardinality.List =>
              s"""if (this._$memberName != null && this._$memberName.nonEmpty) { properties.put("${key.name}", $memberName) }"""
          }
        }.mkString(lineSeparator)

        val putContainedNodesImpl = {
          nodeType.containedNodes.map { cnt =>
            val memberName = cnt.localName
            cnt.cardinality match {
              case Cardinality.One(default) =>
                val isDefaultValueImpl = defaultValueCheckImpl(s"this._$memberName", default)
                s"""if (!($isDefaultValueImpl)) { properties.put("$memberName", this._$memberName) }"""
              case Cardinality.ZeroOrOne =>
                s"""$memberName.map { value => properties.put("$memberName", value) }"""
              case Cardinality.List =>
                s"""if (this._$memberName != null && this._$memberName.nonEmpty) { properties.put("$memberName", this.$memberName) }"""
            }
          }
        }.mkString(lineSeparator)


        s"""{
           |  val properties = new java.util.HashMap[String, Any]
           |  $putKeysImpl
           |  $putContainedNodesImpl
           |  properties
           |}""".stripMargin
      }

      val fromNew = {
        val newNodeCasted = s"newNode.asInstanceOf[New${nodeType.className}]"

        val initKeysImpl = {
          val lines = properties.map { key =>
            import Property.Cardinality
            val memberName = camelCase(key.name)
            key.cardinality match {
              case Cardinality.One(_) =>
                s"""this._$memberName = $newNodeCasted.$memberName""".stripMargin
              case Cardinality.ZeroOrOne =>
                s"""this._$memberName = $newNodeCasted.$memberName.orNull""".stripMargin
              case Cardinality.List =>
                s"""this._$memberName = if ($newNodeCasted.$memberName != null) $newNodeCasted.$memberName else collection.immutable.ArraySeq.empty""".stripMargin
            }
          }
          lines.mkString(lineSeparator)
        }

        val initRefsImpl = {
          nodeType.containedNodes.map { containedNode =>
            import Property.Cardinality
            val memberName = containedNode.localName
            val containedNodeType = containedNode.classNameForStoredNode

            containedNode.cardinality match {
              case Cardinality.One(_) =>
                s"""this._$memberName = $newNodeCasted.$memberName match {
                   |  case null => null
                   |  case newNode: NewNode => mapping(newNode).asInstanceOf[$containedNodeType]
                   |  case oldNode: StoredNode => oldNode.asInstanceOf[$containedNodeType]
                   |  case _ => throw new MatchError("unreachable")
                   |}""".stripMargin
              case Cardinality.ZeroOrOne =>
                s"""this._$memberName = $newNodeCasted.$memberName match {
                   |  case null | None => null
                   |  case Some(newNode:NewNode) => mapping(newNode).asInstanceOf[$containedNodeType]
                   |  case Some(oldNode:StoredNode) => oldNode.asInstanceOf[$containedNodeType]
                   |  case _ => throw new MatchError("unreachable")
                   |}""".stripMargin
              case Cardinality.List =>
                s"""this._$memberName =
                   |  if ($newNodeCasted.$memberName == null || $newNodeCasted.$memberName.isEmpty) {
                   |    collection.immutable.ArraySeq.empty
                   |  } else {
                   |    collection.immutable.ArraySeq.unsafeWrapArray(
                   |      $newNodeCasted.$memberName.map {
                   |        case null => throw new NullPointerException("NullPointers forbidden in contained nodes")
                   |        case newNode:NewNode => mapping(newNode).asInstanceOf[$containedNodeType]
                   |        case oldNode:StoredNode => oldNode.asInstanceOf[$containedNodeType]
                   |        case _ => throw new MatchError("unreachable")
                   |      }.toArray
                   |    )
                   |  }
                   |""".stripMargin
            }
          }
        }.mkString(lineSeparator)

        val registerFullName = if(!properties.map{_.name}.contains("FULL_NAME")) "" else {
          s"""graph.indexManager.putIfIndexed("FULL_NAME", $newNodeCasted.fullName, this.ref)"""
        }

        s"""override def fromNewNode(newNode: NewNode, mapping: NewNode => StoredNode):Unit = {
           |  $initKeysImpl
           |  $initRefsImpl
           |  $registerFullName
           |}""".stripMargin
      }

      val containedNodesAsMembers =
        nodeType.containedNodes
          .map { containedNode =>
            import Property.Cardinality
            val containedNodeType = containedNode.classNameForStoredNode
            containedNode.cardinality match {
              case Cardinality.One(default) =>
                s"""
                   |private var _${containedNode.localName}: $containedNodeType = ${defaultValueImpl(default)}
                   |def ${containedNode.localName}: $containedNodeType = this._${containedNode.localName}
                   |""".stripMargin
              case Cardinality.ZeroOrOne =>
                s"""
                   |private var _${containedNode.localName}: $containedNodeType = null
                   |def ${containedNode.localName}: Option[$containedNodeType] = Option(this._${containedNode.localName})
                   |""".stripMargin
              case Cardinality.List =>
                s"""
                   |private var _${containedNode.localName}: IndexedSeq[$containedNodeType] = collection.immutable.ArraySeq.empty
                   |def ${containedNode.localName}: IndexedSeq[$containedNodeType] = this._${containedNode.localName}
                   |""".stripMargin
            }
          }
          .mkString(lineSeparator)

      val productElements: Seq[ProductElement] = {
        var currIndex = -1
        def nextIdx = { currIndex += 1; currIndex }
        val forId = ProductElement("id", "id", nextIdx)
        val forKeys = properties.map { key =>
          val name = camelCase(key.name)
          ProductElement(name, name, nextIdx)
        }
        val forContainedNodes = nodeType.containedNodes.map { containedNode =>
          ProductElement(
            containedNode.localName,
            containedNode.localName,
            nextIdx)
        }
        forId +: (forKeys ++ forContainedNodes)
      }

      val productElementLabels =
        productElements.map { case ProductElement(name, _, index) =>
          s"""case $index => "$name" """
        }.mkString(lineSeparator)

      val productElementAccessors =
        productElements.map { case ProductElement(_, accessorSrc, index) =>
          s"case $index => $accessorSrc"
        }.mkString(lineSeparator)

      val abstractContainedNodeAccessors = nodeType.containedNodes.map { containedNode =>
        s"""def ${containedNode.localName}: ${getCompleteType(containedNode)}"""
      }.mkString(lineSeparator)

      val delegatingContainedNodeAccessors = nodeType.containedNodes.map { containedNode =>
        import Property.Cardinality

        val tpe = containedNode.classNameForStoredNode
        val src = containedNode.cardinality match {
          case Cardinality.One(_) =>
            s"""def ${containedNode.localName}: $tpe = get().${containedNode.localName}"""
          case Cardinality.ZeroOrOne =>
            s"""def ${containedNode.localName}: Option[$tpe] = get().${containedNode.localName}"""
          case Cardinality.List =>
            s"""def ${containedNode.localName}: collection.immutable.IndexedSeq[$tpe] = get().${containedNode.localName}"""
        }

        s"""${docAnnotationMaybe(containedNode.comment)}
           |$src
           |""".stripMargin
      }.mkString(lineSeparator)

      val nodeBaseImpl =
        s"""trait ${className}Base extends AbstractNode $mixinsForExtendedNodesBase $mixinsForMarkerTraits $propertyBasedTraits {
           |  def asStored : StoredNode = this.asInstanceOf[StoredNode]
           |
           |  $abstractContainedNodeAccessors
           |}
           |""".stripMargin

      val neighborAccessorDelegators = neighborInfos.map { case (neighborInfo, direction) =>
        val edgeAccessorName = neighborAccessorNameForEdge(neighborInfo.edge, direction)
        val nodeDelegators = neighborInfo.nodeInfos.collect {
          case neighborNodeInfo if !neighborNodeInfo.isInherited =>
            val accessorNameForNode = accessorName(neighborNodeInfo)
            s"""/** ${neighborNodeInfo.customStepDoc.getOrElse("")}
               |  * Traverse to ${neighborNodeInfo.neighborNode.name} via ${neighborNodeInfo.edge.name} $direction edge.
               |  */  ${docAnnotationMaybe(neighborNodeInfo.customStepDoc)}
               |def $accessorNameForNode: ${neighborNodeInfo.returnType} = get().$accessorNameForNode""".stripMargin
        }.mkString(lineSeparator)

        s"""def $edgeAccessorName: overflowdb.traversal.Traversal[${neighborInfo.deriveNeighborNodeType}] = get().$edgeAccessorName
           |override def _$edgeAccessorName = get()._$edgeAccessorName
           |
           |$nodeDelegators
           |""".stripMargin
      }.mkString(lineSeparator)

      val nodeRefImpl = {
        val propertyDelegators = properties.map { key =>
          val name = camelCase(key.name)
          s"""override def $name: ${getCompleteType(key)} = get().$name"""
        }.mkString(lineSeparator)

        val propertyDefaultValues = propertyDefaultValueImpl(s"$className.PropertyDefaults", properties)

        s"""class $className(graph: Graph, id: Long) extends NodeRef[$classNameDb](graph, id)
           |  with ${className}Base
           |  with StoredNode
           |  $mixinsForExtendedNodes {
           |  $propertyDelegators
           |  $propertyDefaultValues
           |  $delegatingContainedNodeAccessors
           |    $neighborAccessorDelegators
           |
           |    override def fromNewNode(newNode: NewNode, mapping: NewNode => StoredNode): Unit = get().fromNewNode(newNode, mapping)
           |    override def canEqual(that: Any): Boolean = get.canEqual(that)
           |    override def label: String = {
           |      $className.Label
           |    }
           |
           |    override def productElementLabel(n: Int): String =
           |      n match {
           |        $productElementLabels
           |      }
           |
           |    override def productElement(n: Int): Any =
           |      n match {
           |        $productElementAccessors
           |      }
           |
           |    override def productPrefix = "$className"
           |    override def productArity = ${productElements.size}
           |}
           |""".stripMargin
      }

      val neighborAccessors = neighborInfos.map { case (neighborInfo, direction) =>
        val edgeAccessorName = neighborAccessorNameForEdge(neighborInfo.edge, direction)
        val neighborType = neighborInfo.deriveNeighborNodeType
        val offsetPosition = neighborInfo.offsetPosition

        val nodeAccessors = neighborInfo.nodeInfos.collect {
          case neighborNodeInfo if !neighborNodeInfo.isInherited =>
            val accessorImpl0 = s"$edgeAccessorName.collectAll[${neighborNodeInfo.neighborNode.className}]"
            val accessorImpl1 = neighborNodeInfo.consolidatedCardinality match {
              case EdgeType.Cardinality.One =>
                s"""try { $accessorImpl0.next() } catch {
                   |  case e: java.util.NoSuchElementException =>
                   |    throw new overflowdb.SchemaViolationException("$direction edge with label ${neighborNodeInfo.edge.name} to an adjacent ${neighborNodeInfo.neighborNode.name} is mandatory, but not defined for this ${nodeType.name} node with id=" + id, e)
                   |}""".stripMargin
              case EdgeType.Cardinality.ZeroOrOne => s"$accessorImpl0.nextOption()"
              case _ => accessorImpl0
            }
            s"def ${accessorName(neighborNodeInfo)}: ${neighborNodeInfo.returnType} = $accessorImpl1"
        }.mkString(lineSeparator)

        s"""def $edgeAccessorName: overflowdb.traversal.Traversal[$neighborType] = overflowdb.traversal.Traversal(createAdjacentNodeIteratorByOffSet[$neighborType]($offsetPosition))
           |override def _$edgeAccessorName = createAdjacentNodeIteratorByOffSet[StoredNode]($offsetPosition)
           |$nodeAccessors
           |""".stripMargin
      }.mkString(lineSeparator)

      val updateSpecificPropertyImpl: String = {
        import Property.Cardinality
        def caseEntry(name: String, accessorName: String, cardinality: Cardinality, baseType: String) = {
          val setter = cardinality match {
            case Cardinality.One(_) | Cardinality.ZeroOrOne =>
              s"value.asInstanceOf[$baseType]"
            case Cardinality.List =>
              s"""value match {
                 |  case null => collection.immutable.ArraySeq.empty
                 |  case singleValue: $baseType => collection.immutable.ArraySeq(singleValue)
                 |  case coll: IterableOnce[Any] if coll.iterator.isEmpty => collection.immutable.ArraySeq.empty
                 |  case arr: Array[_] if arr.isEmpty => collection.immutable.ArraySeq.empty
                 |  case arr: Array[_] => collection.immutable.ArraySeq.unsafeWrapArray(arr).asInstanceOf[IndexedSeq[$baseType]]
                 |  case jCollection: java.lang.Iterable[_]  =>
                 |    if (jCollection.iterator.hasNext) {
                 |      collection.immutable.ArraySeq.unsafeWrapArray(
                 |        jCollection.asInstanceOf[java.util.Collection[$baseType]].iterator.asScala.toArray)
                 |    } else collection.immutable.ArraySeq.empty
                 |  case iter: Iterable[_] =>
                 |    if(iter.nonEmpty) {
                 |      collection.immutable.ArraySeq.unsafeWrapArray(iter.asInstanceOf[Iterable[$baseType]].toArray)
                 |    } else collection.immutable.ArraySeq.empty
                 |}""".stripMargin
          }
          s"""|case "$name" => this._$accessorName = $setter"""
        }

        val forKeys = properties.map(p => caseEntry(p.name, camelCase(p.name), p.cardinality, typeFor(p))).mkString(lineSeparator)

        val forContainedNodes = nodeType.containedNodes.map(containedNode =>
          caseEntry(containedNode.localName, containedNode.localName, containedNode.cardinality, containedNode.classNameForStoredNode)
        ).mkString(lineSeparator)

        s"""override protected def updateSpecificProperty(key:String, value: Object): Unit = {
           |  key match {
           |  $forKeys
           |  $forContainedNodes
           |    case _ => PropertyErrorRegister.logPropertyErrorIfFirst(getClass, key)
           |  }
           |}""".stripMargin
      }

      val propertyImpl: String = {
        val forKeys = properties.map(key =>
          s"""|      case "${key.name}" => this._${camelCase(key.name)}"""
        ).mkString(lineSeparator)

        val forContainedKeys = nodeType.containedNodes.map{containedNode =>
          val name = containedNode.localName
          s"""|      case "$name" => this._$name"""
        }.mkString(lineSeparator)

        s"""override def property(key:String): Any = {
           |  key match {
           |    $forKeys
           |    $forContainedKeys
           |    case _ => null
           |  }
           |}""".stripMargin
      }

      def propertyBasedFields(properties: Seq[Property[_]]): String = {
        import Property.Cardinality
        properties.map { property =>
          val publicName = camelCase(property.name)
          val fieldName = s"_$publicName"
          val (publicType, tpeForField, fieldAccessor, defaultValue) = {
            val valueType = typeFor(property)
            property.cardinality match {
              case Cardinality.One(_)  =>
                (valueType, valueType, fieldName, s"$className.PropertyDefaults.${property.className}")
              case Cardinality.ZeroOrOne => (s"Option[$valueType]", valueType, s"Option($fieldName)", "null")
              case Cardinality.List   => (s"IndexedSeq[$valueType]", s"IndexedSeq[$valueType]", fieldName, "collection.immutable.ArraySeq.empty")
            }
          }

          s"""private var $fieldName: $tpeForField = $defaultValue
             |def $publicName: $publicType = $fieldAccessor""".stripMargin
        }.mkString(lineSeparator)
      }

      val classImpl =
        s"""class $classNameDb(ref: NodeRef[NodeDb]) extends NodeDb(ref) with StoredNode
           |  $mixinsForExtendedNodes with ${className}Base {
           |
           |  override def layoutInformation: NodeLayoutInformation = $className.layoutInformation
           |
           |  ${propertyBasedFields(properties)}
           |
           |  $containedNodesAsMembers
           |
           |  /** faster than the default implementation */
           |  override def propertiesMap: java.util.Map[String, Any] =
           |    $propertiesMapImpl
           |
           |  /** faster than the default implementation */
           |  override def propertiesMapForStorage: java.util.Map[String, Any] =
           |    $propertiesMapForStorageImpl
           |
           |
           |  $neighborAccessors
           |
           |  override def label: String = {
           |    $className.Label
           |  }
           |
           |  override def productElementLabel(n: Int): String =
           |    n match {
           |      $productElementLabels
           |    }
           |
           |  override def productElement(n: Int): Any =
           |    n match {
           |      $productElementAccessors
           |    }
           |
           |  override def productPrefix = "$className"
           |  override def productArity = ${productElements.size}
           |
           |  override def canEqual(that: Any): Boolean = that != null && that.isInstanceOf[$classNameDb]
           |
           |  $propertyImpl
           |
           |$updateSpecificPropertyImpl
           |
           |  override def removeSpecificProperty(key: String): Unit =
           |    this.updateSpecificProperty(key, null)
           |
           |override def _initializeFromDetached(data: overflowdb.DetachedNodeData, mapper: java.util.function.Function[overflowdb.DetachedNodeData, Node]) =
           |    fromNewNode(data.asInstanceOf[NewNode], nn=>mapper.apply(nn).asInstanceOf[StoredNode])
           |
           |  $fromNew
           |
           |}""".stripMargin

      s"""$staticHeader
         |$companionObject
         |$nodeBaseImpl
         |$nodeRefImpl
         |$classImpl
         |""".stripMargin
    }

    val results = mutable.Buffer.empty[File]
    val baseDir = outputDir / nodesPackage.replaceAll("\\.", "/")
    if (baseDir.exists) baseDir.delete()
    baseDir.createDirectories()
    results.append(baseDir.createChild("RootTypes.scala").write(rootTypeImpl))
    schema.nodeBaseTypes.foreach { nodeBaseTrait =>
      val src = generateNodeBaseTypeSource(nodeBaseTrait)
      val srcFile = nodeBaseTrait.className + ".scala"
      results.append(baseDir.createChild(srcFile).write(src))
    }
    schema.nodeTypes.foreach { nodeType =>
      val src = generateNodeSource(nodeType)
      val srcFile = nodeType.className + ".scala"
      results.append(baseDir.createChild(srcFile).write(src))
    }
    results.toSeq
  }

  protected def writeNodeTraversalFiles(outputDir: File): Seq[File] = {
    lazy val nodeTraversalImplicits = {
      def implicitForNodeType(name: String) = {
        val traversalName = s"${name}TraversalExtGen"
        s"implicit def to$traversalName[NodeType <: $name](trav: IterableOnce[NodeType]): ${traversalName}[NodeType] = new $traversalName(trav)"
      }

      val implicitsForNodeTraversals =
        schema.nodeTypes.map(_.className).sorted.map(implicitForNodeType).mkString(lineSeparator)

      val implicitsForNodeBaseTypeTraversals =
        schema.nodeBaseTypes.map(_.className).sorted.map(implicitForNodeType).mkString(lineSeparator)

      s"""package $traversalsPackage
         |
         |import $nodesPackage._
         |
         |trait NodeTraversalImplicits extends NodeBaseTypeTraversalImplicits {
         |  $implicitsForNodeTraversals
         |}
         |
         |// lower priority implicits for base types
         |trait NodeBaseTypeTraversalImplicits {
         |  $implicitsForNodeBaseTypeTraversals
         |}
         |""".stripMargin
    }

    def generateCustomStepNameTraversals(nodeType: AbstractNodeType): Seq[String] = {
      for {
        direction <- Seq(Direction.IN, Direction.OUT)
        AdjacentNode(viaEdge, neighbor, cardinality, Some(customStepName), customStepDoc) <- nodeType.edges(direction).sortBy(_.customStepName)
      } yield {
        val mapOrFlatMap = cardinality match {
          case Cardinality.One => "map"
          case Cardinality.ZeroOrOne | Cardinality.List => "flatMap"
        }
        s"""/** ${customStepDoc.getOrElse("")}
           |  * Traverse to ${neighbor.name} via ${viaEdge.name} $direction edge.
           |  */ ${docAnnotationMaybe(customStepDoc)}
           |def $customStepName: Traversal[${neighbor.className}] =
           |  traversal.$mapOrFlatMap(_.$customStepName)
           |""".stripMargin
      }
    }

    def generatePropertyTraversals(properties: Seq[Property[_]]): Seq[String] = {
      import Property.Cardinality
      properties.map { property =>
        val nameCamelCase = camelCase(property.name)
        val baseType = typeFor(property)
        val cardinality = property.cardinality

        val mapOrFlatMap = cardinality match {
          case Cardinality.One(_) => "map"
          case Cardinality.ZeroOrOne | Cardinality.List => "flatMap"
        }

        val filterStepsForSingleString =
          s"""/**
             |  * Traverse to nodes where the $nameCamelCase matches the regular expression `value`
             |  * */
             |def $nameCamelCase(pattern: $baseType): Traversal[NodeType] = {
             |  if(!Misc.isRegex(pattern)){
             |    ${nameCamelCase}Exact(pattern)
             |  } else {
             |    overflowdb.traversal.filter.StringPropertyFilter.regexp(traversal)(_.$nameCamelCase, pattern)
             |  }
             |}
             |
             |/**
             |  * Traverse to nodes where the $nameCamelCase matches at least one of the regular expressions in `values`
             |  * */
             |def $nameCamelCase(patterns: $baseType*): Traversal[NodeType] =
             |  overflowdb.traversal.filter.StringPropertyFilter.regexpMultiple(traversal)(_.$nameCamelCase, patterns)
             |
             |/**
             |  * Traverse to nodes where $nameCamelCase matches `value` exactly.
             |  * */
             |def ${nameCamelCase}Exact(value: $baseType): Traversal[NodeType] = {
             |  val fastResult = traversal match {
             |    case init: overflowdb.traversal.InitialTraversal[NodeType] => init.getByIndex("${property.name}", value).getOrElse(null)
             |    case _ => null
             |  }
             |  if(fastResult != null) fastResult
             |  else traversal.filter{node => node.${nameCamelCase} == value}
             |  }
             |
             |/**
             |  * Traverse to nodes where $nameCamelCase matches one of the elements in `values` exactly.
             |  * */
             |def ${nameCamelCase}Exact(values: $baseType*): Traversal[NodeType] = {
             |  //fixme: use the index
             |  val vset = values.to(Set)
             |  traversal.filter{node => vset.contains(node.${nameCamelCase})}
             |}
             |
             |/**
             |  * Traverse to nodes where $nameCamelCase does not match the regular expression `value`.
             |  * */
             |def ${nameCamelCase}Not(pattern: $baseType): Traversal[NodeType] = {
             |  if(!Misc.isRegex(pattern)){
             |    traversal.filter{node => node.${nameCamelCase} != pattern}
             |  } else {
             |    overflowdb.traversal.filter.StringPropertyFilter.regexpNot(traversal)(_.$nameCamelCase, pattern)
             |  }
             |}
             |
             |/**
             |  * Traverse to nodes where $nameCamelCase does not match any of the regular expressions in `values`.
             |  * */
             |def ${nameCamelCase}Not(patterns: $baseType*): Traversal[NodeType] = {
             |    overflowdb.traversal.filter.StringPropertyFilter.regexpNotMultiple(traversal)(_.$nameCamelCase, patterns)
             | }
             |""".stripMargin

        val filterStepsForOptionalString =
          s"""/**
             |  * Traverse to nodes where the $nameCamelCase matches the regular expression `value`
             |  * */
             |def $nameCamelCase(pattern: $baseType): Traversal[NodeType] = {
             |  if(!Misc.isRegex(pattern)){
             |    traversal.filter{node => node.$nameCamelCase.isDefined && node.${nameCamelCase}.get == pattern}
             |  } else {
             |    overflowdb.traversal.filter.StringPropertyFilter.regexp(traversal.filter(_.$nameCamelCase.isDefined))(_.$nameCamelCase.get, pattern)
             |  }
             |}
             |
             |/**
             |  * Traverse to nodes where the $nameCamelCase matches at least one of the regular expressions in `values`
             |  * */
             |def $nameCamelCase(patterns: $baseType*): Traversal[NodeType] = {
             |  overflowdb.traversal.filter.StringPropertyFilter.regexpMultiple(traversal.filter(_.$nameCamelCase.isDefined))(_.$nameCamelCase.get, patterns)
             |}
             |
             |/**
             |  * Traverse to nodes where $nameCamelCase matches `value` exactly.
             |  * */
             |def ${nameCamelCase}Exact(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{node => node.$nameCamelCase.isDefined && node.${nameCamelCase}.get == value}
             |
             |/**
             |  * Traverse to nodes where $nameCamelCase matches one of the elements in `values` exactly.
             |  * */
             |def ${nameCamelCase}Exact(values: $baseType*): Traversal[NodeType] = {
             |  val vset = values.to(Set)
             |  traversal.filter{node => node.$nameCamelCase.isDefined && vset.contains(node.${nameCamelCase}.get)}
             |}
             |
             |/**
             |  * Traverse to nodes where $nameCamelCase does not match the regular expression `value`.
             |  * */
             |def ${nameCamelCase}Not(pattern: $baseType): Traversal[NodeType] = {
             |  if(!Misc.isRegex(pattern)){
             |    traversal.filter{node => node.$nameCamelCase.isEmpty || node.${nameCamelCase}.get != pattern}
             |  } else {
             |    overflowdb.traversal.filter.StringPropertyFilter.regexpNot(traversal.filter(_.$nameCamelCase.isDefined))(_.$nameCamelCase.get, pattern)
             |  }
             |}
             |
             |/**
             |  * Traverse to nodes where $nameCamelCase does not match any of the regular expressions in `values`.
             |  * */
             |def ${nameCamelCase}Not(patterns: $baseType*): Traversal[NodeType] = {
             |  overflowdb.traversal.filter.StringPropertyFilter.regexpNotMultiple(traversal.filter(_.$nameCamelCase.isDefined))(_.$nameCamelCase.get, patterns)
             | }
             |""".stripMargin

        val filterStepsForSingleBoolean =
          s"""/**
             |  * Traverse to nodes where the $nameCamelCase equals the given `value`
             |  * */
             |def $nameCamelCase(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{_.$nameCamelCase == value}
             |
             |/**
             |  * Traverse to nodes where $nameCamelCase is not equal to the given `value`.
             |  * */
             |def ${nameCamelCase}Not(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{_.$nameCamelCase != value}
             |""".stripMargin

        val filterStepsForOptionalBoolean =
          s"""/**
             |  * Traverse to nodes where the $nameCamelCase equals the given `value`
             |  * */
             |def $nameCamelCase(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{node => node.${nameCamelCase}.isDefined && node.$nameCamelCase.get == value}
             |
             |/**
             |  * Traverse to nodes where $nameCamelCase is not equal to the given `value`.
             |  * */
             |def ${nameCamelCase}Not(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{node => !node.${nameCamelCase}.isDefined || node.$nameCamelCase.get == value}
             |""".stripMargin

        val filterStepsForSingleInt =
          s"""/**
             |  * Traverse to nodes where the $nameCamelCase equals the given `value`
             |  * */
             |def $nameCamelCase(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{_.$nameCamelCase == value}
             |
             |/**
             |  * Traverse to nodes where the $nameCamelCase equals at least one of the given `values`
             |  * */
             |def $nameCamelCase(values: $baseType*): Traversal[NodeType] = {
             |  val vset = values.toSet
             |  traversal.filter{node => vset.contains(node.$nameCamelCase)}
             |}
             |
             |/**
             |  * Traverse to nodes where the $nameCamelCase is greater than the given `value`
             |  * */
             |def ${nameCamelCase}Gt(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{_.$nameCamelCase > value}
             |
             |/**
             |  * Traverse to nodes where the $nameCamelCase is greater than or equal the given `value`
             |  * */
             |def ${nameCamelCase}Gte(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{_.$nameCamelCase >= value}
             |
             |/**
             |  * Traverse to nodes where the $nameCamelCase is less than the given `value`
             |  * */
             |def ${nameCamelCase}Lt(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{_.$nameCamelCase < value}
             |
             |/**
             |  * Traverse to nodes where the $nameCamelCase is less than or equal the given `value`
             |  * */
             |def ${nameCamelCase}Lte(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{_.$nameCamelCase <= value}
             |
             |/**
             |  * Traverse to nodes where $nameCamelCase is not equal to the given `value`.
             |  * */
             |def ${nameCamelCase}Not(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{_.$nameCamelCase != value}
             |
             |/**
             |  * Traverse to nodes where $nameCamelCase is not equal to any of the given `values`.
             |  * */
             |def ${nameCamelCase}Not(values: $baseType*): Traversal[NodeType] = {
             |  val vset = values.toSet
             |  traversal.filter{node => !vset.contains(node.$nameCamelCase)}
             |}
             |""".stripMargin

        val filterStepsForOptionalInt =
          s"""/**
             |  * Traverse to nodes where the $nameCamelCase equals the given `value`
             |  * */
             |def $nameCamelCase(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{node => node.$nameCamelCase.isDefined && node.$nameCamelCase.get == value}
             |
             |/**
             |  * Traverse to nodes where the $nameCamelCase equals at least one of the given `values`
             |  * */
             |def $nameCamelCase(values: $baseType*): Traversal[NodeType] = {
             |  val vset = values.toSet
             |  traversal.filter{node => node.$nameCamelCase.isDefined && vset.contains(node.$nameCamelCase.get)}
             |}
             |
             |/**
             |  * Traverse to nodes where the $nameCamelCase is greater than the given `value`
             |  * */
             |def ${nameCamelCase}Gt(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{node => node.$nameCamelCase.isDefined && node.$nameCamelCase.get > value}
             |
             |/**
             |  * Traverse to nodes where the $nameCamelCase is greater than or equal the given `value`
             |  * */
             |def ${nameCamelCase}Gte(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{node => node.$nameCamelCase.isDefined && node.$nameCamelCase.get >= value}
             |
             |/**
             |  * Traverse to nodes where the $nameCamelCase is less than the given `value`
             |  * */
             |def ${nameCamelCase}Lt(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{node => node.$nameCamelCase.isDefined && node.$nameCamelCase.get < value}
             |
             |/**
             |  * Traverse to nodes where the $nameCamelCase is less than or equal the given `value`
             |  * */
             |def ${nameCamelCase}Lte(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{node => node.$nameCamelCase.isDefined && node.$nameCamelCase.get <= value}
             |
             |/**
             |  * Traverse to nodes where $nameCamelCase is not equal to the given `value`.
             |  * */
             |def ${nameCamelCase}Not(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{node => !node.$nameCamelCase.isDefined || node.$nameCamelCase.get != value}
             |
             |/**
             |  * Traverse to nodes where $nameCamelCase is not equal to any of the given `values`.
             |  * */
             |def ${nameCamelCase}Not(values: $baseType*): Traversal[NodeType] = {
             |  val vset = values.toSet
             |  traversal.filter{node => !node.$nameCamelCase.isDefined || !vset.contains(node.$nameCamelCase.get)}
             |}
             |""".stripMargin

        val filterStepsGenericSingle =
          s"""/**
             |  * Traverse to nodes where the $nameCamelCase equals the given `value`
             |  * */
             |def $nameCamelCase(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{_.$nameCamelCase == value}
             |
             |/**
             |  * Traverse to nodes where the $nameCamelCase equals at least one of the given `values`
             |  * */
             |def $nameCamelCase(values: $baseType*): Traversal[NodeType] = {
             |  val vset = values.toSet
             |  traversal.filter{node => !vset.contains(node.$nameCamelCase)}
             |}
             |
             |/**
             |  * Traverse to nodes where $nameCamelCase is not equal to the given `value`.
             |  * */
             |def ${nameCamelCase}Not(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{_.$nameCamelCase != value}
             |
             |/**
             |  * Traverse to nodes where $nameCamelCase is not equal to any of the given `values`.
             |  * */
             |def ${nameCamelCase}Not(values: $baseType*): Traversal[NodeType] = {
             |  val vset = values.toSet
             |  traversal.filter{node => !vset.contains(node.$nameCamelCase)}
             |}
             |""".stripMargin

        val filterStepsGenericOption =
          s"""/**
             |  * Traverse to nodes where the $nameCamelCase equals the given `value`
             |  * */
             |def $nameCamelCase(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{node => node.$nameCamelCase.isDefined && node.$nameCamelCase.get == value}
             |
             |/**
             |  * Traverse to nodes where the $nameCamelCase equals at least one of the given `values`
             |  * */
             |def $nameCamelCase(values: $baseType*): Traversal[NodeType] = {
             |  val vset = values.toSet
             |  traversal.filter{node => node.$nameCamelCase.isDefined && !vset.contains(node.$nameCamelCase.get)}
             |}
             |
             |/**
             |  * Traverse to nodes where $nameCamelCase is not equal to the given `value`.
             |  * */
             |def ${nameCamelCase}Not(value: $baseType): Traversal[NodeType] =
             |  traversal.filter{node => !node.$nameCamelCase.isDefined || node.$nameCamelCase.get != value}
             |
             |/**
             |  * Traverse to nodes where $nameCamelCase is not equal to any of the given `values`.
             |  * */
             |def ${nameCamelCase}Not(values: $baseType*): Traversal[NodeType] = {
             |  val vset = values.toSet
             |  traversal.filter{node => !node.$nameCamelCase.isDefined || !vset.contains(node.$nameCamelCase.get)}
             |}
             |""".stripMargin

        val filterSteps = (cardinality, property.valueType) match {
          case (Cardinality.List, _) => ""
          case (Cardinality.One(_), ValueType.String) => filterStepsForSingleString
          case (Cardinality.ZeroOrOne, ValueType.String) => filterStepsForOptionalString
          case (Cardinality.One(_), ValueType.Boolean) => filterStepsForSingleBoolean
          case (Cardinality.ZeroOrOne, ValueType.Boolean) => filterStepsForOptionalBoolean
          case (Cardinality.One(_), ValueType.Int) => filterStepsForSingleInt
          case (Cardinality.ZeroOrOne, ValueType.Int) => filterStepsForOptionalInt
          case (Cardinality.One(_), _) => filterStepsGenericSingle
          case (Cardinality.ZeroOrOne, _) => filterStepsGenericOption
          case _ => ""
        }

        s"""/** Traverse to $nameCamelCase property */
           |def $nameCamelCase: Traversal[$baseType] =
           |  traversal.$mapOrFlatMap(_.$nameCamelCase)
           |
           |$filterSteps
           |""".stripMargin
      }
    }

    def generateNodeTraversalExt(nodeType: AbstractNodeType): String = {
      val customStepNameTraversals = generateCustomStepNameTraversals(nodeType)
      val propertyTraversals = generatePropertyTraversals(nodeType.properties)
      val className = nodeType.className

      s"""package $traversalsPackage
         |
         |import overflowdb.traversal._
         |import $nodesPackage._
         |
         |/** Traversal steps for $className */
         |class ${className}TraversalExtGen[NodeType <: $className](val traversal: IterableOnce[NodeType]) extends AnyVal {
         |
         |${customStepNameTraversals.mkString(System.lineSeparator)}
         |
         |${propertyTraversals.mkString(System.lineSeparator)}
         |
         |}""".stripMargin
    }

    val packageObject =
      s"""package $basePackage
         |package object traversal extends NodeTraversalImplicits
         |""".stripMargin

    val results = mutable.Buffer.empty[File]
    val baseDir = outputDir / traversalsPackage.replaceAll("\\.", "/")
    if (baseDir.exists) baseDir.delete()
    baseDir.createDirectories()
    results.append(baseDir.createChild("package.scala").write(packageObject))
    results.append(baseDir.createChild("NodeTraversalImplicits.scala").write(nodeTraversalImplicits))
    schema.allNodeTypes.foreach { nodeType =>
      val src = generateNodeTraversalExt(nodeType)
      val srcFile = nodeType.className + ".scala"
      results.append(baseDir.createChild(srcFile).write(src))
    }
    results.toSeq
  }

  /** generates classes to easily add new nodes to the graph
    * this ability could have been added to the existing nodes, but it turned out as a different specialisation,
    * since e.g. `id` is not set before adding it to the graph */
  protected def writeNewNodeFile(outputDir: File): File = {
    val staticHeader =
      s"""package $nodesPackage
         |
         |/** base type for all nodes that can be added to a graph, e.g. the diffgraph */
         |abstract class NewNode extends AbstractNode with overflowdb.DetachedNodeData with Product {
         |  def properties: Map[String, Any]
         |  def copy: this.type
         |  type StoredType <: StoredNode
         |  private var refOrId: Object = null
         |  override def getRefOrId(): Object = refOrId
         |  override def setRefOrId(r: Object): Unit = {this.refOrId = r}
         |  def stored: Option[StoredType] = if(refOrId != null && refOrId.isInstanceOf[StoredNode]) Some(refOrId).asInstanceOf[Option[StoredType]] else None
         |}
         |""".stripMargin

    def generateNewNodeSource(nodeType: NodeType, properties: Seq[Property[_]]) = {
      import Property.Cardinality
      case class FieldDescription(name: String, valueType: String, fullType: String, cardinality: Cardinality)
      val fieldDescriptions = mutable.ArrayBuffer.empty[FieldDescription]

      for (property <- properties) {
        fieldDescriptions += FieldDescription(
          camelCase(property.name),
          typeFor(property),
          getCompleteType(property),
          property.cardinality)
      }

      for (containedNode <- nodeType.containedNodes) {
        fieldDescriptions += FieldDescription(
          containedNode.localName,
          typeFor(containedNode),
          getCompleteType(containedNode),
          containedNode.cardinality)
      }

      val memberVariables = fieldDescriptions.reverse.map {
        case FieldDescription(name, _, fullType, cardinality) =>
          val defaultValue = cardinality match {
            case Cardinality.One(default) => defaultValueImpl(default)
            case Cardinality.ZeroOrOne => "None"
            case Cardinality.List => "collection.immutable.ArraySeq.empty"
          }
          s"var $name: $fullType = $defaultValue"

      }.mkString(", ")

      val propertiesMapImpl = {
        val putKeysImpl = properties
          .map { key =>
            val memberName = camelCase(key.name)
            import Property.Cardinality
            key.cardinality match {
              case Cardinality.One(default) =>
                val isDefaultValueImpl = defaultValueCheckImpl(memberName, default)
                s"""if (!($isDefaultValueImpl)) { res += "${key.name}" -> $memberName }"""
              case Cardinality.ZeroOrOne =>
                s"""$memberName.map { value => res += "${key.name}" -> value }"""
              case Cardinality.List =>
                s"""if ($memberName != null && $memberName.nonEmpty) { res += "${key.name}" -> $memberName }"""
            }
          }
        val putRefsImpl = nodeType.containedNodes.map { key =>
          import Property.Cardinality
          val memberName = key.localName
          key.cardinality match {
            case Cardinality.One(default) =>
              val isDefaultValueImpl = defaultValueCheckImpl(memberName, default)
              s"""if (!($isDefaultValueImpl)) { res += "$memberName" -> $memberName }"""
            case Cardinality.ZeroOrOne =>
              s"""$memberName.map { value => res += "$memberName" -> value }"""
            case Cardinality.List =>
              s"""if ($memberName != null && $memberName.nonEmpty) { res += "$memberName" -> $memberName }"""
          }
        }

        val propertiesImpl = {
          val lines = putKeysImpl ++ putRefsImpl
          if (lines.nonEmpty) {
            s"""var res = Map[String, Any]()
               |${lines.mkString(lineSeparator)}
               |res""".stripMargin
          } else {
            "Map.empty"
          }
        }

        s"""override def properties: Map[String, Any] = {
           |$propertiesImpl
           |}""".stripMargin
      }

      val nodeClassName = nodeType.className

      val mixins = nodeType.extendz.map{baseType => s"with ${baseType.className}New"}.mkString(" ")

      val propertySettersImpl = fieldDescriptions.map {
        case FieldDescription(name, valueType , _, cardinality) =>
          cardinality match {
            case Cardinality.One(_) =>
              s"""def $name(value: $valueType): this.type = {
                 |  this.$name = value
                 |  this
                 |}
                 |""".stripMargin

            case Cardinality.ZeroOrOne =>
              s"""def $name(value: $valueType): this.type = {
                 |  this.$name = Option(value)
                 |  this
                 |}
                 |
                 |def $name(value: Option[$valueType]): this.type = $name(value.orNull)
                 |""".stripMargin

            case Cardinality.List =>
              s"""def $name(value: IterableOnce[$valueType]): this.type = {
                 |  this.$name = value.iterator.to(collection.immutable.ArraySeq)
                 |  this
                 |}
                 |""".stripMargin
          }
      }.mkString(lineSeparator)

      val copyFieldsImpl = fieldDescriptions.map { field =>
        val memberName = field.name
        s"newInstance.$memberName = this.$memberName"
      }.mkString(lineSeparator)

      val classNameNewNode = s"New$nodeClassName"

      val productElements = fieldDescriptions.reverse.zipWithIndex
      val productElementAccessors = productElements.map {
        case (fieldDescription, index) =>
          s"case $index => this.${fieldDescription.name}"
      }.mkString(lineSeparator)
      val productElementNames = productElements.map {
        case (fieldDescription, index) =>
          s"""case $index => "${fieldDescription.name}""""
      }.mkString(lineSeparator)

      s"""object $classNameNewNode {
         |  def apply(): $classNameNewNode = new $classNameNewNode
         |}
         |
         |class $classNameNewNode($memberVariables)
         |  extends NewNode with ${nodeClassName}Base $mixins {
         |
         |  type StoredType = $nodeClassName
         |
         |  override def label: String = "${nodeType.name}"
         |
         |  override def copy: this.type = {
         |    val newInstance = new New$nodeClassName
         |    $copyFieldsImpl
         |    newInstance.asInstanceOf[this.type]
         |  }
         |
         |  $propertySettersImpl
         |
         |  $propertiesMapImpl
         |
         |  override def productElement(n: Int): Any =
         |    n match {
         |      $productElementAccessors
         |      case _ => null
         |    }
         |
         |  override def productElementName(n: Int): String =
         |    n match {
         |      $productElementNames
         |      case _ => ""
         |    }
         |
         |  override def productPrefix = "$classNameNewNode"
         |  override def productArity = ${fieldDescriptions.size}
         |
         |  override def canEqual(that: Any): Boolean = that != null && that.isInstanceOf[$classNameNewNode]
         |}
         |""".stripMargin
    }

    val outfile = outputDir / nodesPackage.replaceAll("\\.", "/") / "NewNodes.scala"
    if (outfile.exists) outfile.delete()
    outfile.createFile()
    val src = schema.nodeTypes.map { nodeType =>
      generateNewNodeSource(nodeType, nodeType.properties)
    }.mkString(lineSeparator)
    outfile.write(s"""$staticHeader
                     |$src
                     |""".stripMargin)
  }
}

object CodeGen {
  case class ConstantContext(name: String, source: String, documentation: Option[String])
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy