| | | 1 | | /******************************************************************************** |
| | | 2 | | * ValueParserDefinition.cs * |
| | | 3 | | * * |
| | | 4 | | * Author: Denes Solti * |
| | | 5 | | ********************************************************************************/ |
| | | 6 | | using System; |
| | | 7 | | using System.Collections.Generic; |
| | | 8 | | using System.Diagnostics; |
| | | 9 | | using System.Runtime.CompilerServices; |
| | | 10 | | using System.Text.RegularExpressions; |
| | | 11 | | |
| | | 12 | | namespace NanoRoute.Internals |
| | | 13 | | { |
| | | 14 | | using Properties; |
| | | 15 | | |
| | | 16 | | internal readonly struct ValueParserDefinition |
| | | 17 | | { |
| | | 18 | | private const string |
| | | 19 | | // Matches a valid route parser or parameter identifier. |
| | | 20 | | // Rules: |
| | | 21 | | // - must start with a letter or underscore |
| | | 22 | | // - remaining characters can be letters, digits, or underscores |
| | | 23 | | IDENTIFIER = @"[A-Za-z_]\w*", |
| | | 24 | | |
| | | 25 | | // Matches a boolean literal. |
| | | 26 | | BOOLEAN = @"(?:true|false)", |
| | | 27 | | |
| | | 28 | | // Matches a numeric literal without exponent notation. |
| | | 29 | | NUMBER = @"-?\d+(?:\.\d+)?", |
| | | 30 | | |
| | | 31 | | // Matches a single-quoted string literal. |
| | | 32 | | STRING = @"'(?:\\'|[^'])*'", |
| | | 33 | | |
| | | 34 | | // Matches any supported parser-argument value type. |
| | | 35 | | VALUE = $"(?:null|{BOOLEAN}|{NUMBER}|{STRING})", |
| | | 36 | | |
| | | 37 | | // Captures a single name=value pair. |
| | | 38 | | NAME_VALUE_PAIR = $@"(?<name>{IDENTIFIER})\s*=\s*(?<value>{VALUE})", |
| | | 39 | | |
| | | 40 | | // Matches the full input string and captures every name/value pair. |
| | | 41 | | ARGS_PATTERN = $@"\s*(?:{NAME_VALUE_PAIR}(?:\s*,\s*{NAME_VALUE_PAIR})*)?\s*", |
| | | 42 | | |
| | | 43 | | // Matches and captures a complete parser-backed value definition. |
| | | 44 | | PARSER_DEFINITION_PATTERN = $@"\G(?<parserName>{IDENTIFIER})(?:\({ARGS_PATTERN}\))?"; |
| | | 45 | | |
| | 1 | 46 | | private static readonly Regex s_parserDefinition = new(PARSER_DEFINITION_PATTERN, RuntimeFeature.IsDynamicCodeSu |
| | | 47 | | |
| | | 48 | | private static bool TryExtractArguments(Match parsed, out Dictionary<string, string> result) |
| | 1 | 49 | | { |
| | 1 | 50 | | result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); |
| | | 51 | | |
| | 1 | 52 | | CaptureCollection |
| | 1 | 53 | | names = parsed.Groups["name"].Captures, |
| | 1 | 54 | | values = parsed.Groups["value"].Captures; |
| | | 55 | | |
| | 1 | 56 | | for (int i = 0; i < names.Count; i++) |
| | 1 | 57 | | { |
| | 1 | 58 | | string |
| | 1 | 59 | | name = names[i].Value, |
| | 1 | 60 | | value = values[i].Value; |
| | | 61 | | |
| | 1 | 62 | | if (value[0] is '\'') |
| | 1 | 63 | | value = value |
| | 1 | 64 | | .Substring(1, value.Length - 2) |
| | 1 | 65 | | .Replace("\\'", "'"); |
| | | 66 | | |
| | 1 | 67 | | if (result.ContainsKey(name)) |
| | 1 | 68 | | return false; |
| | | 69 | | |
| | 1 | 70 | | result.Add(name, value); |
| | 1 | 71 | | } |
| | | 72 | | |
| | 1 | 73 | | return true; |
| | 1 | 74 | | } |
| | | 75 | | |
| | | 76 | | public static ValueParserDefinition Parse(string pattern, ref int offset) |
| | 1 | 77 | | { |
| | 1 | 78 | | if (s_parserDefinition.Match(pattern, offset) is not { Success: true, Index: int index, Length: int length } |
| | 1 | 79 | | throw new InvalidOperationException(string.Format(Resources.Culture, Resources.ERR_INVALID_PATTERN, offs |
| | | 80 | | |
| | 1 | 81 | | string parserName = parsed.Groups["parserName"].Value; |
| | 1 | 82 | | Debug.Assert(!string.IsNullOrEmpty(parserName), "Parser name could not be extracted"); |
| | | 83 | | |
| | 1 | 84 | | ValueParserDefinition result = new() |
| | 1 | 85 | | { |
| | 1 | 86 | | Name = parserName, |
| | 1 | 87 | | RawArguments = TryExtractArguments(parsed, out Dictionary<string, string> rawArguments) |
| | 1 | 88 | | ? rawArguments |
| | 1 | 89 | | : throw new InvalidOperationException(string.Format(Resources.Culture, Resources.ERR_INVALID_ARGUMEN |
| | 1 | 90 | | }; |
| | | 91 | | |
| | 1 | 92 | | offset += length; |
| | 1 | 93 | | return result; |
| | 1 | 94 | | } |
| | | 95 | | |
| | | 96 | | public required string Name { get; init; } |
| | | 97 | | |
| | | 98 | | public required IReadOnlyDictionary<string, string> RawArguments { get; init; } |
| | | 99 | | |
| | | 100 | | public override bool Equals(object other) |
| | 1 | 101 | | { |
| | 1 | 102 | | if (other is not ValueParserDefinition otherDef || !otherDef.Name.Equals(Name, StringComparison.OrdinalIgnor |
| | 1 | 103 | | return false; |
| | | 104 | | |
| | 1 | 105 | | if (RawArguments.Count != otherDef.RawArguments.Count) |
| | 1 | 106 | | return false; |
| | | 107 | | |
| | 1 | 108 | | foreach (KeyValuePair<string, string> kvp in otherDef.RawArguments) |
| | 1 | 109 | | if (!RawArguments.TryGetValue(kvp.Key, out string val) || !kvp.Value.Equals(val, StringComparison.Ordina |
| | 1 | 110 | | return false; |
| | | 111 | | |
| | 1 | 112 | | return true; |
| | 1 | 113 | | } |
| | | 114 | | |
| | 1 | 115 | | public override int GetHashCode() => throw new NotImplementedException(); |
| | | 116 | | } |
| | | 117 | | } |