diff --git a/codegen/src/main/scala/overflowdb/codegen/CodeGen.scala b/codegen/src/main/scala/overflowdb/codegen/CodeGen.scala index 2c65d0e5..34bd250a 100644 --- a/codegen/src/main/scala/overflowdb/codegen/CodeGen.scala +++ b/codegen/src/main/scala/overflowdb/codegen/CodeGen.scala @@ -254,20 +254,26 @@ class CodeGen(schema: Schema) { }) } - writeConstantsFile("Properties", schema.properties.map { property => - val src = { + // Properties.scala + val propertyKeysConstantsSource = { + schema.properties.map { property => 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>" + val completeType = property.cardinality match { + case Property.Cardinality.One(_) => valueType + case Property.Cardinality.ZeroOrOne => valueType + case Property.Cardinality.List => s"IndexedSeq[$valueType]" } - s"""public static final overflowdb.PropertyKey<$completeType> ${property.name} = new overflowdb.PropertyKey<>("${property.name}");""" + s"""val ${camelCaseCaps(property.name)}: overflowdb.PropertyKey[$completeType] = new overflowdb.PropertyKey("${property.name}")""".stripMargin.trim } - ConstantContext(property.name, src, property.comment) - }) + }.mkString("\n\n") + val file = baseDir.createChild("Properties.scala").write( + s"""package ${schema.basePackage} + | + |object Properties { + |$propertyKeysConstantsSource + |}""".stripMargin + ) + results.append(file) results.toSeq } @@ -343,7 +349,7 @@ class CodeGen(schema: Schema) { properties.map { property => val name = property.name val nameCamelCase = camelCase(name) - val tpe = getCompleteType(property) + val tpe = getCompleteType(property, nullable = false) property.cardinality match { case Cardinality.One(_) => @@ -479,7 +485,7 @@ class CodeGen(schema: Schema) { def generateNodeBaseTypeSource(nodeBaseType: NodeBaseType): String = { val className = nodeBaseType.className val properties = nodeBaseType.properties - val propertyAccessors = Helpers.propertyAccessors(properties) + val propertyAccessors = Helpers.propertyAccessors(properties, nullable = false) val mixinsForBaseTypes = nodeBaseType.extendz.map { baseTrait => s"with ${baseTrait.className}" @@ -601,7 +607,7 @@ class CodeGen(schema: Schema) { val newNodePropertySetters = nodeBaseType.properties.map { property => val camelCaseName = camelCase(property.name) - val tpe = getCompleteType(property) + val tpe = getCompleteType(property, nullable = false) s"def ${camelCaseName}_=(value: $tpe): Unit" }.mkString(lineSeparator) @@ -856,7 +862,7 @@ class CodeGen(schema: Schema) { case Cardinality.One(_) => s"""this._$memberName = $newNodeCasted.$memberName""".stripMargin case Cardinality.ZeroOrOne => - s"""this._$memberName = $newNodeCasted.$memberName.orNull""".stripMargin + s"""this._$memberName = $newNodeCasted.$memberName match { case None => null; case Some(value) => value }""".stripMargin case Cardinality.List => s"""this._$memberName = if ($newNodeCasted.$memberName != null) $newNodeCasted.$memberName else collection.immutable.ArraySeq.empty""".stripMargin } @@ -993,7 +999,7 @@ class CodeGen(schema: Schema) { s"""trait ${className}Base extends AbstractNode $mixinsForExtendedNodesBase $mixinsForMarkerTraits { | def asStored : StoredNode = this.asInstanceOf[StoredNode] | - | ${Helpers.propertyAccessors(properties)} + | ${Helpers.propertyAccessors(properties, nullable = false)} | | $abstractContainedNodeAccessors |} @@ -1021,7 +1027,7 @@ class CodeGen(schema: Schema) { val nodeRefImpl = { val propertyDelegators = properties.map { key => val name = camelCase(key.name) - s"""override def $name: ${getCompleteType(key)} = get().$name""" + s"""override def $name: ${getCompleteType(key, nullable = false)} = get().$name""" }.mkString(lineSeparator) val propertyDefaultValues = propertyDefaultValueImpl(s"$className.PropertyDefaults", properties) @@ -1095,14 +1101,14 @@ class CodeGen(schema: Schema) { val updateSpecificPropertyImpl: String = { import Property.Cardinality - def caseEntry(name: String, accessorName: String, cardinality: Cardinality, baseType: String) = { + def caseEntry(name: String, accessorName: String, cardinality: Cardinality, baseType: String, baseTypeNullable: 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 singleValue: $baseTypeNullable => 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]] @@ -1120,10 +1126,10 @@ class CodeGen(schema: Schema) { s"""|case "$name" => this._$accessorName = $setter""" } - val forKeys = properties.map(p => caseEntry(p.name, camelCase(p.name), p.cardinality, typeFor(p))).mkString(lineSeparator) + val forKeys = properties.map(p => caseEntry(p.name, camelCase(p.name), p.cardinality, typeFor(p), typeFor(p, nullable = true))).mkString(lineSeparator) val forContainedNodes = nodeType.containedNodes.map(containedNode => - caseEntry(containedNode.localName, containedNode.localName, containedNode.cardinality, containedNode.classNameForStoredNode) + caseEntry(containedNode.localName, containedNode.localName, containedNode.cardinality, containedNode.classNameForStoredNode, containedNode.classNameForStoredNode) ).mkString(lineSeparator) s"""override protected def updateSpecificProperty(key:String, value: Object): Unit = { @@ -1161,10 +1167,11 @@ class CodeGen(schema: Schema) { val fieldName = s"_$publicName" val (publicType, tpeForField, fieldAccessor, defaultValue) = { val valueType = typeFor(property) + val valueTypeNullable = typeFor(property, nullable = true) 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") + (valueType, valueTypeNullable, fieldName, s"$className.PropertyDefaults.${property.className}") + case Cardinality.ZeroOrOne => (s"Option[$valueType]", valueTypeNullable, s"Option($fieldName).asInstanceOf[Option[$valueType]]", "null") case Cardinality.List => (s"IndexedSeq[$valueType]", s"IndexedSeq[$valueType]", fieldName, "collection.immutable.ArraySeq.empty") } } @@ -1732,14 +1739,15 @@ class CodeGen(schema: Schema) { def generateNewNodeSource(nodeType: NodeType, properties: Seq[Property[?]], inEdges: Map[String, Set[String]], outEdges: Map[String, Set[String]]) = { import Property.Cardinality - case class FieldDescription(name: String, valueType: String, fullType: String, cardinality: Cardinality) + case class FieldDescription(name: String, valueType: String, valueTypeNullable: String, fullType: String, cardinality: Cardinality) val fieldDescriptions = mutable.ArrayBuffer.empty[FieldDescription] for (property <- properties) { fieldDescriptions += FieldDescription( camelCase(property.name), typeFor(property), - getCompleteType(property), + typeFor(property, nullable = true), + getCompleteType(property, nullable = false), property.cardinality) } @@ -1747,12 +1755,13 @@ class CodeGen(schema: Schema) { fieldDescriptions += FieldDescription( containedNode.localName, typeFor(containedNode), + typeFor(containedNode), getCompleteType(containedNode), containedNode.cardinality) } val memberVariables = fieldDescriptions.reverse.map { - case FieldDescription(name, _, fullType, cardinality) => + case FieldDescription(name, _, _, fullType, cardinality) => val defaultValue = cardinality match { case Cardinality.One(default) => defaultValueImpl(default) case Cardinality.ZeroOrOne => "None" @@ -1812,22 +1821,22 @@ class CodeGen(schema: Schema) { val mixins = nodeType.extendz.map{baseType => s"with ${baseType.className}New"}.mkString(" ") val propertySettersImpl = fieldDescriptions.map { - case FieldDescription(name, valueType , _, cardinality) => + case FieldDescription(name, valueType, valueTypeNullable, _, cardinality) => cardinality match { case Cardinality.One(_) => - s"""def $name(value: $valueType): this.type = { + s"""def $name(value: $valueTypeNullable): this.type = { | this.$name = value | this |} |""".stripMargin case Cardinality.ZeroOrOne => - s"""def $name(value: $valueType): this.type = { - | this.$name = Option(value) + s"""def $name(value: $valueTypeNullable): this.type = { + | this.$name = Option(value).asInstanceOf[Option[$valueType]] | this |} | - |def $name(value: Option[$valueType]): this.type = $name(value.orNull) + |def $name(value: Option[$valueType]): this.type = $name(value match { case None => null; case Some(value) => value: $valueTypeNullable }) |""".stripMargin case Cardinality.List => diff --git a/codegen/src/main/scala/overflowdb/codegen/Helpers.scala b/codegen/src/main/scala/overflowdb/codegen/Helpers.scala index c95952d4..2cb4b42d 100644 --- a/codegen/src/main/scala/overflowdb/codegen/Helpers.scala +++ b/codegen/src/main/scala/overflowdb/codegen/Helpers.scala @@ -32,18 +32,17 @@ object Helpers { case nonEmptyString => Some(nonEmptyString) } - def typeFor[A](property: Property[A]): String = { - val isMandatory = property.isMandatory + def typeFor[A](property: Property[A], nullable: Boolean = false): String = { property.valueType match { - case ValueType.Boolean => if (isMandatory) "Boolean" else "java.lang.Boolean" + case ValueType.Boolean => if (nullable) "java.lang.Boolean" else "Boolean" case ValueType.String => "String" - case ValueType.Byte => if (isMandatory) "Byte" else "java.lang.Byte" - case ValueType.Short => if (isMandatory) "Short" else "java.lang.Short" - case ValueType.Int => if (isMandatory) "scala.Int" else "Integer" - case ValueType.Long => if (isMandatory) "Long" else "java.lang.Long" - case ValueType.Float => if (isMandatory) "Float" else "java.lang.Float" - case ValueType.Double => if (isMandatory) "Double" else "java.lang.Double" - case ValueType.Char => if (isMandatory) "scala.Char" else "Character" + case ValueType.Byte => if (nullable) "java.lang.Byte" else "Byte" + case ValueType.Short => if (nullable) "java.lang.Short" else "Short" + case ValueType.Int => if (nullable) "Integer" else "scala.Int" + case ValueType.Long => if (nullable) "java.lang.Long" else "Long" + case ValueType.Float => if (nullable) "java.lang.Float" else "Float" + case ValueType.Double => if (nullable) "java.lang.Double" else "Double" + case ValueType.Char => if (nullable) "Character" else "scala.Char" case ValueType.List => "Seq[_]" case ValueType.NodeRef => "overflowdb.NodeRef[_]" case ValueType.Unknown => "java.lang.Object" @@ -120,8 +119,8 @@ object Helpers { } } - def getCompleteType[A](property: Property[?]): String = - getCompleteType(property.cardinality, typeFor(property)) + def getCompleteType[A](property: Property[?], nullable: Boolean = true): String = + getCompleteType(property.cardinality, typeFor(property, nullable)) def typeFor(containedNode: ContainedNode): String = { containedNode.nodeType match { @@ -200,10 +199,10 @@ object Helpers { }.mkString(s"$lineSeparator| ") } - def propertyAccessors(properties: Seq[Property[?]]): String = { + def propertyAccessors(properties: Seq[Property[?]], nullable: Boolean = true): String = { properties.map { property => val camelCaseName = camelCase(property.name) - val tpe = getCompleteType(property) + val tpe = getCompleteType(property, nullable = nullable) s"def $camelCaseName: $tpe" }.mkString(lineSeparator) } diff --git a/integration-tests/tests/src/test/scala/Schema01Test.scala b/integration-tests/tests/src/test/scala/Schema01Test.scala index d67668c3..983b442c 100644 --- a/integration-tests/tests/src/test/scala/Schema01Test.scala +++ b/integration-tests/tests/src/test/scala/Schema01Test.scala @@ -6,13 +6,13 @@ import testschema01.nodes._ import testschema01.edges._ import testschema01.traversal._ import scala.jdk.CollectionConverters.IteratorHasAsScala + class Schema01Test extends AnyWordSpec with Matchers { import testschema01.traversal._ "constants" in { PropertyNames.NAME shouldBe "NAME" - Properties.ORDER.name shouldBe "ORDER" + Properties.Order.name shouldBe "ORDER" PropertyNames.ALL.contains("OPTIONS") shouldBe true - Properties.ALL.contains(Properties.OPTIONS) shouldBe true NodeTypes.NODE1 shouldBe "NODE1" NodeTypes.ALL.contains(Node2.Label) shouldBe true @@ -42,8 +42,8 @@ class Schema01Test extends AnyWordSpec with Matchers { "lookup and traverse nodes/edges/properties" in { // generic traversal - graph.nodes.asScala.property(Properties.NAME).toSetMutable shouldBe Set("node 1a", "node 1b", "node 2a", "node 2b") - graph.edges.asScala.property(Properties.NAME).toSetMutable shouldBe Set("edge 2") + graph.nodes.asScala.property(Properties.Name).toSetMutable shouldBe Set("node 1a", "node 1b", "node 2a", "node 2b") + graph.edges.asScala.property(Properties.Name).toSetMutable shouldBe Set("edge 2") node1Traversal.out.toList shouldBe Seq(node2a) node1Traversal.name.toSetMutable shouldBe Set("node 1a", "node 1b") node1Traversal.order.l shouldBe Seq(2) @@ -58,7 +58,7 @@ class Schema01Test extends AnyWordSpec with Matchers { val edge2Specific = edge2.asInstanceOf[Edge2] val name: String = node1aSpecific.name name shouldBe "node 1a" - val o1: Option[Integer] = node1aSpecific.order + val o1: Option[Int] = node1aSpecific.order node1aSpecific.order shouldBe Some(2) node1bSpecific.order shouldBe None val o2: Seq[String] = node2aSpecific.options @@ -97,7 +97,7 @@ class Schema01Test extends AnyWordSpec with Matchers { .name("name1") .node3(node3) .options(Seq("one", "two", "three")) - .placements(Seq(1,2,3): Seq[Integer]) + .placements(Seq(1,2,3)) val builder = new BatchedUpdate.DiffGraphBuilder builder.addNode(newNode2) diff --git a/integration-tests/tests/src/test/scala/Schema02Test.scala b/integration-tests/tests/src/test/scala/Schema02Test.scala index c6290fd3..2534fecd 100644 --- a/integration-tests/tests/src/test/scala/Schema02Test.scala +++ b/integration-tests/tests/src/test/scala/Schema02Test.scala @@ -55,7 +55,7 @@ class Schema02Test extends AnyWordSpec with Matchers { copy.order(3) copy.order shouldBe Some(3) - copy.order(Some(4: Integer)) + copy.order(Some(4)) copy.order shouldBe Some(4) original.name shouldBe "A" diff --git a/integration-tests/tests/src/test/scala/Schema04Test.scala b/integration-tests/tests/src/test/scala/Schema04Test.scala index f7ba112e..19d87273 100644 --- a/integration-tests/tests/src/test/scala/Schema04Test.scala +++ b/integration-tests/tests/src/test/scala/Schema04Test.scala @@ -78,18 +78,18 @@ class Schema04Test extends AnyWordSpec with Matchers { val node2 = graph.addNode(Node1.Label).asInstanceOf[Node1] val edge1 = node1.addEdge(Edge1.Label, node2).asInstanceOf[Edge1] val properties = Seq( - Properties.BOOL.of(false), - Properties.STR.of("foo"), - Properties.BYTE.of(100: Byte), - Properties.SHORT.of(101: Short), - Properties.INT.of(102), - Properties.LONG.of(103), - Properties.FLOAT1.of(Float.NaN), - Properties.FLOAT2.of(104.4f), - Properties.DOUBLE1.of(Double.NaN), - Properties.DOUBLE2.of(105.5), - Properties.CHAR.of('Z'), - Properties.INT_LIST.of(ArraySeq(3, 4, 5)), + Properties.Bool.of(false), + Properties.Str.of("foo"), + Properties.Byte.of(100: Byte), + Properties.Short.of(101: Short), + Properties.Int.of(102), + Properties.Long.of(103), + Properties.Float1.of(Float.NaN), + Properties.Float2.of(104.4f), + Properties.Double1.of(Double.NaN), + Properties.Double2.of(105.5), + Properties.Char.of('Z'), + Properties.IntList.of(ArraySeq(3, 4, 5)), ) properties.foreach(node1.setProperty) properties.foreach(edge1.setProperty) @@ -164,8 +164,8 @@ class Schema04Test extends AnyWordSpec with Matchers { val node1 = graph.addNode(Node1.Label).asInstanceOf[Node1] val node2 = graph.addNode(Node1.Label).asInstanceOf[Node1] val edge1 = node1.addEdge(Edge1.Label, node2).asInstanceOf[Edge1] - node1.setProperty(Properties.INT_LIST.name, Array(1,2,3)) - edge1.setProperty(Properties.INT_LIST.name, Array(3,4,5)) + node1.setProperty(Properties.IntList.name, Array(1,2,3)) + edge1.setProperty(Properties.IntList.name, Array(3,4,5)) node1.intList shouldBe IndexedSeq(1,2,3) edge1.intList shouldBe IndexedSeq(3,4,5) @@ -175,7 +175,7 @@ class Schema04Test extends AnyWordSpec with Matchers { val node1 = NewNode1() .bool(true) .str("foo") - .intList(Seq(1,2,3): Seq[Integer]) + .intList(Seq(1,2,3)) val node2 = NewNode1().node1Inner(node1) diff --git a/integration-tests/tests/src/test/scala/Schema05Test.scala b/integration-tests/tests/src/test/scala/Schema05Test.scala index a8ad213b..48dbf7ba 100644 --- a/integration-tests/tests/src/test/scala/Schema05Test.scala +++ b/integration-tests/tests/src/test/scala/Schema05Test.scala @@ -49,15 +49,15 @@ class Schema05Test extends AnyWordSpec with Matchers { val node2 = graph.addNode(Node1.Label).asInstanceOf[Node1] val edge1 = node1.addEdge(Edge1.Label, node2).asInstanceOf[Edge1] val properties = Seq( - Properties.BOOL.of(false), - Properties.STR.of("foo"), - Properties.BYTE.of(100: Byte), - Properties.SHORT.of(101: Short), - Properties.INT.of(102), - Properties.LONG.of(103), - Properties.FLOAT.of(104.4f), - Properties.DOUBLE.of(105.5), - Properties.CHAR.of('Z'), + Properties.Bool.of(false), + Properties.Str.of("foo"), + Properties.Byte.of(100: Byte), + Properties.Short.of(101: Short), + Properties.Int.of(102), + Properties.Long.of(103), + Properties.Float.of(104.4f), + Properties.Double.of(105.5), + Properties.Char.of('Z'), ) properties.foreach(node1.setProperty) properties.foreach(edge1.setProperty)