Skip to content

Commit bc15631

Browse files
committed
Merge branch 'feature/MultiMap' into develop
Previously, the `TopicData.Relationships` collection mapped to a custom `RelationshipDataCollection` or `RelationshipData` objects with a `Relationships` property. But there's nothing relationship-specific about the collection. And, indeed, in OnTopic 5.0.0, the corresponding `Topic.Relationships` property uses a new `TopicMultiMap` as the base class, which doesn't rely on relationship semantics. To maintain parity with that, the `RelationshipDataCollection` is renamed to `MultiMap`, thus mapping to `TopicMultiMap`, and the `RelationshipData` class is renamed to `KeyValuesPair`, thus mapping to `KeyValuesPair<T>` in the OnTopic Library. In addition, a new `KeyValuesPairConverter` has been introduced which handles backward compatibility with legacy JSON that uses the `Relationships` nomenclature instead of the new `Values` nomenclature. The name `MultiMap` might seem a bit generic. I evaluated using `TopicDataMultiMap` instead, which provides a more direct association with `TopicMultiMap`. But while the `Collection<String>` used for the `Values` property is _intended_ to be used for mapping `TopicData.UniqueKey` values, there's nothing about it that's `TopicData` specific. Further, the name `TopicDataMultiMap` might suggest that it will expose a collection of collections containing `TopicData` references, instead of just raw strings which can be used to refer to `TopicData` instances. Fortunately, the name `MultiMap` doesn't occur in the JSON data, so we can rename it in the future with minimal concern for backward compatibility if it really becomes an issue. In the meanwhile, the more general `Values` property used in `KeyValuesPair` _is_ exposed to the JSON, and offers not only more intuitive semantics for relationships—by avoiding e.g. `TopicData.Relationships[{Relationships:[]}]`—but also offers a more reusable data model in case we need to map other multimaps in the future (possibly for other `TopicMultiMap` properties).
2 parents 2f8ca6b + a40dc64 commit bc15631

File tree

10 files changed

+214
-59
lines changed

10 files changed

+214
-59
lines changed

OnTopic.Data.Transfer.Tests/DeserializationTest.cs

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Linq;
88
using System.Text.Json;
99
using Microsoft.VisualStudio.TestTools.UnitTesting;
10+
using OnTopic.Data.Transfer.Converters;
1011

1112
namespace OnTopic.Data.Transfer.Tests {
1213

@@ -95,30 +96,60 @@ public void Deserialize_DerivedTopicKey_ReturnsExpectedResults() {
9596
}
9697
#pragma warning restore CS0618 // Type or member is obsolete
9798

99+
/*==========================================================================================================================
100+
| TEST: DESERIALIZE: KEY/VALUES PAIR: RETURNS EXPECTED RESULTS
101+
\-------------------------------------------------------------------------------------------------------------------------*/
102+
/// <summary>
103+
/// Creates a JSON string and attempts to deserialize it as a <see cref="KeyValuesPair"/> class.
104+
/// </summary>
105+
[TestMethod]
106+
public void Deserialize_KeyValuesPair_ReturnsExpectedResults() {
107+
108+
var sourceData = new KeyValuesPair() {
109+
Key = "Test"
110+
};
111+
sourceData.Values.Add("Root:Web");
112+
113+
var json = $"{{" +
114+
$"\"Key\":\"{sourceData.Key}\"," +
115+
$"\"Values\":[\"Root:Web\"]" +
116+
$"}}";
117+
118+
var keyValuesPair = JsonSerializer.Deserialize<KeyValuesPair>(json);
119+
120+
Assert.AreEqual<string>(sourceData.Key, keyValuesPair.Key);
121+
Assert.AreEqual<int>(sourceData.Values.Count, keyValuesPair.Values.Count);
122+
Assert.AreEqual<string>(sourceData.Values.FirstOrDefault(), keyValuesPair.Values.FirstOrDefault());
123+
124+
}
125+
98126
/*==========================================================================================================================
99127
| TEST: DESERIALIZE: RELATIONSHIP DATA: RETURNS EXPECTED RESULTS
100128
\-------------------------------------------------------------------------------------------------------------------------*/
101129
/// <summary>
102-
/// Creates a json string and attempts to deserialize it as a <see cref="RelationshipData"/> class.
130+
/// Creates a JSON string representing the legacy <c>RelationshipData</c> class (which used a <c>Relationships</c> array),
131+
/// and attempts to deserialize it as a <see cref="KeyValuesPair"/> class, ensuring that the <see cref="
132+
/// KeyValuesPairConverter"/> properly translates the <c>Relationships</c> array to the <see cref="KeyValuesPair.Values"/>
133+
/// collection.
103134
/// </summary>
104135
[TestMethod]
105136
public void Deserialize_RelationshipData_ReturnsExpectedResults() {
106137

107-
var sourceData = new RelationshipData() {
138+
var sourceData = new KeyValuesPair() {
108139
Key = "Test"
109140
};
110-
sourceData.Relationships.Add("Root:Web");
141+
sourceData.Values.Add("Root:Web");
111142

112143
var json = $"{{" +
113144
$"\"Key\":\"{sourceData.Key}\"," +
114145
$"\"Relationships\":[\"Root:Web\"]" +
115146
$"}}";
116147

117-
var relationshipData = JsonSerializer.Deserialize<RelationshipData>(json);
148+
var keyValuesPair = JsonSerializer.Deserialize<KeyValuesPair>(json);
118149

119-
Assert.AreEqual<string>(sourceData.Key, relationshipData.Key);
120-
Assert.AreEqual<int>(sourceData.Relationships.Count, relationshipData.Relationships.Count);
121-
Assert.AreEqual<string>(sourceData.Relationships.FirstOrDefault(), relationshipData.Relationships.FirstOrDefault());
150+
Assert.AreEqual<string>(sourceData.Key, keyValuesPair.Key);
151+
Assert.AreEqual<int>(sourceData.Values.Count, keyValuesPair.Values.Count);
152+
Assert.AreEqual<string>(sourceData.Values.FirstOrDefault(), keyValuesPair.Values.FirstOrDefault());
122153

123154
}
124155

@@ -165,7 +196,7 @@ public void Deserialize_TopicGraph_ReturnsExpectedResults() {
165196
UniqueKey = "Root:Test",
166197
ContentType = "Container"
167198
};
168-
var sourceRelationshipData= new RelationshipData() {
199+
var sourceRelationshipData= new KeyValuesPair() {
169200
Key = "Test"
170201
};
171202
var sourceAttributeData = new RecordData() {
@@ -183,7 +214,7 @@ public void Deserialize_TopicGraph_ReturnsExpectedResults() {
183214
ContentType = "Container"
184215
};
185216

186-
sourceRelationshipData.Relationships.Add("Root:Web");
217+
sourceRelationshipData.Values.Add("Root:Web");
187218
sourceTopicData.Relationships.Add(sourceRelationshipData);
188219
sourceTopicData.Attributes.Add(sourceAttributeData);
189220
sourceTopicData.Children.Add(sourceChildTopicData);
@@ -202,7 +233,7 @@ public void Deserialize_TopicGraph_ReturnsExpectedResults() {
202233
$"\"Relationships\":[" +
203234
$"{{" +
204235
$"\"Key\":\"{sourceRelationshipData.Key}\"," +
205-
$"\"Relationships\":[\"Root:Web\"]" +
236+
$"\"Values\":[\"Root:Web\"]" +
206237
$"}}" +
207238
$"]," +
208239
$"\"References\":[" +
@@ -239,8 +270,8 @@ public void Deserialize_TopicGraph_ReturnsExpectedResults() {
239270
Assert.AreEqual<int>(1, sourceTopicData.Children.Count);
240271

241272
Assert.AreEqual<string>(sourceRelationshipData.Key, relationshipData.Key);
242-
Assert.AreEqual<int?>(sourceRelationshipData.Relationships.Count, relationshipData.Relationships.Count);
243-
Assert.AreEqual<string>(sourceRelationshipData.Relationships.FirstOrDefault(), relationshipData.Relationships.FirstOrDefault());
273+
Assert.AreEqual<int?>(sourceRelationshipData.Values.Count, relationshipData.Values.Count);
274+
Assert.AreEqual<string>(sourceRelationshipData.Values.FirstOrDefault(), relationshipData.Values.FirstOrDefault());
244275

245276
Assert.AreEqual<string>(sourceReferenceData.Key, referenceData.Key);
246277
Assert.AreEqual<string>(sourceReferenceData.Value, referenceData.Value);

OnTopic.Data.Transfer.Tests/ImportOptionsTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ public void ImportAsReplace_TopicDataWithRelationships_DeletesOrphanedRelationsh
232232
UniqueKey = topic.GetUniqueKey(),
233233
ContentType = topic.ContentType
234234
};
235-
var relationshipData = new RelationshipData() {
235+
var relationshipData = new KeyValuesPair() {
236236
Key = "Related"
237237
};
238238

@@ -241,7 +241,7 @@ public void ImportAsReplace_TopicDataWithRelationships_DeletesOrphanedRelationsh
241241
topic.Relationships.SetValue("Cousin", relatedTopic3);
242242

243243
topicData.Relationships.Add(relationshipData);
244-
relationshipData.Relationships.Add(relatedTopic1.GetUniqueKey());
244+
relationshipData.Values.Add(relatedTopic1.GetUniqueKey());
245245

246246
topic.Import(
247247
topicData,

OnTopic.Data.Transfer.Tests/SerializationTest.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,25 +54,25 @@ public void Serialize_TopicData_ReturnsExpectedResults() {
5454
}
5555

5656
/*==========================================================================================================================
57-
| TEST: SERIALIZE: RELATIONSHIP DATA: RETURNS EXPECTED RESULTS
57+
| TEST: SERIALIZE: KEY/VALUES PAIR: RETURNS EXPECTED RESULTS
5858
\-------------------------------------------------------------------------------------------------------------------------*/
5959
/// <summary>
60-
/// Creates a <see cref="RelationshipData"/>, serializes it, and confirms the resulting JSON.
60+
/// Creates a <see cref="KeyValuesPair"/>, serializes it, and confirms the resulting JSON.
6161
/// </summary>
6262
[TestMethod]
63-
public void Serialize_RelationshipData_ReturnsExpectedResults() {
63+
public void Serialize_KeyValuesPair_ReturnsExpectedResults() {
6464

65-
var relationshipData = new RelationshipData() {
65+
var keyValuesPair = new KeyValuesPair() {
6666
Key = "Test"
6767
};
68-
relationshipData.Relationships.Add("Root:Web");
68+
keyValuesPair.Values.Add("Root:Web");
6969

7070
var expected = $"{{" +
71-
$"\"Key\":\"{relationshipData.Key}\"," +
72-
$"\"Relationships\":[\"Root:Web\"]" +
71+
$"\"Key\":\"{keyValuesPair.Key}\"," +
72+
$"\"Values\":[\"Root:Web\"]" +
7373
$"}}";
7474

75-
var json = JsonSerializer.Serialize(relationshipData);
75+
var json = JsonSerializer.Serialize(keyValuesPair);
7676

7777
Assert.AreEqual<string>(expected, json);
7878

@@ -118,7 +118,7 @@ public void Serialize_TopicGraph_ReturnsExpectedResults() {
118118
UniqueKey = "Root:Test",
119119
ContentType = "Container"
120120
};
121-
var relationshipData = new RelationshipData() {
121+
var relationshipData = new KeyValuesPair() {
122122
Key = "Test"
123123
};
124124
var referenceData = new RecordData() {
@@ -135,7 +135,7 @@ public void Serialize_TopicGraph_ReturnsExpectedResults() {
135135
ContentType = "Container"
136136
};
137137

138-
relationshipData.Relationships.Add("Root:Web");
138+
relationshipData.Values.Add("Root:Web");
139139
topicData.Relationships.Add(relationshipData);
140140
topicData.References.Add(referenceData);
141141
topicData.Attributes.Add(attributeData);
@@ -154,7 +154,7 @@ public void Serialize_TopicGraph_ReturnsExpectedResults() {
154154
$"\"Relationships\":[" +
155155
$"{{" +
156156
$"\"Key\":\"{relationshipData.Key}\"," +
157-
$"\"Relationships\":[\"Root:Web\"]" +
157+
$"\"Values\":[\"Root:Web\"]" +
158158
$"}}" +
159159
$"]," +
160160
$"\"References\":[" +

OnTopic.Data.Transfer.Tests/TopicExtensionsTest.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ public void Export_TopicWithRelationships_MapsRelationshipDataCollection() {
108108
Assert.IsNotNull(topicData);
109109
Assert.IsNotNull(childTopicData);
110110
Assert.AreEqual<int>(1, childTopicData.Relationships.Count);
111-
Assert.AreEqual<string>("Root:Related", childTopicData.Relationships.FirstOrDefault().Relationships.FirstOrDefault());
111+
Assert.AreEqual<string>("Root:Related", childTopicData.Relationships.FirstOrDefault().Values.FirstOrDefault());
112112

113113
}
114114

@@ -138,7 +138,7 @@ public void ExportWithExternalAssociations_TopicWithRelationships_ExcludesExtern
138138

139139
Assert.IsNotNull(topicData);
140140
Assert.AreEqual<int>(1, topicData.Relationships.Count);
141-
Assert.AreEqual<string>("Root:Related", topicData.Relationships.FirstOrDefault().Relationships.FirstOrDefault());
141+
Assert.AreEqual<string>("Root:Related", topicData.Relationships.FirstOrDefault().Values.FirstOrDefault());
142142

143143
}
144144

@@ -606,12 +606,12 @@ public void Import_TopicDataWithRelationships_MapsRelationshipCollection() {
606606
UniqueKey = topic.GetUniqueKey(),
607607
ContentType = topic.ContentType
608608
};
609-
var relationshipData = new RelationshipData() {
610-
Key = "Related"
609+
var relationshipData = new KeyValuesPair() {
610+
Key = "Related"
611611
};
612612

613613
topicData.Relationships.Add(relationshipData);
614-
relationshipData.Relationships.Add(relatedTopic.GetUniqueKey());
614+
relationshipData.Values.Add(relatedTopic.GetUniqueKey());
615615

616616
topic.Import(topicData);
617617

@@ -638,14 +638,14 @@ public void Import_TopicDataWithRelationships_MaintainsExisting() {
638638
UniqueKey = topic.GetUniqueKey(),
639639
ContentType = topic.ContentType
640640
};
641-
var relationshipData = new RelationshipData() {
642-
Key = "Related"
641+
var relationshipData = new KeyValuesPair() {
642+
Key = "Related"
643643
};
644644

645645
topic.Relationships.SetValue("Related", relatedTopic1);
646646

647647
topicData.Relationships.Add(relationshipData);
648-
relationshipData.Relationships.Add(relatedTopic2.GetUniqueKey());
648+
relationshipData.Values.Add(relatedTopic2.GetUniqueKey());
649649

650650
topic.Import(topicData);
651651

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*==============================================================================================================================
2+
| Author Ignia, LLC
3+
| Client Ignia, LLC
4+
| Project Topics Library
5+
\=============================================================================================================================*/
6+
using System;
7+
using System.Text.Json;
8+
using System.Text.Json.Serialization;
9+
using OnTopic.Internal.Diagnostics;
10+
11+
namespace OnTopic.Data.Transfer.Converters {
12+
13+
/*============================================================================================================================
14+
| CLASS: KEY/VALUES PAIR CONVERTER
15+
\---------------------------------------------------------------------------------------------------------------------------*/
16+
/// <summary>
17+
/// Provides instructions for serializing or deserializing a <see cref="KeyValuesPair"/> instance.
18+
/// </summary>
19+
/// <remarks>
20+
/// The converter allows backward compatibility with legacy conventions which used <c>Relationships</c> instead of <c>
21+
/// Values</c>.
22+
/// </remarks>
23+
class KeyValuesPairConverter: JsonConverter<KeyValuesPair> {
24+
25+
/*==========================================================================================================================
26+
| METHOD: READ
27+
\-------------------------------------------------------------------------------------------------------------------------*/
28+
/// <summary>
29+
/// Deserializes the data from JSON.
30+
/// </summary>
31+
public override KeyValuesPair Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
32+
33+
/*------------------------------------------------------------------------------------------------------------------------
34+
| Validate parameters
35+
\-----------------------------------------------------------------------------------------------------------------------*/
36+
if (reader.TokenType != JsonTokenType.StartObject) {
37+
throw new JsonException();
38+
}
39+
40+
/*------------------------------------------------------------------------------------------------------------------------
41+
| Retrieve data
42+
\-----------------------------------------------------------------------------------------------------------------------*/
43+
var key = (string?)null;
44+
var values = (string[]?)null;
45+
46+
while (reader.Read()) {
47+
if (reader.TokenType == JsonTokenType.EndObject) {
48+
break;
49+
}
50+
if (reader.TokenType != JsonTokenType.PropertyName) {
51+
throw new JsonException();
52+
}
53+
var propertyName = reader.GetString();
54+
if (propertyName is null) {
55+
continue;
56+
}
57+
if (propertyName.Equals("Key", StringComparison.OrdinalIgnoreCase)) {
58+
key = JsonSerializer.Deserialize<String>(ref reader, options);
59+
}
60+
else if (
61+
propertyName.Equals("Relationships", StringComparison.OrdinalIgnoreCase) ||
62+
propertyName.Equals("Values", StringComparison.OrdinalIgnoreCase)
63+
) {
64+
values = JsonSerializer.Deserialize<String[]>(ref reader, options);
65+
}
66+
}
67+
68+
/*------------------------------------------------------------------------------------------------------------------------
69+
| Validate data
70+
\-----------------------------------------------------------------------------------------------------------------------*/
71+
Contract.Assume(key, nameof(key));
72+
73+
/*------------------------------------------------------------------------------------------------------------------------
74+
| Create data
75+
\-----------------------------------------------------------------------------------------------------------------------*/
76+
var relationship = new KeyValuesPair() {
77+
Key = key,
78+
Values = new(values?? Array.Empty<string>())
79+
};
80+
81+
/*------------------------------------------------------------------------------------------------------------------------
82+
| Return data
83+
\-----------------------------------------------------------------------------------------------------------------------*/
84+
return relationship;
85+
86+
}
87+
88+
/*==========================================================================================================================
89+
| METHOD: WRITE
90+
\-------------------------------------------------------------------------------------------------------------------------*/
91+
/// <summary>
92+
/// Serializes the data from JSON.
93+
/// </summary>
94+
public override void Write(Utf8JsonWriter writer, KeyValuesPair relationshipData, JsonSerializerOptions options) {
95+
writer.WriteStartObject();
96+
writer.WritePropertyName("Key");
97+
writer.WriteStringValue(relationshipData.Key.ToString());
98+
writer.WriteStartArray("Values");
99+
foreach (var uniqueKey in relationshipData.Values) {
100+
writer.WriteStringValue(uniqueKey);
101+
}
102+
writer.WriteEndArray();
103+
writer.WriteEndObject();
104+
}
105+
106+
}
107+
}

OnTopic.Data.Transfer/Interchange/TopicExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,18 +122,18 @@ public static TopicData Export(this Topic topic, [NotNull]ExportOptions? options
122122
| Set relationships
123123
\-----------------------------------------------------------------------------------------------------------------------*/
124124
foreach (var relationship in topic.Relationships) {
125-
var relationshipData = new RelationshipData() {
125+
var relationshipData = new KeyValuesPair() {
126126
Key = relationship.Key,
127127
};
128128
foreach (var relatedTopic in relationship.Values) {
129129
if (
130130
options.IncludeExternalAssociations ||
131131
relatedTopic.GetUniqueKey().StartsWith(options.ExportScope, StringComparison.InvariantCultureIgnoreCase)
132132
) {
133-
relationshipData.Relationships.Add(relatedTopic.GetUniqueKey());
133+
relationshipData.Values.Add(relatedTopic.GetUniqueKey());
134134
}
135135
}
136-
if (relationshipData.Relationships.Count > 0) {
136+
if (relationshipData.Values.Count > 0) {
137137
topicData.Relationships.Add(relationshipData);
138138
}
139139
}
@@ -384,7 +384,7 @@ attribute.Value is not null &&
384384

385385
//Update records based on the source collection
386386
foreach (var relationship in topicData.Relationships) {
387-
foreach (var relatedTopicKey in relationship.Relationships) {
387+
foreach (var relatedTopicKey in relationship.Values) {
388388
var relatedTopic = topic.GetByUniqueKey(relatedTopicKey);
389389
if (relationship.Key is not null && relatedTopic is not null) {
390390
topic.Relationships.SetValue(relationship.Key, relatedTopic);

OnTopic.Data.Transfer/Interchange/UnresolvedAssociation.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ internal UnresolvedAssociation(AssociationType type, string key, Topic source, s
7474
/// </summary>
7575
/// <remarks>
7676
/// This maps to either the <see cref="RecordData.Key"/>, if the association is from <see cref="TopicData.References"/>,
77-
/// or <see cref="RelationshipData.Key"/>, if the association is from <see cref="TopicData.Relationships"/>.
77+
/// or <see cref="KeyValuesPair.Key"/>, if the association is from <see cref="TopicData.Relationships"/>.
7878
/// </remarks>
7979
internal string Key { get; init; }
8080

0 commit comments

Comments
 (0)