public EndPointGroup(List<EndPointDescriptor> endPoints) { Trie = new Trie(); EndPoints = endPoints; foreach (var endPoint in EndPoints) { var oneEndPointTrie = new Trie(); oneEndPointTrie.Insert(endPoint.Path, endPoint); ValidatePathParameters(oneEndPointTrie, endPoint); Trie.Merge(oneEndPointTrie, endPoint.Path); } }
/// <summary> /// Validates that Path Parameters match the replaceable tokens in the /// BaseRouteAttribute on the method. /// </summary> /// <param name="trie"></param> /// <param name="endPoint"></param> void ValidatePathParameters(Trie trie, EndPointDescriptor endPoint) { // If we don't have any parameters, we're done. if (endPoint.Parameters == null || !endPoint.Parameters.Any()) { return; } // If none of the parameters are Path Parameters, then we're done. var pathParameters = endPoint.Parameters.Where(p => p is PathAttribute); if (!pathParameters.Any()) { return; } var pathTokens = trie.GetReplaceableTokens(endPoint.Path); // Now, check that there are no replaceable tokens from the BaseRouteAttribute // that lack counterparts in the parameter list of the method. foreach (var pathParameter in pathParameters) { var matchingToken = pathTokens.SingleOrDefault(t => t.Equals(pathParameter.ParameterInfo.Name)); if (matchingToken == null) { throw new Exception(String.Format("FATAL ERROR - Method {0} has a path " + "parameter {1} whose name is not found in the replaceable tokens in " + "its BaseRoutePath {2}", endPoint.Name, pathParameter.ParameterInfo.Name, endPoint.Path)); } } // Just compare the counts now. var pathParametersCount = pathParameters.Count(); var pathTokensCount = pathTokens.Count(); if (pathParametersCount != pathTokensCount) { var parmNames = String.Join(",", pathParameters.Select(pp => pp.ParameterInfo.Name)); var tokenNames = String.Join(",", pathTokens); throw new Exception(String.Format("FATAL ERROR - Method {0} has {1} " + "parameterized tokens in BaseRouteAttribute {2}, tokens = \"{3}\", and it " + "has {4} method signature [Path] parameters \"{5}\".", endPoint.Name, pathTokensCount, endPoint.Path, tokenNames, pathParametersCount, parmNames)); } }
/// <summary> /// Given an intermediate node, gets the list of terminal regexes for the various /// sub-paths that sprout from it. /// </summary> /// <returns>A list of regexes, paired with the trie from *this* level that is the ///root to the matching sub-path. Basically, each one just uses the passed-in trie /// as a value in the keyvaluepair, but this is called for each child, and aggregated; /// there's a different trie for each 'batch' of these.</returns> /// <param name="trie">Trie.</param> IEnumerable<KeyValuePair<Regex, Trie>> GetPathRegexes(Trie trie) { var regexes = new List<KeyValuePair<Regex, Trie>>(); if (IsPathRegex) { regexes.Add(new KeyValuePair<Regex, Trie>(PathRegex, trie)); } if (Children == null) { return regexes; } foreach (var child in Children) { regexes.AddRange(child.GetPathRegexes(trie)); // pass in parent Trie. } return regexes; }
/// <summary> /// Clone the trie node, but not its children. /// </summary> /// <param name="source">source</param> /// <param name="trieToMergePath">Path for node.</param> /// <param name="destination">Destination.</param> static void ShallowClone(Trie destination, Trie source, string trieToMergePath) { destination.Path = trieToMergePath; destination.Terminal = source.Terminal; destination.IsGreedyStar = source.IsGreedyStar; // Don't allow new shorter terminal paths to override the replaceable token nature // of longer paths that share a subpath. if (source.IsNodeReplaceableToken) { destination.IsNodeReplaceableToken = source.IsNodeReplaceableToken; } destination.PathRegex = source.PathRegex; destination.EndPoint = source.EndPoint; }
/// <summary> /// Merges a single path trie to an already-existing one. /// </summary> /// <param name="trieToMerge">Single path trie to merge.</param> /// <param name="trieToMergePath">Path for merging trie.</param> public void Merge(Trie trieToMerge, string trieToMergePath) { var thisTrie = this; var segs = trieToMergePath.Trim('/').Split('/'); var i = 0; while (i < segs.Length) { trieToMerge = trieToMerge.Next(segs, i); if (trieToMerge == null) { break; } var next = thisTrie.Next(segs, i, true); i++; // If Next returns null, or the segment it returns does not equal this // one, and it's not a replaceable token, then create a new one here. if (next == null || (!next.Segment.Equals(trieToMerge.Segment) && !next.IsNodeReplaceableToken)) { if (thisTrie.Children == null) { thisTrie.Children = new List<Trie>(); } thisTrie.Children.Add(trieToMerge); return; } thisTrie = next; } ShallowClone(thisTrie, trieToMerge, trieToMergePath); }
/// <summary> /// Loads up a trie with a complete path. /// </summary> /// <param name="path">Path to break into nodes and store.</param> /// <param name="endPoint">EndPointDescriptor to store at the terminus.</param> public void Insert(string path, IEndPointDescriptor endPoint) { var createRegex = false; var t = this; var inputSegments = path.Split('/'); var regexPathSegments = new List<string>(); var i = 0; foreach (var segment in inputSegments) { var greedyStar = false; var regexString = CheckIfReplaceableToken(segment, out greedyStar); regexPathSegments.Add(regexString ?? segment); var segmentIsRegex = !string.IsNullOrEmpty(regexString); createRegex = createRegex | segmentIsRegex; var next = t.Next(inputSegments, i++); if (next == null) { next = new Trie(segment, segmentIsRegex, greedyStar); if (t.Children == null) { t.Children = new List<Trie>(); } t.Children.Add(next); } t = next; if (greedyStar) { break; } } t.Terminal = true; t.Path = path; t.EndPoint = endPoint; if (!createRegex) { return; } t.PathRegex = new Regex("^/?" + string.Join("/", regexPathSegments) + "$", RegexOptions.Compiled); }