<PackageReference Include="System.ClientModel" Version="1.7.0" />

JsonPathReader

Reads a JsonPath expression and breaks it into tokens for processing. Follows RFC 9535 minus the filter expressions and wildcards, which are not supported in this implementation.
using System.Runtime.CompilerServices; using System.Text; namespace System.ClientModel.Primitives { internal ref struct JsonPathReader { private ref struct PeekedJsonPathToken { public JsonPathToken Token; public bool HasValue; public int Consumed; } private const byte DollarSign = 36; private const byte Dot = 46; private const byte OpenBracket = 91; private const byte CloseBracket = 93; private const byte SingleQuote = 39; private const byte DoubleQuote = 34; private const byte Zero = 48; private const byte Nine = 57; private readonly ReadOnlySpan<byte> _jsonPath; private int _consumed; private int _length; private PeekedJsonPathToken _peeked; public JsonPathToken Current { [System.Runtime.CompilerServices.IsReadOnly] get; private set; } [System.Runtime.CompilerServices.NullableContext(1)] public JsonPathReader(string jsonPath) { this = new JsonPathReader(Encoding.UTF8.GetBytes(jsonPath).AsSpan()); } public JsonPathReader(ReadOnlySpan<byte> jsonPath) { _peeked = default(PeekedJsonPathToken); Current = default(JsonPathToken); _jsonPath = jsonPath; _consumed = 0; _length = jsonPath.Length; } public bool Read() { if (_peeked.HasValue) { Current = _peeked.Token; _consumed = _peeked.Consumed; _peeked.HasValue = false; return true; } JsonPathToken current = Current; if (current.TokenType == JsonPathTokenType.End) return false; if (_consumed < _length) { switch (_jsonPath[_consumed]) { case 36: { ReadOnlySpan<byte> valueSpan = _jsonPath.Slice(_consumed, 1); Current = new JsonPathToken(JsonPathTokenType.Root, _consumed++, valueSpan); return true; } case 46: Current = new JsonPathToken(JsonPathTokenType.PropertySeparator, _consumed++, default(ReadOnlySpan<byte>)); return true; case 91: { if (_consumed + 1 >= _length) throw new FormatException($"""{_consumed + 1}"""); byte b = _jsonPath[_consumed + 1]; if (b == 39 || b == 34) { Current = new JsonPathToken(JsonPathTokenType.PropertySeparator, _consumed++, default(ReadOnlySpan<byte>)); return true; } if (!IsDigit(b)) throw new FormatException($"""{_consumed + 1}"""); _consumed++; Current = ReadNumber(); if (_consumed >= _length || _jsonPath[_consumed] != 93) throw new FormatException($"""{_consumed}"""); _consumed++; return true; } case 34: case 39: current = Current; if (current.TokenType == JsonPathTokenType.PropertySeparator && _jsonPath[_consumed - 1] == 46) Current = ReadProperty(); else { Current = ReadQuotedString(); if (_consumed >= _length || _jsonPath[_consumed] != 93) throw new FormatException($"""{_consumed}"""); _consumed++; } return true; default: Current = ReadProperty(); return true; } } Current = new JsonPathToken(JsonPathTokenType.End, _length - 1, default(ReadOnlySpan<byte>)); return true; } public JsonPathToken Peek() { if (Current.TokenType == JsonPathTokenType.End) return Current; if (_peeked.HasValue) return _peeked.Token; JsonPathReader jsonPathReader = this; jsonPathReader.Read(); _peeked.HasValue = true; _peeked.Consumed = jsonPathReader._consumed; _peeked.Token = jsonPathReader.Current; return _peeked.Token; } public ReadOnlySpan<byte> GetFirstProperty() { if (_jsonPath.IsRoot()) return _jsonPath; if (!Read()) return ReadOnlySpan<byte>.Empty; JsonPathToken current = Current; if (current.TokenType != 0) return ReadOnlySpan<byte>.Empty; if (!Read()) return ReadOnlySpan<byte>.Empty; current = Current; if (current.TokenType != JsonPathTokenType.PropertySeparator || Read()) { current = Current; switch (current.TokenType) { case JsonPathTokenType.Property: return _jsonPath.Slice(0, _consumed); case JsonPathTokenType.ArrayIndex: return _jsonPath.Slice(0, _consumed); default: return ReadOnlySpan<byte>.Empty; } } return ReadOnlySpan<byte>.Empty; } public ReadOnlySpan<byte> GetNextArray() { while (Read()) { if (Current.TokenType == JsonPathTokenType.ArrayIndex) return _jsonPath.Slice(0, _consumed); } return ReadOnlySpan<byte>.Empty; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private JsonPathToken ReadQuotedString() { ReadOnlySpan<byte> jsonPath = _jsonPath; byte b = jsonPath[_consumed]; int num = ++_consumed; while (_consumed < _length && (jsonPath[_consumed] != b || _consumed + 1 >= _length || jsonPath[_consumed + 1] != 93)) { _consumed++; } if (_consumed >= _length) throw new FormatException("Unterminated quoted string in JsonPath"); ReadOnlySpan<byte> valueSpan = jsonPath.Slice(num, _consumed - num); _consumed++; return new JsonPathToken(JsonPathTokenType.Property, num, valueSpan); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private JsonPathToken ReadNumber() { ReadOnlySpan<byte> jsonPath = _jsonPath; int consumed = _consumed; while (_consumed < _length && IsDigit(jsonPath[_consumed])) { _consumed++; } return new JsonPathToken(JsonPathTokenType.ArrayIndex, consumed, jsonPath.Slice(consumed, _consumed - consumed)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private JsonPathToken ReadProperty() { ReadOnlySpan<byte> jsonPath = _jsonPath; int consumed = _consumed; while (_consumed < _length) { byte b = jsonPath[_consumed]; if (b == 46 || b == 91 || b == 93) break; _consumed++; } return new JsonPathToken(JsonPathTokenType.Property, consumed, jsonPath.Slice(consumed, _consumed - consumed)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool IsDigit(byte b) { if (b >= 48) return b <= 57; return false; } public bool Advance(ReadOnlySpan<byte> prefix) { if (prefix.IsEmpty || _jsonPath.IsEmpty) return false; if (prefix.Length >= _length) return false; if (_jsonPath.StartsWith(prefix)) { _consumed = prefix.Length; return true; } if (prefix[0] != 36 || _jsonPath[0] != 36) return false; JsonPathReader jsonPathReader = new JsonPathReader(prefix); JsonPathToken current; while (Read() && jsonPathReader.Read()) { current = Current; ReadOnlySpan<byte> valueSpan = current.ValueSpan; current = jsonPathReader.Current; if (!valueSpan.SequenceEqual(current.ValueSpan)) return false; } current = jsonPathReader.Current; if (current.TokenType == JsonPathTokenType.End) { current = Current; return current.TokenType != JsonPathTokenType.End; } return false; } internal ReadOnlySpan<byte> GetParsedPath() { if (_consumed == 0) return ReadOnlySpan<byte>.Empty; return _jsonPath.Slice(0, _consumed); } public bool Equals(JsonPathReader other) { return JsonPathComparer.Default.NormalizedEquals(_jsonPath, other._jsonPath); } public override int GetHashCode() { return JsonPathComparer.Default.GetNormalizedHashCode(_jsonPath); } } }