Skip to content

Commit d18984d

Browse files
committed
Merge branch 'feature/topic-querying-extensions' into develop
Added additional extension methods to the existing `OnTopic.Querying.TopicExtensions` class. These new methods make it easier to work with the _entire_ topic graph from an arbitrary position within that graph, as opposed to the previous methods which were focused primarily around working with descendant topics from that relative starting place. Notably, these make it easier to find references within the topic graph—such as relationships, derived topics, and associated content type descriptors. Previously, this required an otherwise-unnecessary dependency on an `ITopicRepository` implementation. These new extensions include `GetRootTopic()`, which gets the root of the current topic graph, and `GetTopicByUniqueKey()`, which looks up any topic within the graph based on its unique key. This will allow us to reduce code in other libraries, such as the new **OnTopic Date Exchange**. Within the **OnTopic Library**, it has already allowed us to reduce the code within `CachedTopicRepository`.
2 parents 2806a5f + 867b110 commit d18984d

4 files changed

Lines changed: 184 additions & 102 deletions

File tree

OnTopic.Data.Caching/CachedTopicRepository.cs

Lines changed: 1 addition & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() {
102102
| Lookup by TopicKey
103103
\-----------------------------------------------------------------------------------------------------------------------*/
104104
if (topicKey != null && !topicKey.Length.Equals(0)) {
105-
return GetTopic(_cache, topicKey);
105+
return _cache.GetByUniqueKey(topicKey);
106106
}
107107

108108
/*------------------------------------------------------------------------------------------------------------------------
@@ -131,76 +131,6 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() {
131131

132132
}
133133

134-
/*==========================================================================================================================
135-
| METHOD: GET TOPIC
136-
\-------------------------------------------------------------------------------------------------------------------------*/
137-
/// <summary>
138-
/// Retrieves a topic object based on the specified partial or full (prefixed) topic key.
139-
/// </summary>
140-
/// <param name="sourceTopic">The root topic to search from.</param>
141-
/// <param name="uniqueKey">
142-
/// The partial or full string value representing the key (or <see cref="Topic.GetUniqueKey"/>) for the topic.
143-
/// </param>
144-
/// <returns>The topic or null, if the topic is not found.</returns>
145-
private Topic? GetTopic(Topic? sourceTopic, string uniqueKey) {
146-
147-
/*------------------------------------------------------------------------------------------------------------------------
148-
| Validate input
149-
\-----------------------------------------------------------------------------------------------------------------------*/
150-
if (sourceTopic == null) return null;
151-
if (String.IsNullOrWhiteSpace(uniqueKey)) return null;
152-
153-
/*------------------------------------------------------------------------------------------------------------------------
154-
| Provide shortcut for local calls
155-
\-----------------------------------------------------------------------------------------------------------------------*/
156-
if (uniqueKey.IndexOf(":", StringComparison.InvariantCulture) < 0 && uniqueKey != "Root") {
157-
if (sourceTopic.Children.Contains(uniqueKey)) {
158-
return sourceTopic.Children[uniqueKey];
159-
}
160-
return null;
161-
}
162-
163-
/*------------------------------------------------------------------------------------------------------------------------
164-
| Provide implicit root
165-
>-------------------------------------------------------------------------------------------------------------------------
166-
| ###NOTE JJC080313: While a root topic is required by the data structure, it should be implicit from the perspective of
167-
| the calling application. A developer should be able to call GetTopic("Namespace:TopicPath") to get to a topic, without
168-
| needing to be aware of the root.
169-
\-----------------------------------------------------------------------------------------------------------------------*/
170-
if (
171-
!uniqueKey.StartsWith("Root:", StringComparison.OrdinalIgnoreCase) &&
172-
!uniqueKey.Equals("Root", StringComparison.OrdinalIgnoreCase)
173-
) {
174-
uniqueKey = "Root:" + uniqueKey;
175-
}
176-
177-
/*------------------------------------------------------------------------------------------------------------------------
178-
| Validate parameters
179-
\-----------------------------------------------------------------------------------------------------------------------*/
180-
if (!uniqueKey.StartsWith(sourceTopic.GetUniqueKey(), StringComparison.OrdinalIgnoreCase)) return null;
181-
if (uniqueKey.Equals(sourceTopic.GetUniqueKey(), StringComparison.OrdinalIgnoreCase)) return sourceTopic;
182-
183-
/*------------------------------------------------------------------------------------------------------------------------
184-
| Define variables
185-
\-----------------------------------------------------------------------------------------------------------------------*/
186-
var remainder = uniqueKey.Substring(sourceTopic.GetUniqueKey().Length + 1);
187-
var marker = remainder.IndexOf(":", StringComparison.Ordinal);
188-
var nextChild = (marker < 0) ? remainder : remainder.Substring(0, marker);
189-
190-
/*------------------------------------------------------------------------------------------------------------------------
191-
| Find topic
192-
\-----------------------------------------------------------------------------------------------------------------------*/
193-
if (!sourceTopic.Children.Contains(nextChild)) return null;
194-
195-
if (nextChild == remainder) return sourceTopic.Children[nextChild];
196-
197-
/*------------------------------------------------------------------------------------------------------------------------
198-
| Return the topic
199-
\-----------------------------------------------------------------------------------------------------------------------*/
200-
return GetTopic(sourceTopic.Children[nextChild], uniqueKey);
201-
202-
}
203-
204134
/*==========================================================================================================================
205135
| METHOD: SAVE
206136
\-------------------------------------------------------------------------------------------------------------------------*/

OnTopic.Tests/TopicQueryingTest.cs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*==============================================================================================================================
2+
| Author Ignia, LLC
3+
| Client Ignia, LLC
4+
| Project Topics Library
5+
\=============================================================================================================================*/
6+
using System;
7+
using System.Linq;
8+
using Microsoft.VisualStudio.TestTools.UnitTesting;
9+
using OnTopic.Querying;
10+
11+
namespace OnTopic.Tests {
12+
13+
/*============================================================================================================================
14+
| CLASS: TOPIC QUERYING TEST
15+
\---------------------------------------------------------------------------------------------------------------------------*/
16+
/// <summary>
17+
/// Provides unit tests for the <see cref="TopicExtensions"/> class.
18+
/// </summary>
19+
[TestClass]
20+
public class TopicQueryingTest {
21+
22+
/*==========================================================================================================================
23+
| TEST: FIND ALL BY ATTRIBUTE: RETURNS CORRECT TOPICS
24+
\-------------------------------------------------------------------------------------------------------------------------*/
25+
/// <summary>
26+
/// Looks for a deeply nested child topic using only the attribute value.
27+
/// </summary>
28+
[TestMethod]
29+
public void FindAllByAttribute_ReturnsCorrectTopics() {
30+
31+
var parentTopic = TopicFactory.Create("ParentTopic", "Page", 1);
32+
var childTopic = TopicFactory.Create("ChildTopic", "Page", 5, parentTopic);
33+
var grandChildTopic = TopicFactory.Create("GrandChildTopic", "Page", 20, childTopic);
34+
var grandNieceTopic = TopicFactory.Create("GrandNieceTopic", "Page", 3, childTopic);
35+
var greatGrandChildTopic = TopicFactory.Create("GreatGrandChildTopic", "Page", 7, grandChildTopic);
36+
37+
grandChildTopic.Attributes.SetValue("Foo", "Baz");
38+
greatGrandChildTopic.Attributes.SetValue("Foo", "Bar");
39+
grandNieceTopic.Attributes.SetValue("Foo", "Bar");
40+
41+
Assert.ReferenceEquals(parentTopic.FindAllByAttribute("Foo", "Bar").First(), grandNieceTopic);
42+
Assert.AreEqual<int>(2, parentTopic.FindAllByAttribute("Foo", "Bar").Count);
43+
Assert.ReferenceEquals(parentTopic.FindAllByAttribute("Foo", "Baz").First(), grandChildTopic);
44+
45+
}
46+
47+
/*==========================================================================================================================
48+
| TEST: GET ROOT TOPIC: RETURNS ROOT TOPIC
49+
\-------------------------------------------------------------------------------------------------------------------------*/
50+
/// <summary>
51+
/// Given a deeply nested <see cref="Topic"/>, returns the root <see cref="Topic"/>.
52+
/// </summary>
53+
[TestMethod]
54+
public void GetRootTopic_ReturnsRootTopic() {
55+
56+
var parentTopic = TopicFactory.Create("ParentTopic", "Page", 1);
57+
var childTopic = TopicFactory.Create("ChildTopic", "Page", 5, parentTopic);
58+
var grandChildTopic = TopicFactory.Create("GrandChildTopic", "Page", 20, childTopic);
59+
var greatGrandChildTopic = TopicFactory.Create("GreatGrandChildTopic", "Page", 7, grandChildTopic);
60+
61+
var rootTopic = greatGrandChildTopic.GetRootTopic();
62+
63+
Assert.ReferenceEquals(parentTopic, rootTopic);
64+
65+
}
66+
67+
/*==========================================================================================================================
68+
| TEST: GET BY UNIQUE KEY: VALID KEY: RETURNS TOPIC
69+
\-------------------------------------------------------------------------------------------------------------------------*/
70+
/// <summary>
71+
/// Given a deeply nested <see cref="Topic"/>, returns the expected <see cref="Topic"/>.
72+
/// </summary>
73+
[TestMethod]
74+
public void GetByUniqueKey_ValidKey_ReturnsTopic() {
75+
76+
var parentTopic = TopicFactory.Create("ParentTopic", "Page", 1);
77+
var childTopic = TopicFactory.Create("ChildTopic", "Page", 5, parentTopic);
78+
var grandChildTopic = TopicFactory.Create("GrandChildTopic", "Page", 20, childTopic);
79+
var greatGrandChildTopic1 = TopicFactory.Create("GreatGrandChildTopic1", "Page", 7, grandChildTopic);
80+
var greatGrandChildTopic2 = TopicFactory.Create("GreatGrandChildTopic2", "Page", 7, grandChildTopic);
81+
82+
var foundTopic = greatGrandChildTopic1.GetByUniqueKey("ParentTopic:ChildTopic:GrandChildTopic:GreatGrandChildTopic2");
83+
84+
Assert.ReferenceEquals(greatGrandChildTopic2, foundTopic);
85+
86+
}
87+
88+
/*==========================================================================================================================
89+
| TEST: GET BY UNIQUE KEY: INVALID KEY: RETURNS NULL
90+
\-------------------------------------------------------------------------------------------------------------------------*/
91+
/// <summary>
92+
/// Given an invalid <c>UniqueKey</c>, the <see cref="TopicExtensions.GetByUniqueKey(Topic, String)"/> returns
93+
/// <c>null</c>.
94+
/// </summary>
95+
[TestMethod]
96+
public void GetByUniqueKey_InvalidKey_ReturnsNull() {
97+
98+
var parentTopic = TopicFactory.Create("ParentTopic", "Page", 1);
99+
var childTopic = TopicFactory.Create("ChildTopic", "Page", 5, parentTopic);
100+
var grandChildTopic = TopicFactory.Create("GrandChildTopic", "Page", 20, childTopic);
101+
var greatGrandChildTopic = TopicFactory.Create("GreatGrandChildTopic", "Page", 7, grandChildTopic);
102+
103+
var foundTopic = greatGrandChildTopic.GetByUniqueKey("ParentTopic:ChildTopic:GrandChildTopic:GreatGrandChildTopic2");
104+
105+
Assert.IsNull(foundTopic);
106+
107+
}
108+
109+
} //Class
110+
} //Namespace

OnTopic.Tests/TopicTest.cs

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using System.Globalization;
88
using System.Linq;
99
using OnTopic.Metadata;
10-
using OnTopic.Querying;
1110
using Microsoft.VisualStudio.TestTools.UnitTesting;
1211
using OnTopic.Collections;
1312

@@ -179,36 +178,6 @@ public void UniqueKey_ReturnsUniqueKey() {
179178

180179
}
181180

182-
/*==========================================================================================================================
183-
| TEST: FIND ALL BY ATTRIBUTE: RETURNS CORRECT TOPICS
184-
\-------------------------------------------------------------------------------------------------------------------------*/
185-
/// <summary>
186-
/// Looks for a deeply nested child topic using only the attribute value.
187-
/// </summary>
188-
[TestMethod]
189-
public void FindAllByAttribute_ReturnsCorrectTopics() {
190-
191-
var parentTopic = TopicFactory.Create("ParentTopic", "Page", 1);
192-
var childTopic = TopicFactory.Create("ChildTopic", "Page", 5);
193-
var grandChildTopic = TopicFactory.Create("GrandChildTopic", "Page", 20);
194-
var grandNieceTopic = TopicFactory.Create("GrandNieceTopic", "Page", 3);
195-
var greatGrandChildTopic = TopicFactory.Create("GreatGrandChildTopic", "Page", 7);
196-
197-
childTopic.Parent = parentTopic;
198-
grandChildTopic.Parent = childTopic;
199-
grandNieceTopic.Parent = childTopic;
200-
greatGrandChildTopic.Parent = grandChildTopic;
201-
202-
grandChildTopic.Attributes.SetValue("Foo", "Baz");
203-
greatGrandChildTopic.Attributes.SetValue("Foo", "Bar");
204-
grandNieceTopic.Attributes.SetValue("Foo", "Bar");
205-
206-
Assert.ReferenceEquals(parentTopic.FindAllByAttribute("Foo", "Bar").First(), grandNieceTopic);
207-
Assert.AreEqual<int>(2, parentTopic.FindAllByAttribute("Foo", "Bar").Count);
208-
Assert.ReferenceEquals(parentTopic.FindAllByAttribute("Foo", "Baz").First(), grandChildTopic);
209-
210-
}
211-
212181
/*==========================================================================================================================
213182
| TEST: IS VISIBLE: RETURNS EXPECTED VALUE
214183
\-------------------------------------------------------------------------------------------------------------------------*/

OnTopic/Querying/TopicExtensions.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,5 +142,78 @@ public static ReadOnlyTopicCollection<Topic> FindAllByAttribute(this Topic topic
142142

143143
}
144144

145+
/*==========================================================================================================================
146+
| METHOD: GET ROOT TOPIC
147+
\-------------------------------------------------------------------------------------------------------------------------*/
148+
/// <summary>
149+
/// Retrieves the root <see cref="Topic"/> in the current topic graph.
150+
/// </summary>
151+
/// <param name="topic">The instance of the <see cref="Topic"/> to operate against; populated automatically by .NET.</param>
152+
/// <returns>The <see cref="Topic"/> at the root o the current topic graph.</returns>
153+
public static Topic GetRootTopic(this Topic topic) {
154+
155+
/*------------------------------------------------------------------------------------------------------------------------
156+
| Validate contracts
157+
\-----------------------------------------------------------------------------------------------------------------------*/
158+
Contract.Requires(topic, "The topic parameter must be specified.");
159+
160+
/*------------------------------------------------------------------------------------------------------------------------
161+
| Find lowest common root
162+
\-----------------------------------------------------------------------------------------------------------------------*/
163+
while (topic.Parent != null) {
164+
topic = topic.Parent;
165+
}
166+
167+
/*------------------------------------------------------------------------------------------------------------------------
168+
| Return root
169+
\-----------------------------------------------------------------------------------------------------------------------*/
170+
return topic;
171+
172+
}
173+
174+
/*==========================================================================================================================
175+
| METHOD: GET BY UNIQUE KEY
176+
\-------------------------------------------------------------------------------------------------------------------------*/
177+
/// <summary>
178+
/// Retrieves a <see cref="Topic"/> with the specified <paramref name="uniqueKey"/>, if available.
179+
/// </summary>
180+
/// <param name="topic">The instance of the <see cref="Topic"/> to operate against; populated automatically by .NET.</param>
181+
/// <param name="uniqueKey">The <see cref="Topic.GetUniqueKey()"/> of the <see cref="Topic"/> to return.</param>
182+
/// <returns>A <see cref="Topic"/> with the specified <paramref name="uniqueKey"/>, if found.</returns>
183+
public static Topic? GetByUniqueKey(this Topic topic, string uniqueKey) {
184+
185+
/*------------------------------------------------------------------------------------------------------------------------
186+
| Validate contracts
187+
\-----------------------------------------------------------------------------------------------------------------------*/
188+
Contract.Requires(topic, "The topic parameter must be specified.");
189+
Contract.Requires<ArgumentNullException>(!String.IsNullOrWhiteSpace(uniqueKey), "The unique key must be specified.");
190+
191+
/*------------------------------------------------------------------------------------------------------------------------
192+
| Find lowest common root
193+
\-----------------------------------------------------------------------------------------------------------------------*/
194+
var currentTopic = (Topic?)topic.GetRootTopic();
195+
196+
/*------------------------------------------------------------------------------------------------------------------------
197+
| Process keys
198+
\-----------------------------------------------------------------------------------------------------------------------*/
199+
if (uniqueKey.StartsWith(currentTopic!.Key + ":", StringComparison.InvariantCultureIgnoreCase)) {
200+
uniqueKey = uniqueKey.Substring(currentTopic!.Key.Length + 1);
201+
}
202+
var keys = uniqueKey.Split(new char[] {':'}, StringSplitOptions.RemoveEmptyEntries);
203+
204+
/*------------------------------------------------------------------------------------------------------------------------
205+
| Navigate to the specific path
206+
\-----------------------------------------------------------------------------------------------------------------------*/
207+
foreach (var key in keys) {
208+
currentTopic = currentTopic?.Children?.GetTopic(key);
209+
}
210+
211+
/*------------------------------------------------------------------------------------------------------------------------
212+
| Return topic
213+
\-----------------------------------------------------------------------------------------------------------------------*/
214+
return currentTopic;
215+
216+
}
217+
145218
} //Class
146219
} //Namespace

0 commit comments

Comments
 (0)