Introduces Bytes class (#1019)

This introduces a new `Bytes` standard library class, for working with
binary data.

* Add Bytes class to the standard library
* Change CLI to eval `output.bytes`
* Change code generators to map Bytes to respective underlying type
* Add subscript and concat operator support
* Add binary encoding for Bytes
* Add PCF and Plist rendering for Bytes

Co-authored-by: Kushal Pisavadia <kushi.p@gmail.com>
This commit is contained in:
Daniel Chao
2025-06-11 16:23:55 -07:00
committed by GitHub
parent 3bd8a88506
commit e9320557b7
104 changed files with 2210 additions and 545 deletions

View File

@@ -226,6 +226,9 @@ class KotlinCodeGenerator(
fun PClass.Property.isRegex(): Boolean =
(this.type as? PType.Class)?.pClass?.info == PClassInfo.Regex
fun PClass.Property.isByteArray(): Boolean =
(this.type as? PType.Class)?.pClass?.info == PClassInfo.Bytes
fun generateConstructor(): FunSpec {
val builder = FunSpec.constructorBuilder()
for ((name, property) in allProperties) {
@@ -302,12 +305,20 @@ class KotlinCodeGenerator(
.addStatement("other as %T", kotlinPoetClassName)
for ((propertyName, property) in allProperties) {
val accessor = if (property.isRegex()) "%N.pattern" else "%N"
builder.addStatement(
"if (this.$accessor != other.$accessor) return false",
propertyName,
propertyName,
)
if (property.isByteArray()) {
builder.addStatement(
"if (!this.%N.contentEquals(other.%N)) return false",
propertyName,
propertyName,
)
} else {
val accessor = if (property.isRegex()) "%N.pattern" else "%N"
builder.addStatement(
"if (this.$accessor != other.$accessor) return false",
propertyName,
propertyName,
)
}
}
builder.addStatement("return true")
@@ -322,14 +333,18 @@ class KotlinCodeGenerator(
.addStatement("var result = 1")
for ((propertyName, property) in allProperties) {
val accessor = if (property.isRegex()) "this.%N.pattern" else "this.%N"
// use Objects.hashCode() because Kotlin's Any?.hashCode()
// doesn't work for platform types (will get NPE if null)
builder.addStatement(
"result = 31 * result + %T.hashCode($accessor)",
Objects::class,
propertyName,
)
if (property.isByteArray()) {
builder.addStatement("result = 31 * result + this.%N.contentHashCode()", propertyName)
} else {
val accessor = if (property.isRegex()) "this.%N.pattern" else "this.%N"
// use Objects.hashCode() because Kotlin's Any?.hashCode()
// doesn't work for platform types (will get NPE if null)
builder.addStatement(
"result = 31 * result + %T.hashCode($accessor)",
Objects::class,
propertyName,
)
}
}
builder.addStatement("return result")
@@ -347,10 +362,15 @@ class KotlinCodeGenerator(
.apply {
add("%L", pClass.toKotlinPoetName().simpleName)
add("(")
for ((index, propertyName) in allProperties.keys.withIndex()) {
for ((index, entry) in allProperties.entries.withIndex()) {
val (propertyName, property) = entry
add(if (index == 0) "%L" else ", %L", propertyName)
add("=$")
add("%N", propertyName)
if (property.isByteArray()) {
add("=\${%T.toString(%L)}", Arrays::class, propertyName)
} else {
add("=$")
add("%N", propertyName)
}
}
add(")")
}
@@ -488,16 +508,16 @@ class KotlinCodeGenerator(
builder.addKdoc(renderAsKdoc(docComment))
}
var hasRegex = false
var hasRegexOrByteArray = false
for ((name, property) in properties) {
hasRegex = hasRegex || property.isRegex()
hasRegexOrByteArray = hasRegexOrByteArray || property.isRegex() || property.isByteArray()
builder.addProperty(generateProperty(name, property))
}
// kotlin.text.Regex (and java.util.regex.Pattern) defines equality as identity.
// To match Pkl semantics and compare regexes by their String pattern,
// override equals and hashCode if the data class has a property of type Regex.
if (hasRegex) {
// Regex and ByteArray define equaltiy as identity.
// To match Pkl semantics, override equals and hashCode if the data class has a property of
// type Regex or ByteArray.
if (hasRegexOrByteArray) {
builder.addFunction(generateEqualsMethod()).addFunction(generateHashCodeMethod())
}
@@ -632,6 +652,7 @@ class KotlinCodeGenerator(
PClassInfo.Float -> DOUBLE
PClassInfo.Duration -> DURATION
PClassInfo.DataSize -> DATA_SIZE
PClassInfo.Bytes -> BYTE_ARRAY
PClassInfo.Pair ->
KOTLIN_PAIR.parameterizedBy(
if (typeArguments.isEmpty()) ANY_NULL else typeArguments[0].toKotlinPoetName(),

View File

@@ -120,6 +120,7 @@ class KotlinCodeGeneratorTest {
any: Any
nonNull: NonNull
enum: Direction
bytes: Bytes
}
open class Other {
@@ -193,7 +194,6 @@ class KotlinCodeGeneratorTest {
@Test
fun testToString() {
val (_, propertyTypes) = instantiateOtherAndPropertyTypes()
assertThat(propertyTypes.toString())
.isEqualTo(
"""PropertyTypes(boolean=true, int=42, float=42.3, string=string, duration=5.min, """ +
@@ -204,7 +204,8 @@ class KotlinCodeGeneratorTest {
"""set2=[Other(name=pigeon)], map={1=one, 2=two}, map2={one=Other(name=pigeon), """ +
"""two=Other(name=pigeon)}, container={1=one, 2=two}, container2={one=Other(name=pigeon), """ +
"""two=Other(name=pigeon)}, other=Other(name=pigeon), regex=(i?)\w*, any=Other(name=pigeon), """ +
"""nonNull=Other(name=pigeon), enum=north)"""
"""nonNull=Other(name=pigeon), enum=north, """ +
"""bytes=[1, 2, 3, 4])"""
)
}
@@ -412,6 +413,7 @@ class KotlinCodeGeneratorTest {
assertThat(readProperty(propertyTypes, "regex")).isInstanceOf(Regex::class.java)
assertThat(readProperty(propertyTypes, "any")).isEqualTo(other)
assertThat(readProperty(propertyTypes, "nonNull")).isEqualTo(other)
assertThat(readProperty(propertyTypes, "bytes")).isEqualTo(byteArrayOf(1, 2, 3, 4))
}
private fun readProperty(receiver: Any, name: String): Any? {
@@ -571,6 +573,25 @@ class KotlinCodeGeneratorTest {
.contains("result = 31 * result + Objects.hashCode(this.name.pattern)")
}
@Test
fun `data class with ByteArray property has custom equals and hashCode methods`() {
val kotlinCode =
generateKotlinCode(
"""
module my.mod
class Foo {
bytes: Bytes
}
"""
.trimIndent()
)
assertThat(kotlinCode)
.contains("if (!this.bytes.contentEquals(other.bytes)) return false")
.contains("result = 31 * result + this.bytes.contentHashCode()")
}
@Test
fun `recursive types`() {
val kotlinCode =
@@ -2078,6 +2099,7 @@ class KotlinCodeGeneratorTest {
other,
other,
enumValue,
byteArrayOf(1, 2, 3, 4),
)
return other to propertyTypes

View File

@@ -1,8 +1,10 @@
package my
import java.util.Arrays
import java.util.Objects
import kotlin.Any
import kotlin.Boolean
import kotlin.ByteArray
import kotlin.Double
import kotlin.Int
import kotlin.Long
@@ -46,7 +48,8 @@ object Mod {
open val regex: Regex,
open val any: Any?,
open val nonNull: Any,
open val enum: Direction
open val enum: Direction,
open val bytes: ByteArray
) {
open fun copy(
boolean: Boolean = this.boolean,
@@ -75,10 +78,11 @@ object Mod {
regex: Regex = this.regex,
any: Any? = this.any,
nonNull: Any = this.nonNull,
enum: Direction = this.enum
enum: Direction = this.enum,
bytes: ByteArray = this.bytes
): PropertyTypes = PropertyTypes(boolean, int, float, string, duration, durationUnit, dataSize,
dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map,
map2, container, container2, other, regex, any, nonNull, enum)
map2, container, container2, other, regex, any, nonNull, enum, bytes)
override fun equals(other: Any?): Boolean {
if (this === other) return true
@@ -111,6 +115,7 @@ object Mod {
if (this.any != other.any) return false
if (this.nonNull != other.nonNull) return false
if (this.enum != other.enum) return false
if (!this.bytes.contentEquals(other.bytes)) return false
return true
}
@@ -143,11 +148,12 @@ object Mod {
result = 31 * result + Objects.hashCode(this.any)
result = 31 * result + Objects.hashCode(this.nonNull)
result = 31 * result + Objects.hashCode(this.enum)
result = 31 * result + this.bytes.contentHashCode()
return result
}
override fun toString(): String =
"""PropertyTypes(boolean=$boolean, int=$int, float=$float, string=$string, duration=$duration, durationUnit=$durationUnit, dataSize=$dataSize, dataSizeUnit=$dataSizeUnit, nullable=$nullable, nullable2=$nullable2, pair=$pair, pair2=$pair2, coll=$coll, coll2=$coll2, list=$list, list2=$list2, set=$set, set2=$set2, map=$map, map2=$map2, container=$container, container2=$container2, other=$other, regex=$regex, any=$any, nonNull=$nonNull, enum=$enum)"""
"""PropertyTypes(boolean=$boolean, int=$int, float=$float, string=$string, duration=$duration, durationUnit=$durationUnit, dataSize=$dataSize, dataSizeUnit=$dataSizeUnit, nullable=$nullable, nullable2=$nullable2, pair=$pair, pair2=$pair2, coll=$coll, coll2=$coll2, list=$list, list2=$list2, set=$set, set2=$set2, map=$map, map2=$map2, container=$container, container2=$container2, other=$other, regex=$regex, any=$any, nonNull=$nonNull, enum=$enum, bytes=${Arrays.toString(bytes)})"""
}
open class Other(