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

overflowdb.schema.Schema.scala Maven / Gradle / Ivy

package overflowdb.schema

import overflowdb.codegen.Helpers._
import overflowdb.schema.Property.Default

import scala.collection.mutable

/**
 * @param basePackage: specific for your domain, e.g. `com.example.mydomain`
 * @param additionalTraversalsPackages: additional packages that contain your traversals - used for `.help` to find @Doc annotations via reflection
 */
class Schema(val domainShortName: String,
             val basePackage: String,
             val additionalTraversalsPackages: Seq[String],
             val properties: Seq[Property[_]],
             val nodeBaseTypes: Seq[NodeBaseType],
             val nodeTypes: Seq[NodeType],
             val edgeTypes: Seq[EdgeType],
             val constantsByCategory: Map[String, Seq[Constant[_]]],
             val protoOptions: Option[ProtoOptions],
             val noWarnList: Set[(AbstractNodeType, Property[_])]) {

  /** nodeTypes and nodeBaseTypes combined */
  lazy val allNodeTypes: Seq[AbstractNodeType] =
    nodeTypes ++ nodeBaseTypes

  /** properties that are used in node types */
  def nodeProperties: Seq[Property[_]] =
    properties.filter(property =>
      (nodeTypes ++ nodeBaseTypes).exists(_.properties.contains(property))
    )

  /** properties that are used in edge types */
  def edgeProperties: Seq[Property[_]] =
    properties.filter(property =>
      edgeTypes.exists(_.properties.contains(property))
    )
}

abstract class AbstractNodeType(val name: String, val comment: Option[String], val schemaInfo: SchemaInfo)
  extends HasClassName with HasProperties with HasSchemaInfo {
  protected val _extendz: mutable.Set[NodeBaseType] = mutable.Set.empty
  protected val _outEdges: mutable.Set[AdjacentNode] = mutable.Set.empty
  protected val _inEdges: mutable.Set[AdjacentNode] = mutable.Set.empty
  protected val _markerTraits: mutable.Set[MarkerTrait] = mutable.Set.empty

  /** all node types that extend this node */
  def subtypes(allNodes: Set[AbstractNodeType]): Set[AbstractNodeType]


  /** properties (including potentially inherited properties) */
  override def properties: Seq[Property[_]] = {
    val entireClassHierarchy = this +: extendzRecursively
    entireClassHierarchy.flatMap(_.propertiesWithoutInheritance).distinct.sortBy(_.name.toLowerCase)
  }

  def propertiesWithoutInheritance: Seq[Property[_]] =
    _properties.toSeq.sortBy(_.name.toLowerCase)

  def extendz(additional: NodeBaseType*): this.type = {
    additional.foreach(_extendz.add)
    this
  }

  def extendz: Seq[NodeBaseType] =
    _extendz.toSeq

  def extendzRecursively: Seq[NodeBaseType] = {
    val extendsLevel1 = extendz
    (extendsLevel1 ++ extendsLevel1.flatMap(_.extendzRecursively)).distinct
  }

  /**
   * note: allowing to define one outEdge for ONE inNode only - if you are looking for Union Types, please use NodeBaseTypes
   */
  def addOutEdge(edge: EdgeType,
                 inNode: AbstractNodeType,
                 cardinalityOut: EdgeType.Cardinality = EdgeType.Cardinality.List,
                 cardinalityIn: EdgeType.Cardinality = EdgeType.Cardinality.List,
                 stepNameOut: String = "",
                 stepNameOutDoc: String = "",
                 stepNameIn: String = "",
                 stepNameInDoc: String = ""): this.type = {
    _outEdges.add(AdjacentNode(edge, inNode, cardinalityOut, stringToOption(stepNameOut), stringToOption(stepNameOutDoc)))
    inNode._inEdges.add(AdjacentNode(edge, this, cardinalityIn, stringToOption(stepNameIn), stringToOption(stepNameInDoc)))
    this
  }

  def addInEdge(edge: EdgeType,
                outNode: AbstractNodeType,
                cardinalityIn: EdgeType.Cardinality = EdgeType.Cardinality.List,
                cardinalityOut: EdgeType.Cardinality = EdgeType.Cardinality.List,
                stepNameIn: String = "",
                stepNameInDoc: String = "",
                stepNameOut: String = "",
                stepNameOutDoc: String = ""): this.type = {
    _inEdges.add(AdjacentNode(edge, outNode, cardinalityIn, stringToOption(stepNameIn), stringToOption(stepNameInDoc)))
    outNode._outEdges.add(AdjacentNode(edge, this, cardinalityOut, stringToOption(stepNameOut), stringToOption(stepNameOutDoc)))
    this
  }

  def outEdges: Seq[AdjacentNode] =
    _outEdges.toSeq

  def inEdges: Seq[AdjacentNode] =
    _inEdges.toSeq

  def edges(direction: Direction.Value): Seq[AdjacentNode] =
    direction match {
      case Direction.IN => inEdges
      case Direction.OUT => outEdges
    }

  def edges: Seq[AdjacentNode] =
    outEdges ++ inEdges

  def addMarkerTrait(name: String): this.type = {
    _markerTraits.add(MarkerTrait(name))
    this
  }

  def markerTraits: Seq[MarkerTrait] =
    _markerTraits.toSeq
}

class NodeType(name: String, comment: Option[String], schemaInfo: SchemaInfo)
  extends AbstractNodeType(name, comment, schemaInfo) with HasOptionalProtoId  {
  protected val _containedNodes: mutable.Set[ContainedNode] = mutable.Set.empty

  lazy val classNameDb = s"${className}Db"

  /** all node types that extend this node */
  override def subtypes(allNodes: Set[AbstractNodeType]) = Set.empty

  def containedNodes: Seq[ContainedNode] =
    _containedNodes.toSeq.sortBy(_.localName.toLowerCase)

  def addContainedNode(node: AbstractNodeType,
                       localName: String,
                       cardinality: Property.Cardinality,
                       comment: String = ""): NodeType = {
    _containedNodes.add(ContainedNode(node, localName, cardinality, stringToOption(comment)))
    this
  }

  override def toString = s"NodeType($name)"
}

/** root node trait for all nodes - use if you want to be explicitly unspecific */
object AnyNodeType extends AbstractNodeType(
  name = "AnyNode",
  comment = Some("generic node base trait - use if you want to be explicitly unspecific"),
  SchemaInfo.Unknown) {
  /** all node types extend this node */
  override def subtypes(allNodes: Set[AbstractNodeType]): Set[AbstractNodeType] = allNodes
}

class NodeBaseType(name: String, comment: Option[String], schemaInfo: SchemaInfo)
  extends AbstractNodeType(name, comment, schemaInfo) {

  /** all node types that extend this node */
  override def subtypes(allNodes: Set[AbstractNodeType]) =
    allNodes.filter { candidate =>
      candidate.extendzRecursively.contains(this)
    }

  override def toString = s"NodeBaseType($name)"
}

case class AdjacentNode(viaEdge: EdgeType, neighbor: AbstractNodeType, cardinality: EdgeType.Cardinality,
                        customStepName: Option[String] = None,
                        customStepDoc: Option[String] = None)

case class ContainedNode(nodeType: AbstractNodeType,
                         localName: String,
                         cardinality: Property.Cardinality,
                         comment: Option[String]) {
  lazy val classNameForStoredNode =
    if (nodeType == AnyNodeType) "StoredNode"
    else nodeType.className
}

/** An empty trait without any implementation, e.g. to mark a semantic relationship between certain types */
case class MarkerTrait(name: String)

class EdgeType(val name: String, val comment: Option[String], val schemaInfo: SchemaInfo)
  extends HasClassName with HasProperties with HasOptionalProtoId with HasSchemaInfo {
  override def toString = s"EdgeType($name)"

  /** properties (including potentially inherited properties) */
  def properties: Seq[Property[_]] =
    _properties.toSeq.sortBy(_.name.toLowerCase)
}

object EdgeType {
  sealed abstract class Cardinality
  object Cardinality {
    case object One extends Cardinality
    case object ZeroOrOne extends Cardinality
    case object List extends Cardinality
  }
}

class Property[A](val name: String,
                  val valueType: Property.ValueType[A],
                  val comment: Option[String] = None,
                  val schemaInfo: SchemaInfo) extends HasClassName with HasOptionalProtoId with HasSchemaInfo {
  import Property.Cardinality
  protected var _cardinality: Cardinality = Cardinality.ZeroOrOne
  def cardinality: Cardinality = _cardinality

  /** make this a mandatory property, which allows us to use primitives (better memory footprint, no GC, ...) */
  def mandatory(default: A): Property[A] = {
    _cardinality = Cardinality.One(Property.Default(default))
    this
  }

  def isMandatory: Boolean =
    cardinality.isInstanceOf[Cardinality.One[_]]

  def hasDefault: Boolean =
    default.isDefined

  def default: Option[Default[A]] =
    _cardinality match {
      case c: Cardinality.One[_] =>
        // casting is safe here because only `mandatory(A)` can set the value
        Option(c.default).map(_.asInstanceOf[Default[A]])
      case _ =>
        None
    }

  /** make this a list property, using a regular Sequence, with linear (slow) random access */
  def asList(): Property[A] = {
    _cardinality = Cardinality.List
    this
  }
}

object Property {

  sealed trait ValueType[A]
  object ValueType {
    object Boolean extends ValueType[Boolean]
    object String extends ValueType[String]
    object Byte extends ValueType[Byte]
    object Short extends ValueType[Short]
    object Int extends ValueType[Int]
    object Long extends ValueType[Long]
    object Float extends ValueType[Float]
    object Double extends ValueType[Double]
    object List extends ValueType[Seq[_]]
    object Char extends ValueType[Char]
    object NodeRef extends ValueType[Any]
    object Unknown extends ValueType[Any]
  }

  sealed trait Cardinality
  object Cardinality {
    case object ZeroOrOne extends Cardinality
    case object List extends Cardinality
    case class One[A](default: Default[A]) extends Cardinality
  }

  case class Default[A](value: A)
}

class Constant[A](val name: String,
               val value: String,
               val valueType: Property.ValueType[A],
               val comment: Option[String],
               val schemaInfo: SchemaInfo) extends HasOptionalProtoId with HasSchemaInfo {
  override def toString = s"Constant($name)"
}

object Constant {
  def apply[A](name: String, value: String, valueType: Property.ValueType[A], comment: String = "")(
    implicit schemaInfo: SchemaInfo = SchemaInfo.Unknown): Constant[A] =
    new Constant[A](name, value, valueType, stringToOption(comment), schemaInfo)
}

case class NeighborInfoForEdge(edge: EdgeType, nodeInfos: Seq[NeighborInfoForNode], offsetPosition: Int) {
  lazy val deriveNeighborNodeType: String =
    deriveCommonRootType(nodeInfos.map(_.neighborNode).toSet)
}

case class NeighborInfoForNode(
  neighborNode: AbstractNodeType,
  edge: EdgeType,
  direction: Direction.Value,
  cardinality: EdgeType.Cardinality,
  isInherited: Boolean,
  customStepName: Option[String] = None,
  customStepDoc: Option[String] = None) {

  /** handling some accidental complexity within the schema: if a relationship is defined on a base node and
   * separately on a concrete node, with different cardinalities, we need to use the highest cardinality  */
  lazy val consolidatedCardinality: EdgeType.Cardinality = {
    val inheritedCardinalities = neighborNode.extendzRecursively.flatMap(_.inEdges).collect {
      case AdjacentNode(viaEdge, neighbor, cardinality, _, _)
        if viaEdge == edge && neighbor == neighborNode => cardinality
    }
    val allCardinalities = cardinality +: inheritedCardinalities
    allCardinalities.distinct.sortBy {
      case EdgeType.Cardinality.List => 0
      case EdgeType.Cardinality.ZeroOrOne => 1
      case EdgeType.Cardinality.One => 2
    }.head
  }

  lazy val returnType: String =
    fullScalaType(neighborNode, consolidatedCardinality)

}

object Direction extends Enumeration {
  val IN, OUT = Value
  val all = List(IN, OUT)
}

case class ProductElement(name: String, accessorSrc: String, index: Int)

case class ProtoOptions(pkg: String,
                        javaOuterClassname: String,
                        javaPackage: String,
                        goPackage: String,
                        csharpNamespace: String,
                        uncommonProtoEnumNameMappings: Map[String, String] = Map.empty)

trait HasClassName {
  def name: String
  lazy val className = camelCaseCaps(name)
}

trait HasProperties {
  protected val _properties: mutable.Set[Property[_]] = mutable.Set.empty

  def addProperty(additional: Property[_]): this.type = {
    _properties.add(additional)
    this
  }

  def addProperties(additional: Property[_]*): this.type = {
    additional.foreach(addProperty)
    this
  }

  /** properties (including potentially inherited properties) */
  def properties: Seq[Property[_]]
}

trait HasOptionalProtoId {
  protected var _protoId: Option[Int] = None

  def protoId(id: Int): this.type = {
    _protoId = Some(id)
    this
  }

  def protoId: Option[Int] = _protoId
}

trait HasSchemaInfo {
  def schemaInfo: SchemaInfo
}

/** carry extra information on where a schema element is being defined, e.g. when we want to be able to
 * refer back that `node XYZ` was defined in `BaseSchema`, e.g. for documentation */
case class SchemaInfo(definedIn: Option[Class[_]])
object SchemaInfo {
  val Unknown = SchemaInfo(None)

  def forClass(schemaClass: Class[_]): SchemaInfo =
    SchemaInfo(Option(schemaClass))
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy