File: parent\Shared\HttpRoutePatternParser.cs
Web Access
Project: ..\..\..\src\aspnetcore\src\Grpc\JsonTranscoding\src\Microsoft.AspNetCore.Grpc.JsonTranscoding\Microsoft.AspNetCore.Grpc.JsonTranscoding.csproj (Microsoft.AspNetCore.Grpc.JsonTranscoding)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
 
namespace Grpc.Shared;
 
// HTTP Template Grammar:
//
// Template = "/" | "/" Segments [ Verb ] ;
// Segments = Segment { "/" Segment } ;
// Segment  = "*" | "**" | LITERAL | Variable ;
// Variable = "{" FieldPath [ "=" Segments ] "}" ;
// FieldPath = IDENT { "." IDENT } ;
// Verb     = ":" LITERAL ;
internal class HttpRoutePatternParser
{
    private readonly string _input;
 
    // Token delimiter indexes
    private int _tokenStart;
    private int _tokenEnd;
 
    private bool _inVariable;
 
    private readonly List<string> _segments;
    private string? _verb;
    private readonly List<HttpRouteVariable> _variables;
    private bool _hasCatchAllSegment;
 
    public List<string> Segments => _segments;
    public string? Verb => _verb;
    public List<HttpRouteVariable> Variables => _variables;
 
    public HttpRoutePatternParser(string input)
    {
        _input = input;
        _segments = new List<string>();
        _variables = new List<HttpRouteVariable>();
    }
 
    public void Parse()
    {
        try
        {
            ParseTemplate();
 
            if (_tokenStart < _input.Length)
            {
                throw new InvalidOperationException("Path template wasn't parsed to the end.");
            }
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException($"Error parsing path template '{_input}'.", ex);
        }
    }
 
    // Template = "/" Segments [ Verb ] ;
    private void ParseTemplate()
    {
        if (!Consume('/'))
        {
            throw new InvalidOperationException("Path template must start with a '/'.");
        }
        ParseSegments();
 
        if (EnsureCurrent())
        {
            if (CurrentChar != ':')
            {
                throw new InvalidOperationException("Path segment must end with a '/'.");
            }
            ParseVerb();
        }
    }
 
    // Segments = Segment { "/" Segment } ;
    private void ParseSegments()
    {
        while (true)
        {
            if (!ParseSegment())
            {
                // Support '/' template.
                if (_segments.Count > 0)
                {
                    throw new InvalidOperationException("Route template shouldn't end with a '/'.");
                }
            }
            if (!Consume('/'))
            {
                break;
            }
        }
    }
 
    // Segment  = "*" | "**" | LITERAL | Variable ;
    private bool ParseSegment()
    {
        if (!EnsureCurrent())
        {
            return false;
        }
        switch (CurrentChar)
        {
            case '*':
                {
                    if (_hasCatchAllSegment)
                    {
                        throw new InvalidOperationException("Only literal segments can follow a catch-all segment.");
                    }
 
                    ConsumeAndAssert('*');
 
                    // Check for '**'
                    if (Consume('*'))
                    {
                        _segments.Add("**");
                        _hasCatchAllSegment = true;
                        if (_inVariable)
                        {
                            CurrentVariable.HasCatchAllPath = true;
                        }
                        return true;
                    }
                    else
                    {
                        _segments.Add("*");
                        return true;
                    }
                }
 
            case '{':
                if (_hasCatchAllSegment)
                {
                    throw new InvalidOperationException("Only literal segments can follow a catch-all segment.");
                }
 
                ParseVariable();
                return true;
            default:
                ParseLiteralSegment();
                return true;
        }
    }
 
    // Variable = "{" FieldPath [ "=" Segments ] "}" ;
    private void ParseVariable()
    {
        ConsumeAndAssert('{');
        StartVariable();
        ParseFieldPath();
        if (Consume('='))
        {
            ParseSegments();
        }
        else
        {
            _segments.Add("*");
        }
        EndVariable();
        ConsumeAndAssert('}');
    }
 
    private void ParseLiteralSegment()
    {
        if (!TryParseLiteral(out var literal))
        {
            throw new InvalidOperationException("Empty literal segment.");
        }
        _segments.Add(literal);
    }
 
    // FieldPath = IDENT { "." IDENT } ;
    private void ParseFieldPath()
    {
        do
        {
            if (!ParseIdentifier())
            {
                throw new InvalidOperationException("Incomplete or empty field path.");
            }
        }
        while (Consume('.'));
    }
 
    // Verb     = ":" LITERAL ;
    private void ParseVerb()
    {
        ConsumeAndAssert(':');
        if (!TryParseLiteral(out _verb))
        {
            throw new InvalidOperationException("Empty verb.");
        }
    }
 
    private bool ParseIdentifier()
    {
        var identifier = string.Empty;
        var hasEndChar = false;
 
        while (!hasEndChar && NextChar())
        {
            var c = CurrentChar;
            switch (c)
            {
                case '.':
                case '}':
                case '=':
                    hasEndChar = true;
                    break;
                default:
                    Consume(c);
                    identifier += c;
                    break;
            }
        }
 
        if (string.IsNullOrEmpty(identifier))
        {
            return false;
        }
 
        CurrentVariable.FieldPath.Add(identifier);
        return true;
    }
 
    private bool TryParseLiteral([NotNullWhen(true)] out string? literal)
    {
        literal = null;
 
        if (!EnsureCurrent())
        {
            return false;
        }
 
        // Initialize to false in case we encounter an empty literal.
        var result = false;
 
        while (true)
        {
            var c = CurrentChar;
            switch (c)
            {
                case '/':
                case ':':
                case '}':
                    if (!result)
                    {
                        throw new InvalidOperationException("Path template has an empty segment.");
                    }
                    return result;
                default:
                    Consume(c);
                    literal += c;
                    break;
            }
 
            result = true;
 
            if (!NextChar())
            {
                break;
            }
        }
 
        return result;
    }
 
    private void ConsumeAndAssert(char? c)
    {
        if (!Consume(c))
        {
            throw new InvalidOperationException($"Expected '{c}' when parsing path template.");
        }
    }
 
    private bool Consume(char? c)
    {
        if (!EnsureCurrent())
        {
            return false;
        }
        if (CurrentChar != c)
        {
            return false;
        }
        _tokenStart++;
        return true;
    }
 
    private bool EnsureCurrent() => _tokenStart < _tokenEnd || NextChar();
 
    private bool NextChar()
    {
        if (_tokenEnd < _input.Length)
        {
            _tokenEnd++;
            return true;
        }
        else
        {
            return false;
        }
    }
 
    private char? CurrentChar => _tokenStart < _tokenEnd && _tokenEnd <= _input.Length ? _input[_tokenEnd - 1] : null;
 
    private HttpRouteVariable CurrentVariable
    {
        get
        {
            if (!_inVariable || _variables.LastOrDefault() is not HttpRouteVariable variable)
            {
                throw new InvalidOperationException("Unexpected error when updating variable.");
            }
 
            return variable;
        }
 
    }
 
    private void StartVariable()
    {
        if (_inVariable)
        {
            throw new InvalidOperationException("Variable can't be nested.");
        }
 
        _variables.Add(new HttpRouteVariable());
        _inVariable = true;
        CurrentVariable.StartSegment = _segments.Count;
        CurrentVariable.HasCatchAllPath = false;
    }
 
    private void EndVariable()
    {
        CurrentVariable.EndSegment = _segments.Count;
 
        Debug.Assert(CurrentVariable.FieldPath.Any());
        Debug.Assert(CurrentVariable.StartSegment < CurrentVariable.EndSegment);
        Debug.Assert(CurrentVariable.EndSegment <= _segments.Count);
 
        _inVariable = false;
    }
}