-
Notifications
You must be signed in to change notification settings - Fork 651
Structured cbor #3036
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Structured cbor #3036
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
/* | ||
* Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
@file:Suppress("unused") | ||
@file:OptIn(ExperimentalUnsignedTypes::class) | ||
|
||
package kotlinx.serialization.cbor | ||
|
||
import kotlinx.serialization.* | ||
import kotlinx.serialization.cbor.internal.* | ||
|
||
/** | ||
* Class representing single CBOR element. | ||
* Can be [CborPrimitive], [CborMap] or [CborList]. | ||
* | ||
* [CborElement.toString] properly prints CBOR tree as a human-readable representation. | ||
* Whole hierarchy is serializable, but only when used with [Cbor] as [CborElement] is purely CBOR-specific structure | ||
* which has a meaningful schemaless semantics only for CBOR. | ||
* | ||
* The whole hierarchy is [serializable][Serializable] only by [Cbor] format. | ||
*/ | ||
@Serializable(with = CborElementSerializer::class) | ||
public sealed class CborElement( | ||
/** | ||
* CBOR tags associated with this element. | ||
* Tags are optional semantic tagging of other major types (major type 6). | ||
* See [RFC 8949 3.4. Tagging of Items](https://datatracker.ietf.org/doc/html/rfc8949#name-tagging-of-items). | ||
*/ | ||
@OptIn(ExperimentalUnsignedTypes::class) | ||
public val tags: ULongArray = ulongArrayOf() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You probably want to keep out the default initializer to ensure that any concrete subclass passes the value (it is sealed so it is not a usability issue to require it to be passed along). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that if tags are uncommon you should probably make it nullable to avoid the overhead of the empty array. Alternatively you could create an internal empty array value that is reused. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'd go for an internal empty array. It does not make sense to differentiate between a null value and an empty array |
||
) | ||
|
||
/** | ||
* Class representing CBOR primitive value. | ||
* CBOR primitives include numbers, strings, booleans, byte arrays and special null value [CborNull]. | ||
*/ | ||
@Serializable(with = CborPrimitiveSerializer::class) | ||
public sealed class CborPrimitive( | ||
tags: ULongArray = ulongArrayOf() | ||
) : CborElement(tags) | ||
|
||
/** | ||
* Class representing signed CBOR integer (major type 1). | ||
*/ | ||
@Serializable(with = CborIntSerializer::class) | ||
public class CborNegativeInt( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The name NegativeInt is misleading as it appears to mean a value that can only be negative, not a signed value (that can be positive or negative). Given this name is poor, you also want to rename the counterpart from PositiveInt. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That was intentional. IIRC only negative ints are encoded as signed int in cbor, so it really must be a negative int There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But what if they are created programmatically? If you have this value based type you need a factory that creates the correct version on demand (and probably have an "CborInt" parent type that is sign independent) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a sealed parent makes sense (with a custom invoke function that returns the subtype based on sign), but the name for this class is the only one i could think of that is actually accurate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @JesusMcCloud With the explanation it certainly does make sense to call it positive/negative, although you could also just have a single type and make the distinction only on writing (as it is value dependent and deterministic anyway). Maybe the single type is best as that also avoids instances being created with invalid values. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it definitely makes life easier when you don't parse, but create. when parsing, though, I am slightly leaning towards being very explicit and very close to the wire format (which is what we're representing). I'll let @sandwwraith decide |
||
public val value: Long, | ||
tags: ULongArray = ulongArrayOf() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above, remove the default There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well actually, I am confident that all empty arrays of primitives delegate to a Singleton, so no premature optimisation planned here and in all other places |
||
) : CborPrimitive(tags) { | ||
init { | ||
require(value < 0) { "Number must be negative: $value" } | ||
} | ||
|
||
override fun equals(other: Any?): Boolean = | ||
other is CborNegativeInt && other.value == value && other.tags.contentEquals(tags) | ||
|
||
override fun hashCode(): Int = value.hashCode() * 31 + tags.contentHashCode() | ||
} | ||
|
||
/** | ||
* Class representing unsigned CBOR integer (major type 0). | ||
*/ | ||
@Serializable(with = CborUIntSerializer::class) | ||
public class CborPositiveInt( | ||
public val value: ULong, | ||
tags: ULongArray = ulongArrayOf() | ||
) : CborPrimitive(tags) { | ||
|
||
override fun equals(other: Any?): Boolean = | ||
other is CborPositiveInt && other.value == value && other.tags.contentEquals(tags) | ||
|
||
override fun hashCode(): Int = value.hashCode() * 31 + tags.contentHashCode() | ||
} | ||
|
||
/** | ||
* Class representing CBOR floating point value (major type 7). | ||
*/ | ||
@Serializable(with = CborDoubleSerializer::class) | ||
public class CborDouble( | ||
public val value: Double, | ||
tags: ULongArray = ulongArrayOf() | ||
) : CborPrimitive(tags) { | ||
|
||
override fun equals(other: Any?): Boolean = | ||
other is CborDouble && other.value == value && other.tags.contentEquals(tags) | ||
|
||
override fun hashCode(): Int = value.hashCode() * 31 + tags.contentHashCode() | ||
} | ||
|
||
/** | ||
* Class representing CBOR string value. | ||
*/ | ||
@Serializable(with = CborStringSerializer::class) | ||
public class CborString( | ||
public val value: String, | ||
tags: ULongArray = ulongArrayOf() | ||
) : CborPrimitive(tags) { | ||
|
||
override fun equals(other: Any?): Boolean = | ||
other is CborString && other.value == value && other.tags.contentEquals(tags) | ||
|
||
override fun hashCode(): Int = value.hashCode() * 31 + tags.contentHashCode() | ||
} | ||
|
||
/** | ||
* Class representing CBOR boolean value. | ||
*/ | ||
@Serializable(with = CborBooleanSerializer::class) | ||
public class CborBoolean( | ||
private val value: Boolean, | ||
tags: ULongArray = ulongArrayOf() | ||
) : CborPrimitive(tags) { | ||
|
||
/** | ||
* Returns the boolean value. | ||
*/ | ||
public val boolean: Boolean get() = value | ||
|
||
override fun equals(other: Any?): Boolean = | ||
other is CborBoolean && other.value == value && other.tags.contentEquals(tags) | ||
|
||
override fun hashCode(): Int = value.hashCode() * 31 + tags.contentHashCode() | ||
} | ||
|
||
/** | ||
* Class representing CBOR byte string value. | ||
*/ | ||
@Serializable(with = CborByteStringSerializer::class) | ||
public class CborByteString( | ||
private val value: ByteArray, | ||
tags: ULongArray = ulongArrayOf() | ||
) : CborPrimitive(tags) { | ||
|
||
/** | ||
* Returns the byte array value. | ||
*/ | ||
public val bytes: ByteArray get() = value.copyOf() | ||
|
||
override fun equals(other: Any?): Boolean = | ||
other is CborByteString && other.value.contentEquals(value) && other.tags.contentEquals(tags) | ||
|
||
override fun hashCode(): Int = value.contentHashCode() * 31 + tags.contentHashCode() | ||
} | ||
|
||
/** | ||
* Class representing CBOR `null` value | ||
*/ | ||
@Serializable(with = CborNullSerializer::class) | ||
public class CborNull(tags: ULongArray=ulongArrayOf()) : CborPrimitive(tags) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The default value should remain, but use an internal array (or null) to avoid extensive copies of empty arrays. |
||
// Note: CborNull is an object, so it cannot have constructor parameters for tags | ||
// If tags are needed for null values, this would need to be changed to a class | ||
override fun equals(other: Any?): Boolean { | ||
if (this === other) return true | ||
if (other !is CborNull) return false | ||
return true | ||
} | ||
|
||
override fun hashCode(): Int { | ||
return this::class.hashCode() | ||
} | ||
} | ||
|
||
/** | ||
* Class representing CBOR map, consisting of key-value pairs, where both key and value are arbitrary [CborElement] | ||
* | ||
* Since this class also implements [Map] interface, you can use | ||
* traditional methods like [Map.get] or [Map.getValue] to obtain CBOR elements. | ||
*/ | ||
@Serializable(with = CborMapSerializer::class) | ||
public class CborMap( | ||
private val content: Map<CborElement, CborElement>, | ||
tags: ULongArray = ulongArrayOf() | ||
) : CborElement(tags), Map<CborElement, CborElement> by content { | ||
|
||
public override fun equals(other: Any?): Boolean = | ||
other is CborMap && other.content == content && other.tags.contentEquals(tags) | ||
|
||
public override fun hashCode(): Int = content.hashCode() * 31 + tags.contentHashCode() | ||
|
||
public override fun toString(): String = content.toString() | ||
} | ||
|
||
/** | ||
* Class representing CBOR array, consisting of CBOR elements. | ||
* | ||
* Since this class also implements [List] interface, you can use | ||
* traditional methods like [List.get] or [List.size] to obtain CBOR elements. | ||
*/ | ||
@Serializable(with = CborListSerializer::class) | ||
public class CborList( | ||
private val content: List<CborElement>, | ||
tags: ULongArray = ulongArrayOf() | ||
) : CborElement(tags), List<CborElement> by content { | ||
|
||
public override fun equals(other: Any?): Boolean = | ||
other is CborList && other.content == content && other.tags.contentEquals(tags) | ||
|
||
public override fun hashCode(): Int = content.hashCode() * 31 + tags.contentHashCode() | ||
|
||
public override fun toString(): String = content.toString() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As these parameters change the behaviour of the system you should not change the default. Instead you could provide a way for users to choose a newer/different configuration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks! definitely not intended