Files
yes.blog/Yes.Infrastructure/ViewEngine/UriTemplateCompoundPathSegment.cs
2025-04-30 09:51:36 +08:00

484 lines
21 KiB
C#

namespace Yes.Infrastructure.ViewEngine
{
// Thin wrapper around formatted string; use type system to help ensure we
// are doing canonicalization right/consistently - the literal sections are held in an
// un-escaped format
// We are assuming that the string will be always built as Lit{Var}Lit[{Var}Lit[{Var}Lit[...]]],
// when the first and last literals may be empty
internal class UriTemplateCompoundPathSegment : UriTemplatePathSegment, IComparable<UriTemplateCompoundPathSegment>
{
private readonly string _firstLiteral;
private readonly List<VarAndLitPair> _varLitPairs;
private CompoundSegmentClass _csClass;
private UriTemplateCompoundPathSegment(string originalSegment, bool endsWithSlash, string firstLiteral)
: base(originalSegment, UriTemplatePartType.Compound, endsWithSlash)
{
_firstLiteral = firstLiteral;
_varLitPairs = new List<VarAndLitPair>();
}
public static new UriTemplateCompoundPathSegment CreateFromUriTemplate(string segment, UriTemplate template)
{
var origSegment = segment;
var endsWithSlash = segment.EndsWith("/", StringComparison.Ordinal);
if (endsWithSlash)
{
segment = segment.Remove(segment.Length - 1);
}
var nextVarStart = segment.IndexOf("{", StringComparison.Ordinal);
Ensure.Nonnegative(nextVarStart,
"The method is only called after identifying a '{' character in the segment");
var firstLiteral = nextVarStart > 0 ? segment.Substring(0, nextVarStart) : string.Empty;
if (firstLiteral.IndexOf(UriTemplate.WildcardPath, StringComparison.Ordinal) != -1)
{
throw new FormatException($"UTInvalidWildcardInVariableOrLiteral {template.OriginalTemplate} {UriTemplate.WildcardPath}");
}
var result = new UriTemplateCompoundPathSegment(origSegment, endsWithSlash,
firstLiteral != string.Empty ? Uri.UnescapeDataString(firstLiteral) : string.Empty);
do
{
var nextVarEnd = segment.IndexOf("}", nextVarStart + 1, StringComparison.Ordinal);
if (nextVarEnd < nextVarStart + 2)
{
throw new FormatException($"UTInvalidFormatSegmentOrQueryPart {segment}");
}
bool hasDefault;
var varName = template.AddPathVariable(UriTemplatePartType.Compound,
segment.Substring(nextVarStart + 1, nextVarEnd - nextVarStart - 1), out hasDefault);
if (hasDefault)
{
throw new InvalidOperationException(
$"UTDefaultValueToCompoundSegmentVar template:{template},origSegment:{origSegment},varName:{varName}");
}
nextVarStart = segment.IndexOf("{", nextVarEnd + 1, StringComparison.Ordinal);
string literal;
if (nextVarStart > 0)
{
if (nextVarStart == nextVarEnd + 1)
{
throw new ArgumentException($"UTDoesNotSupportAdjacentVarsInCompoundSegment template:{template},segment:{segment}");
}
literal = segment.Substring(nextVarEnd + 1, nextVarStart - nextVarEnd - 1);
}
else if (nextVarEnd + 1 < segment.Length)
{
literal = segment.Substring(nextVarEnd + 1);
}
else
{
literal = string.Empty;
}
if (literal.IndexOf(UriTemplate.WildcardPath, StringComparison.Ordinal) != -1)
{
throw new FormatException($"UTInvalidWildcardInVariableOrLiteral {template.OriginalTemplate} {UriTemplate.WildcardPath}");
}
if (literal.IndexOf('}') != -1)
{
throw new FormatException($"UTInvalidFormatSegmentOrQueryPart {segment}");
}
result._varLitPairs.Add(new VarAndLitPair(varName, literal == string.Empty ? string.Empty : Uri.UnescapeDataString(literal)));
} while (nextVarStart > 0);
if (string.IsNullOrEmpty(result._firstLiteral))
{
if (string.IsNullOrEmpty(result._varLitPairs[result._varLitPairs.Count - 1].Literal))
{
result._csClass = CompoundSegmentClass.HasNoPrefixNorSuffix;
}
else
{
result._csClass = CompoundSegmentClass.HasOnlySuffix;
}
}
else
{
if (string.IsNullOrEmpty(result._varLitPairs[result._varLitPairs.Count - 1].Literal))
{
result._csClass = CompoundSegmentClass.HasOnlyPrefix;
}
else
{
result._csClass = CompoundSegmentClass.HasPrefixAndSuffix;
}
}
return result;
}
public override void Bind(string[] values, ref int valueIndex, StringBuilder path)
{
if (valueIndex + _varLitPairs.Count <= values.Length == false)
throw new Exception("Not enough values to bind");
path.Append(_firstLiteral);
for (var pairIndex = 0; pairIndex < _varLitPairs.Count; pairIndex++)
{
path.Append(values[valueIndex++]);
path.Append(_varLitPairs[pairIndex].Literal);
}
if (EndsWithSlash)
{
path.Append("/");
}
}
public override bool IsEquivalentTo(UriTemplatePathSegment other, bool ignoreTrailingSlash)
{
Ensure.NotNull(other, "why would we ever call this?");
//if (other == null) {
// Fx.Assert("why would we ever call this?");
// return false;
//}
if (!ignoreTrailingSlash && EndsWithSlash != other.EndsWithSlash)
{
return false;
}
var otherAsCompound = other as UriTemplateCompoundPathSegment;
if (otherAsCompound == null)
{
// if other can't be cast as a compound then it can't be equivalent
return false;
}
if (_varLitPairs.Count != otherAsCompound._varLitPairs.Count)
{
return false;
}
if (StringComparer.OrdinalIgnoreCase.Compare(_firstLiteral, otherAsCompound._firstLiteral) != 0)
{
return false;
}
for (var pairIndex = 0; pairIndex < _varLitPairs.Count; pairIndex++)
{
if (StringComparer.OrdinalIgnoreCase.Compare(_varLitPairs[pairIndex].Literal,
otherAsCompound._varLitPairs[pairIndex].Literal) != 0)
{
return false;
}
}
return true;
}
public override bool IsMatch(UriTemplateLiteralPathSegment segment, bool ignoreTrailingSlash)
{
if (!ignoreTrailingSlash && EndsWithSlash != segment.EndsWithSlash)
{
return false;
}
return TryLookup(segment.AsUnescapedString(), null);
}
public override void Lookup(string segment, NameValueCollection boundParameters)
{
if (!TryLookup(segment, boundParameters))
{
//Fx.Assert("How can that be? Lookup is expected to be called after IsMatch");
throw new InvalidOperationException("UTCSRLookupBeforeMatch How can that be? Lookup is expected to be called after IsMatch");
}
}
private bool TryLookup(string segment, NameValueCollection boundParameters)
{
var segmentPosition = 0;
if (!string.IsNullOrEmpty(_firstLiteral))
{
if (segment.StartsWith(_firstLiteral, StringComparison.Ordinal))
{
segmentPosition = _firstLiteral.Length;
}
else
{
return false;
}
}
for (var pairIndex = 0; pairIndex < _varLitPairs.Count - 1; pairIndex++)
{
var nextLiteralPosition = segment.IndexOf(_varLitPairs[pairIndex].Literal, segmentPosition, StringComparison.Ordinal);
if (nextLiteralPosition < segmentPosition + 1)
{
return false;
}
if (boundParameters != null)
{
var varValue = segment.Substring(segmentPosition, nextLiteralPosition - segmentPosition);
boundParameters.Add(_varLitPairs[pairIndex].VarName, varValue);
}
segmentPosition = nextLiteralPosition + _varLitPairs[pairIndex].Literal.Length;
}
if (segmentPosition < segment.Length)
{
if (string.IsNullOrEmpty(_varLitPairs[_varLitPairs.Count - 1].Literal))
{
if (boundParameters != null)
{
boundParameters.Add(_varLitPairs[_varLitPairs.Count - 1].VarName,
segment.Substring(segmentPosition));
}
return true;
}
else if (segmentPosition + _varLitPairs[_varLitPairs.Count - 1].Literal.Length < segment.Length &&
segment.EndsWith(_varLitPairs[_varLitPairs.Count - 1].Literal, StringComparison.Ordinal))
{
if (boundParameters != null)
{
boundParameters.Add(_varLitPairs[_varLitPairs.Count - 1].VarName,
segment.Substring(segmentPosition, segment.Length - segmentPosition - _varLitPairs[_varLitPairs.Count - 1].Literal.Length));
}
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}
// A note about comparing compound segments:
// We are using this for generating the sorted collections at the nodes of the UriTemplateTrieNode.
// The idea is that we are sorting the segments based on preferred matching, when we have two
// compound segments matching the same wire segment, we will give preference to the preceding one.
// The order is based on the following concepts:
// - We are defining four classes of compound segments: prefix+suffix, prefix-only, suffix-only
// and none
// - Whenever we are comparing segments from different class the preferred one is the segment with
// the prefared class, based on the order we defined them (p+s \ p \ s \ n).
// - Within each class the preference is based on the prefix\suffix, while prefix has precedence
// over suffix if both exists.
// - If after comparing the class, as well as the prefix\suffix, we didn't reach to a conclusion,
// the preference is given to the segment with more variables parts.
// This order mostly follows the intuitive common sense; the major issue comes from preferring the
// prefix over the suffix in the case where both exist. This is derived from the problematic of any
// other type of solution that don't prefere the prefix over the suffix or vice versa. To better
// understanding lets considered the following example:
// In comparing 'foo{x}bar' and 'food{x}ar', unless we are preferring prefix or suffix, we have
// to state that they have the same order. So is the case with 'foo{x}babar' and 'food{x}ar', which
// will lead us to claiming the 'foo{x}bar' and 'foo{x}babar' are from the same order, which they
// clearly are not.
// Taking other approaches to this problem results in similar cases. The only solution is preferring
// either the prefix or the suffix over the other; since we already preferred prefix over suffix
// implicitly (we preferred the prefix only class over the suffix only, we also prefared literal
// over variable, if in the same path segment) that still maintain consistency.
// Therefore:
// - 'food{var}' should be before 'foo{var}'; '{x}.{y}.{z}' should be before '{x}.{y}'.
// - the order between '{var}bar' and '{var}qux' is not important
// - '{x}.{y}' and '{x}_{y}' should have the same order
// - 'foo{x}bar' is less preferred than 'food{x}ar'
// In the above third case - if we are opening the table with allowDuplicate=false, we will throw;
// if we are opening it with allowDuplicate=true we will let it go and might match both templates
// for certain wire candidates.
int IComparable<UriTemplateCompoundPathSegment>.CompareTo(UriTemplateCompoundPathSegment other)
{
Ensure.NotNull(other, "We are only expected to get here for comparing real compound segments");
switch (_csClass)
{
case CompoundSegmentClass.HasPrefixAndSuffix:
switch (other._csClass)
{
case CompoundSegmentClass.HasPrefixAndSuffix:
return CompareToOtherThatHasPrefixAndSuffix(other);
case CompoundSegmentClass.HasOnlyPrefix:
case CompoundSegmentClass.HasOnlySuffix:
case CompoundSegmentClass.HasNoPrefixNorSuffix:
return -1;
default:
throw new Exception("Invalid other.CompoundSegmentClass");
}
case CompoundSegmentClass.HasOnlyPrefix:
switch (other._csClass)
{
case CompoundSegmentClass.HasPrefixAndSuffix:
return 1;
case CompoundSegmentClass.HasOnlyPrefix:
return CompareToOtherThatHasOnlyPrefix(other);
case CompoundSegmentClass.HasOnlySuffix:
case CompoundSegmentClass.HasNoPrefixNorSuffix:
return -1;
default:
throw new Exception("Invalid other.CompoundSegmentClass");
}
case CompoundSegmentClass.HasOnlySuffix:
switch (other._csClass)
{
case CompoundSegmentClass.HasPrefixAndSuffix:
case CompoundSegmentClass.HasOnlyPrefix:
return 1;
case CompoundSegmentClass.HasOnlySuffix:
return CompareToOtherThatHasOnlySuffix(other);
case CompoundSegmentClass.HasNoPrefixNorSuffix:
return -1;
default:
throw new Exception("Invalid other.CompoundSegmentClass");
}
case CompoundSegmentClass.HasNoPrefixNorSuffix:
switch (other._csClass)
{
case CompoundSegmentClass.HasPrefixAndSuffix:
case CompoundSegmentClass.HasOnlyPrefix:
case CompoundSegmentClass.HasOnlySuffix:
return 1;
case CompoundSegmentClass.HasNoPrefixNorSuffix:
return CompareToOtherThatHasNoPrefixNorSuffix(other);
default:
throw new Exception("Invalid other.CompoundSegmentClass");
}
default:
throw new Exception("Invalid this.CompoundSegmentClass");
}
}
private int CompareToOtherThatHasPrefixAndSuffix(UriTemplateCompoundPathSegment other)
{
if (_csClass == CompoundSegmentClass.HasPrefixAndSuffix == false)
throw new Exception("Otherwise, how did we got here?");
if (other._csClass == CompoundSegmentClass.HasPrefixAndSuffix == false)
throw new Exception("Otherwise, how did we got here?");
// In this case we are determining the order based on the prefix of the two segments,
// then by their suffix and then based on the number of variables
var prefixOrder = ComparePrefixToOtherPrefix(other);
if (prefixOrder == 0)
{
var suffixOrder = CompareSuffixToOtherSuffix(other);
if (suffixOrder == 0)
{
return other._varLitPairs.Count - _varLitPairs.Count;
}
else
{
return suffixOrder;
}
}
else
{
return prefixOrder;
}
}
private int CompareToOtherThatHasOnlyPrefix(UriTemplateCompoundPathSegment other)
{
if (_csClass == CompoundSegmentClass.HasOnlyPrefix == false)
throw new Exception("Otherwise, how did we got here?");
if (other._csClass == CompoundSegmentClass.HasOnlyPrefix == false)
throw new Exception("Otherwise, how did we got here?");
// In this case we are determining the order based on the prefix of the two segments,
// then based on the number of variables
var prefixOrder = ComparePrefixToOtherPrefix(other);
if (prefixOrder == 0)
{
return other._varLitPairs.Count - _varLitPairs.Count;
}
else
{
return prefixOrder;
}
}
private int CompareToOtherThatHasOnlySuffix(UriTemplateCompoundPathSegment other)
{
if (_csClass == CompoundSegmentClass.HasOnlySuffix == false)
throw new Exception("Otherwise, how did we got here?");
if (other._csClass == CompoundSegmentClass.HasOnlySuffix == false)
throw new Exception("Otherwise, how did we got here?");
// In this case we are determining the order based on the suffix of the two segments,
// then based on the number of variables
var suffixOrder = CompareSuffixToOtherSuffix(other);
if (suffixOrder == 0)
{
return other._varLitPairs.Count - _varLitPairs.Count;
}
else
{
return suffixOrder;
}
}
private int CompareToOtherThatHasNoPrefixNorSuffix(UriTemplateCompoundPathSegment other)
{
if (_csClass == CompoundSegmentClass.HasNoPrefixNorSuffix == false)
throw new Exception("Otherwise, how did we got here?");
if (other._csClass == CompoundSegmentClass.HasNoPrefixNorSuffix == false)
throw new Exception("Otherwise, how did we got here?");
// In this case the order is determined by the number of variables
return other._varLitPairs.Count - _varLitPairs.Count;
}
private int ComparePrefixToOtherPrefix(UriTemplateCompoundPathSegment other)
{
return string.Compare(other._firstLiteral, _firstLiteral, StringComparison.OrdinalIgnoreCase);
}
private int CompareSuffixToOtherSuffix(UriTemplateCompoundPathSegment other)
{
var reversedSuffix = ReverseString(_varLitPairs[_varLitPairs.Count - 1].Literal);
var reversedOtherSuffix = ReverseString(other._varLitPairs[other._varLitPairs.Count - 1].Literal);
return string.Compare(reversedOtherSuffix, reversedSuffix, StringComparison.OrdinalIgnoreCase);
}
private static string ReverseString(string stringToReverse)
{
var reversedString = new char[stringToReverse.Length];
for (var i = 0; i < stringToReverse.Length; i++)
{
reversedString[i] = stringToReverse[stringToReverse.Length - i - 1];
}
return new string(reversedString);
}
private enum CompoundSegmentClass
{
Undefined,
HasPrefixAndSuffix,
HasOnlyPrefix,
HasOnlySuffix,
HasNoPrefixNorSuffix
}
private struct VarAndLitPair
{
private readonly string _literal;
private readonly string _varName;
public VarAndLitPair(string varName, string literal)
{
_varName = varName;
_literal = literal;
}
public string Literal
{
get
{
return _literal;
}
}
public string VarName
{
get
{
return _varName;
}
}
}
}
}