diff --git a/api/snakeyaml-engine-kmp.api b/api/snakeyaml-engine-kmp.api index 2b23ec57c..ae2afeaed 100644 --- a/api/snakeyaml-engine-kmp.api +++ b/api/snakeyaml-engine-kmp.api @@ -38,6 +38,7 @@ public final class it/krzeminski/snakeyaml/engine/kmp/api/DumpSettings { public static final fun builder ()Lit/krzeminski/snakeyaml/engine/kmp/api/DumpSettingsBuilder; public final fun getCustomProperty (Lit/krzeminski/snakeyaml/engine/kmp/api/SettingKey;)Ljava/lang/Object; public final fun isCanonical ()Z + public final fun isDereferenceAliases ()Z public final fun isExplicitEnd ()Z public final fun isExplicitStart ()Z public final fun isMultiLineFlow ()Z @@ -57,6 +58,7 @@ public final class it/krzeminski/snakeyaml/engine/kmp/api/DumpSettingsBuilder { public final fun setCustomProperty (Lit/krzeminski/snakeyaml/engine/kmp/api/SettingKey;Ljava/lang/Object;)Lit/krzeminski/snakeyaml/engine/kmp/api/DumpSettingsBuilder; public final fun setDefaultFlowStyle (Lit/krzeminski/snakeyaml/engine/kmp/common/FlowStyle;)Lit/krzeminski/snakeyaml/engine/kmp/api/DumpSettingsBuilder; public final fun setDefaultScalarStyle (Lit/krzeminski/snakeyaml/engine/kmp/common/ScalarStyle;)Lit/krzeminski/snakeyaml/engine/kmp/api/DumpSettingsBuilder; + public final fun setDereferenceAliases (Z)Lit/krzeminski/snakeyaml/engine/kmp/api/DumpSettingsBuilder; public final fun setDumpComments (Z)Lit/krzeminski/snakeyaml/engine/kmp/api/DumpSettingsBuilder; public final fun setExplicitEnd (Z)Lit/krzeminski/snakeyaml/engine/kmp/api/DumpSettingsBuilder; public final fun setExplicitRootTag (Lit/krzeminski/snakeyaml/engine/kmp/nodes/Tag;)Lit/krzeminski/snakeyaml/engine/kmp/api/DumpSettingsBuilder; diff --git a/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/api/DumpSettings.kt b/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/api/DumpSettings.kt index d87cd5dfc..f4f3f7a3c 100644 --- a/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/api/DumpSettings.kt +++ b/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/api/DumpSettings.kt @@ -52,6 +52,7 @@ class DumpSettings internal constructor( private val customProperties: Map, @JvmField val indentWithIndicator: Boolean, @JvmField val dumpComments: Boolean, + val isDereferenceAliases: Boolean, ) { fun getCustomProperty(key: SettingKey): Any? = customProperties[key] diff --git a/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/api/DumpSettingsBuilder.kt b/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/api/DumpSettingsBuilder.kt index 18ad7181d..6446cbc53 100644 --- a/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/api/DumpSettingsBuilder.kt +++ b/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/api/DumpSettingsBuilder.kt @@ -53,6 +53,7 @@ class DumpSettingsBuilder internal constructor() { private var maxSimpleKeyLength = 128 private var indentWithIndicator = false private var dumpComments = false + private var isDereferenceAliases = false private var schema: Schema = JsonSchema() /** @@ -328,6 +329,23 @@ class DumpSettingsBuilder internal constructor() { return this } + /** + * + * Disable usage of anchors and aliases while serialising an instance. Recursive objects will not + * work when they are disabled. (Forces Serializer to skip emitting anchors names, emit Node + * content instead of Alias, fail with SerializationException if serialized structure is + * recursive.) + * + * @param dereferenceAliases - true to use copies of objects instead of references to the same + * instance + * @return the builder with the provided value + */ + fun setDereferenceAliases(dereferenceAliases: Boolean): DumpSettingsBuilder { + this.isDereferenceAliases = dereferenceAliases + return this + } + + /** * Create immutable DumpSettings * @@ -358,6 +376,7 @@ class DumpSettingsBuilder internal constructor() { customProperties = customProperties, indentWithIndicator = indentWithIndicator, dumpComments = dumpComments, + isDereferenceAliases = isDereferenceAliases, ) } } diff --git a/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/serializer/IdentitySet.kt b/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/serializer/IdentitySet.kt new file mode 100644 index 000000000..c7872f42f --- /dev/null +++ b/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/serializer/IdentitySet.kt @@ -0,0 +1,29 @@ +package it.krzeminski.snakeyaml.engine.kmp.serializer + +import it.krzeminski.snakeyaml.engine.kmp.internal.IdentityHashCode +import it.krzeminski.snakeyaml.engine.kmp.internal.identityHashCode + +/** + * A set that compares objects by their identities, not values. + * It's an attempt to reimplement `Collections.newSetFromMap(new IdentityHashMap())` + * from the JVM. + */ +internal class IdentitySet { + private val contents: MutableSet = mutableSetOf() + + fun add(obj: T) { + contents.add(identityHashCode(obj)) + } + + fun contains(obj: T): Boolean { + return contents.contains(identityHashCode(obj)) + } + + fun clear() { + contents.clear() + } + + fun remove(obj: T) { + contents.remove(identityHashCode(obj)) + } +} diff --git a/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/serializer/Serializer.kt b/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/serializer/Serializer.kt index 013580757..23102040c 100644 --- a/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/serializer/Serializer.kt +++ b/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/serializer/Serializer.kt @@ -18,6 +18,7 @@ import it.krzeminski.snakeyaml.engine.kmp.comments.CommentLine import it.krzeminski.snakeyaml.engine.kmp.common.Anchor import it.krzeminski.snakeyaml.engine.kmp.emitter.Emitable import it.krzeminski.snakeyaml.engine.kmp.events.* +import it.krzeminski.snakeyaml.engine.kmp.exceptions.YamlEngineException import it.krzeminski.snakeyaml.engine.kmp.nodes.* /** @@ -33,6 +34,8 @@ class Serializer( ) { private val serializedNodes: MutableSet = mutableSetOf() private val anchors: MutableMap = mutableMapOf() + private val isDereferenceAliases: Boolean = settings.isDereferenceAliases + private val recursive: IdentitySet = IdentitySet() /** * Serialize document @@ -55,6 +58,7 @@ class Serializer( emitable.emit(DocumentEndEvent(settings.isExplicitEnd)) serializedNodes.clear() anchors.clear() + recursive.clear() } /** Emit [StreamStartEvent] */ @@ -110,8 +114,18 @@ class Serializer( private fun serializeNode(node: Node) { val realNode = if (node is AnchorNode) node.realNode else node - val tAlias = (anchors[realNode]) - if (serializedNodes.contains(realNode)) { + + if (isDereferenceAliases && recursive.contains(node)) { + throw YamlEngineException("Cannot dereference aliases for recursive structures.") + } + recursive.add(node) + val tAlias = if (!isDereferenceAliases) { + anchors[realNode] + } else { + null + } + + if (!isDereferenceAliases && serializedNodes.contains(realNode)) { emitable.emit(AliasEvent(tAlias)) } else { serializedNodes.add(realNode) @@ -184,6 +198,7 @@ class Serializer( NodeType.ANCHOR -> {} } } + recursive.remove(node) } private fun serializeComments(comments: List?) { diff --git a/src/commonTest/kotlin/it/krzeminski/snakeyaml/engine/kmp/serializer/IdentitySetTest.kt b/src/commonTest/kotlin/it/krzeminski/snakeyaml/engine/kmp/serializer/IdentitySetTest.kt new file mode 100644 index 000000000..e5e2f94e6 --- /dev/null +++ b/src/commonTest/kotlin/it/krzeminski/snakeyaml/engine/kmp/serializer/IdentitySetTest.kt @@ -0,0 +1,54 @@ +package it.krzeminski.snakeyaml.engine.kmp.serializer + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +data class TestClass(val value: String) + +class IdentitySetTest : FunSpec({ + val objectFoo1 = TestClass(value = "foo") + val objectFoo2 = TestClass(value = "foo") + val objectBar = TestClass(value = "bar") + + test("expecting existence of the same object wrt. identity") { + val identitySet = IdentitySet() + identitySet.add(objectFoo1) + identitySet.contains(objectFoo1) shouldBe true + } + + test("comparing two objects that are equal according to 'equals'") { + val identitySet = IdentitySet() + identitySet.add(objectFoo1) + identitySet.contains(objectFoo2) shouldBe false + } + + test("comparing two objects that are not equal according to 'equals'") { + val identitySet = IdentitySet() + identitySet.add(objectFoo1) + identitySet.contains(objectBar) shouldBe false + } + + test("clearing the set") { + val identitySet = IdentitySet() + identitySet.add(objectFoo1) + identitySet.contains(objectFoo1) shouldBe true + identitySet.clear() + identitySet.contains(objectFoo1) shouldBe false + } + + test("removing the same object") { + val identitySet = IdentitySet() + identitySet.add(objectFoo1) + identitySet.contains(objectFoo1) shouldBe true + identitySet.remove(objectFoo1) + identitySet.contains(objectFoo1) shouldBe false + } + + test("removing object that is equal according to 'equals'") { + val identitySet = IdentitySet() + identitySet.add(objectFoo1) + identitySet.contains(objectFoo1) shouldBe true + identitySet.remove(objectFoo2) + identitySet.contains(objectFoo1) shouldBe true + } +}) diff --git a/src/jvmTest/java/it/krzeminski/snakeyaml/engine/kmp/usecases/references/DereferenceAliasesTest.kt b/src/jvmTest/java/it/krzeminski/snakeyaml/engine/kmp/usecases/references/DereferenceAliasesTest.kt new file mode 100644 index 000000000..76f34085d --- /dev/null +++ b/src/jvmTest/java/it/krzeminski/snakeyaml/engine/kmp/usecases/references/DereferenceAliasesTest.kt @@ -0,0 +1,47 @@ +package it.krzeminski.snakeyaml.engine.kmp.usecases.references + +import it.krzeminski.snakeyaml.engine.kmp.api.Dump +import it.krzeminski.snakeyaml.engine.kmp.api.DumpSettings +import it.krzeminski.snakeyaml.engine.kmp.api.Load +import it.krzeminski.snakeyaml.engine.kmp.api.LoadSettings +import it.krzeminski.snakeyaml.engine.kmp.common.FlowStyle +import it.krzeminski.snakeyaml.engine.kmp.exceptions.YamlEngineException +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.snakeyaml.engine.v2.utils.TestUtils +import java.io.StringWriter + +@Tag("fast") +class DereferenceAliasesTest { + @Test + fun testNoAliases() { + val settings = LoadSettings.builder().build() + val load = Load(settings) + val map = load.loadFromString(TestUtils.getResource("/issues/issue1086-1-input.yaml")) as Map<*, *>? + val setting = DumpSettings.builder().setDefaultFlowStyle(FlowStyle.BLOCK) + .setDereferenceAliases(true).build() + val dump = Dump(setting) + val node = dump.dumpToString(map) + val out = StringWriter() + val expected = TestUtils.getResource("/issues/issue1086-1-expected.yaml") + assertEquals(expected, node) + } + + @Test + fun testNoAliasesRecursive() { + val settings = LoadSettings.builder().build() + val load = Load(settings) + val map = load.loadFromString(TestUtils.getResource("/issues/issue1086-2-input.yaml")) as Map<*, *>? + val setting = DumpSettings.builder().setDefaultFlowStyle(FlowStyle.BLOCK) + .setDereferenceAliases(true).build() + val dump = Dump(setting) + try { + dump.dumpToString(map) + fail() + } catch (e: YamlEngineException) { + assertEquals("Cannot dereference aliases for recursive structures.", e.message) + } + } +} diff --git a/src/jvmTest/resources/issues/issue1086-1-expected.yaml b/src/jvmTest/resources/issues/issue1086-1-expected.yaml new file mode 100644 index 000000000..a58d61046 --- /dev/null +++ b/src/jvmTest/resources/issues/issue1086-1-expected.yaml @@ -0,0 +1,21 @@ +defines: + serverPattern1: + type: t3 + strage: 500 + serverPattern2: + type: t2 + strage: 250 + lbPattern1: + name: lbForPublic + vpc: vpc1 + lbPattern2: + name: lbForInternal + vpc: vpc1 +current: + assenbled1: + server: + type: t3 + strage: 500 + lb: + name: lbForPublic + vpc: vpc1 diff --git a/src/jvmTest/resources/issues/issue1086-1-input.yaml b/src/jvmTest/resources/issues/issue1086-1-input.yaml new file mode 100644 index 000000000..c5b0ddf59 --- /dev/null +++ b/src/jvmTest/resources/issues/issue1086-1-input.yaml @@ -0,0 +1,17 @@ +defines: + serverPattern1: &server_HighPerformance + type: t3 + strage: 500 + serverPattern2: &server_LowPerformance + type: t2 + strage: 250 + lbPattern1: &lb_Public + name: lbForPublic + vpc: vpc1 + lbPattern2: &lb_Internal + name: lbForInternal + vpc: vpc1 +current: + assenbled1: + server: *server_HighPerformance + lb: *lb_Public diff --git a/src/jvmTest/resources/issues/issue1086-2-input.yaml b/src/jvmTest/resources/issues/issue1086-2-input.yaml new file mode 100644 index 000000000..9b7088c72 --- /dev/null +++ b/src/jvmTest/resources/issues/issue1086-2-input.yaml @@ -0,0 +1,34 @@ +&id002 +bankAccountOwner: &id001 + bankAccountOwner: *id001 + birthPlace: Leningrad + birthday: 1970-01-12T13:46:40Z + children: &id003 + - *id002 + - bankAccountOwner: *id001 + birthPlace: New York + birthday: 1983-04-24T02:40:00Z + children: [] + father: *id001 + mother: &id004 + bankAccountOwner: *id001 + birthPlace: Saint-Petersburg + birthday: 1973-03-03T09:46:40Z + children: *id003 + father: null + mother: null + name: Mother + partner: *id001 + name: Daughter + partner: null + father: null + mother: null + name: Father + partner: *id004 +birthPlace: Munich +birthday: 1979-10-28T23:06:40Z +children: [] +father: *id001 +mother: *id004 +name: Son +partner: null diff --git a/upstream-commit.txt b/upstream-commit.txt index bb49f2a6f..bde084b67 100644 --- a/upstream-commit.txt +++ b/upstream-commit.txt @@ -1 +1 @@ -3d97ee77ff70ab1093a63e4d3dadda9e885fe857 +1af7dcd2c9c64a9d7cd9de3cd5f64bb2fc2aab6a