Skip to content

Commit 0a29586

Browse files
committed
Add TryAdd, Fix Add to throw on existing keys in dictionaries
1 parent 83d64d8 commit 0a29586

3 files changed

Lines changed: 109 additions & 36 deletions

File tree

ValveKeyValue/ValveKeyValue.Test/EdgeCaseTestCase.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,56 @@ public void AddStringKVObjectAddsNamedChild()
431431
Assert.That((string)obj["key1"], Is.EqualTo("value1"));
432432
}
433433

434+
[Test]
435+
public void AddThrowsOnDuplicateKeyInDictCollection()
436+
{
437+
var obj = KVObject.Collection();
438+
obj.Add("key", "value1");
439+
440+
Assert.That(() => obj.Add("key", "value2"), Throws.ArgumentException);
441+
}
442+
443+
[Test]
444+
public void AddAllowsDuplicateKeyInListCollection()
445+
{
446+
var obj = KVObject.ListCollection();
447+
obj.Add("key", "value1");
448+
obj.Add("key", "value2");
449+
450+
Assert.That(obj.Count, Is.EqualTo(2));
451+
}
452+
453+
[Test]
454+
public void TryAddReturnsFalseOnDuplicateKeyInDictCollection()
455+
{
456+
var obj = KVObject.Collection();
457+
obj.Add("key", "value1");
458+
459+
Assert.That(obj.TryAdd("key", "value2"), Is.False);
460+
Assert.That(obj.Count, Is.EqualTo(1));
461+
Assert.That((string)obj["key"], Is.EqualTo("value1"));
462+
}
463+
464+
[Test]
465+
public void TryAddReturnsTrueOnNewKeyInDictCollection()
466+
{
467+
var obj = KVObject.Collection();
468+
469+
Assert.That(obj.TryAdd("key", "value"), Is.True);
470+
Assert.That(obj.Count, Is.EqualTo(1));
471+
Assert.That((string)obj["key"], Is.EqualTo("value"));
472+
}
473+
474+
[Test]
475+
public void TryAddAlwaysReturnsTrueForListCollection()
476+
{
477+
var obj = KVObject.ListCollection();
478+
obj.Add("key", "value1");
479+
480+
Assert.That(obj.TryAdd("key", "value2"), Is.True);
481+
Assert.That(obj.Count, Is.EqualTo(2));
482+
}
483+
434484
#endregion
435485

436486
#region CreateArray from KVObject enumerable

ValveKeyValue/ValveKeyValue.Test/Test Data/apisurface.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public class ValveKeyValue.KVDocument
8484
public ushort ToUInt16(IFormatProvider provider);
8585
public uint ToUInt32(IFormatProvider provider);
8686
public ulong ToUInt64(IFormatProvider provider);
87+
public bool TryAdd(string key, ValveKeyValue.KVObject value);
8788
public bool TryGetValue(string name, out ValveKeyValue.KVObject& child);
8889
}
8990

@@ -239,6 +240,7 @@ public class ValveKeyValue.KVObject
239240
public ushort ToUInt16(IFormatProvider provider);
240241
public uint ToUInt32(IFormatProvider provider);
241242
public ulong ToUInt64(IFormatProvider provider);
243+
public bool TryAdd(string key, ValveKeyValue.KVObject value);
242244
public bool TryGetValue(string name, out ValveKeyValue.KVObject& child);
243245
}
244246

ValveKeyValue/ValveKeyValue/KVObject.cs

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ public KVObject this[string key]
192192
set
193193
{
194194
ArgumentNullException.ThrowIfNull(key);
195-
SetChild(key, value);
195+
TryInsert(key, value ?? Null(), InsertionBehavior.OverwriteExisting);
196196
}
197197
}
198198

@@ -296,22 +296,25 @@ public bool ContainsKey(string name)
296296

297297
/// <summary>
298298
/// Adds a named child to this collection.
299+
/// For dictionary-backed collections, throws if the key already exists.
300+
/// For list-backed collections, always appends (duplicate keys are allowed).
299301
/// </summary>
302+
/// <exception cref="ArgumentException">The key already exists in a dictionary-backed collection.</exception>
300303
public void Add(string key, KVObject value)
301304
{
302305
ArgumentNullException.ThrowIfNull(key);
306+
TryInsert(key, value, InsertionBehavior.ThrowOnExisting);
307+
}
303308

304-
switch (_ref)
305-
{
306-
case Dictionary<string, KVObject> dict:
307-
dict[key] = value;
308-
break;
309-
case List<KeyValuePair<string, KVObject>> list when ValueType == KVValueType.Collection:
310-
list.Add(new KeyValuePair<string, KVObject>(key, value));
311-
break;
312-
default:
313-
throw new InvalidOperationException($"Cannot add a named child to a {ValueType} value.");
314-
}
309+
/// <summary>
310+
/// Tries to add a named child to this collection.
311+
/// Returns <c>false</c> if the key already exists in a dictionary-backed collection.
312+
/// For list-backed collections, always succeeds (duplicate keys are allowed).
313+
/// </summary>
314+
public bool TryAdd(string key, KVObject value)
315+
{
316+
ArgumentNullException.ThrowIfNull(key);
317+
return TryInsert(key, value, InsertionBehavior.None);
315318
}
316319

317320
/// <summary>
@@ -500,48 +503,66 @@ public ReadOnlySpan<byte> AsSpan()
500503
internal List<KVObject> GetArrayList()
501504
=> (List<KVObject>)_ref;
502505

503-
internal Dictionary<string, KVObject> GetCollectionDict()
504-
=> (Dictionary<string, KVObject>)_ref;
505-
506-
internal List<KeyValuePair<string, KVObject>> GetCollectionList()
507-
=> (List<KeyValuePair<string, KVObject>>)_ref;
508-
509506
#endregion
510507

511508
#region Private helpers
512509

513-
private void SetChild(string key, KVObject value)
510+
private enum InsertionBehavior
514511
{
515-
value ??= Null();
512+
None,
513+
OverwriteExisting,
514+
ThrowOnExisting,
515+
}
516516

517+
private bool TryInsert(string key, KVObject value, InsertionBehavior behavior)
518+
{
517519
switch (_ref)
518520
{
519521
case Dictionary<string, KVObject> dict:
520-
dict[key] = value;
521-
break;
522-
case List<KeyValuePair<string, KVObject>> list when ValueType == KVValueType.Collection:
523-
var firstIndex = list.FindIndex(c => c.Key == key);
524-
if (firstIndex >= 0)
522+
if (behavior == InsertionBehavior.OverwriteExisting)
525523
{
526-
list[firstIndex] = new KeyValuePair<string, KVObject>(key, value);
524+
dict[key] = value;
525+
return true;
526+
}
527+
528+
if (dict.TryAdd(key, value))
529+
{
530+
return true;
531+
}
532+
533+
if (behavior == InsertionBehavior.ThrowOnExisting)
534+
{
535+
throw new ArgumentException($"An item with the same key has already been added. Key: {key}");
536+
}
527537

528-
// Remove any remaining duplicates after the replaced entry
529-
for (var i = list.Count - 1; i > firstIndex; i--)
538+
return false;
539+
540+
case List<KeyValuePair<string, KVObject>> list when ValueType == KVValueType.Collection:
541+
if (behavior == InsertionBehavior.OverwriteExisting)
542+
{
543+
var firstIndex = list.FindIndex(c => c.Key == key);
544+
if (firstIndex >= 0)
530545
{
531-
if (list[i].Key == key)
546+
list[firstIndex] = new KeyValuePair<string, KVObject>(key, value);
547+
548+
// Remove any remaining duplicates after the replaced entry
549+
for (var i = list.Count - 1; i > firstIndex; i--)
532550
{
533-
list.RemoveAt(i);
551+
if (list[i].Key == key)
552+
{
553+
list.RemoveAt(i);
554+
}
534555
}
556+
557+
return true;
535558
}
536559
}
537-
else
538-
{
539-
list.Add(new KeyValuePair<string, KVObject>(key, value));
540-
}
541560

542-
break;
561+
list.Add(new KeyValuePair<string, KVObject>(key, value));
562+
return true;
563+
543564
default:
544-
throw new InvalidOperationException($"Cannot set a child on a {ValueType} value.");
565+
throw new InvalidOperationException($"Cannot insert a named child into a {ValueType} value.");
545566
}
546567
}
547568

0 commit comments

Comments
 (0)