File: src\libraries\System.Text.Json\Common\JsonSeparatorNamingPolicy.cs
Web Access
Project: src\src\libraries\System.Text.Json\gen\System.Text.Json.SourceGeneration.Roslyn4.0.csproj (System.Text.Json.SourceGeneration)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
 
namespace System.Text.Json
{
    internal abstract class JsonSeparatorNamingPolicy : JsonNamingPolicy
    {
        private readonly bool _lowercase;
        private readonly char _separator;
 
        internal JsonSeparatorNamingPolicy(bool lowercase, char separator)
        {
            Debug.Assert(char.IsPunctuation(separator));
 
            _lowercase = lowercase;
            _separator = separator;
        }
 
        public sealed override string ConvertName(string name)
        {
            if (name is null)
            {
                ThrowHelper.ThrowArgumentNullException(nameof(name));
            }
 
            return ConvertNameCore(_separator, _lowercase, name.AsSpan());
        }
 
        private static string ConvertNameCore(char separator, bool lowercase, ReadOnlySpan<char> chars)
        {
            char[]? rentedBuffer = null;
 
            // While we can't predict the expansion factor of the resultant string,
            // start with a buffer that is at least 20% larger than the input.
            int initialBufferLength = (int)(1.2 * chars.Length);
            Span<char> destination = initialBufferLength <= JsonConstants.StackallocCharThreshold
                ? stackalloc char[JsonConstants.StackallocCharThreshold]
                : (rentedBuffer = ArrayPool<char>.Shared.Rent(initialBufferLength));
 
            SeparatorState state = SeparatorState.NotStarted;
            int charsWritten = 0;
 
            for (int i = 0; i < chars.Length; i++)
            {
                // NB this implementation does not handle surrogate pair letters
                // cf. https://github.com/dotnet/runtime/issues/90352
 
                char current = chars[i];
                UnicodeCategory category = char.GetUnicodeCategory(current);
 
                switch (category)
                {
                    case UnicodeCategory.UppercaseLetter:
 
                        switch (state)
                        {
                            case SeparatorState.NotStarted:
                                break;
 
                            case SeparatorState.LowercaseLetterOrDigit:
                            case SeparatorState.SpaceSeparator:
                                // An uppercase letter following a sequence of lowercase letters or spaces
                                // denotes the start of a new grouping: emit a separator character.
                                WriteChar(separator, ref destination);
                                break;
 
                            case SeparatorState.UppercaseLetter:
                                // We are reading through a sequence of two or more uppercase letters.
                                // Uppercase letters are grouped together with the exception of the
                                // final letter, assuming it is followed by lowercase letters.
                                // For example, the value 'XMLReader' should render as 'xml_reader',
                                // however 'SHA512Hash' should render as 'sha512-hash'.
                                if (i + 1 < chars.Length && char.IsLower(chars[i + 1]))
                                {
                                    WriteChar(separator, ref destination);
                                }
                                break;
 
                            default:
                                Debug.Fail($"Unexpected state {state}");
                                break;
                        }
 
                        if (lowercase)
                        {
                            current = char.ToLowerInvariant(current);
                        }
 
                        WriteChar(current, ref destination);
                        state = SeparatorState.UppercaseLetter;
                        break;
 
                    case UnicodeCategory.LowercaseLetter:
                    case UnicodeCategory.DecimalDigitNumber:
 
                        if (state is SeparatorState.SpaceSeparator)
                        {
                            // Normalize preceding spaces to one separator.
                            WriteChar(separator, ref destination);
                        }
 
                        if (!lowercase && category is UnicodeCategory.LowercaseLetter)
                        {
                            current = char.ToUpperInvariant(current);
                        }
 
                        WriteChar(current, ref destination);
                        state = SeparatorState.LowercaseLetterOrDigit;
                        break;
 
                    case UnicodeCategory.SpaceSeparator:
                        // Space characters are trimmed from the start and end of the input string
                        // but are normalized to separator characters if between letters.
                        if (state != SeparatorState.NotStarted)
                        {
                            state = SeparatorState.SpaceSeparator;
                        }
                        break;
 
                    default:
                        // Non-alphanumeric characters (including the separator character and surrogates)
                        // are written as-is to the output and reset the separator state.
                        // E.g. 'ABC???def' maps to 'abc???def' in snake_case.
 
                        WriteChar(current, ref destination);
                        state = SeparatorState.NotStarted;
                        break;
                }
            }
 
            string result = destination.Slice(0, charsWritten).ToString();
 
            if (rentedBuffer is not null)
            {
                destination.Slice(0, charsWritten).Clear();
                ArrayPool<char>.Shared.Return(rentedBuffer);
            }
 
            return result;
 
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            void WriteChar(char value, ref Span<char> destination)
            {
                if (charsWritten == destination.Length)
                {
                    ExpandBuffer(ref destination);
                }
 
                destination[charsWritten++] = value;
            }
 
            void ExpandBuffer(ref Span<char> destination)
            {
                int newSize = checked(destination.Length * 2);
                char[] newBuffer = ArrayPool<char>.Shared.Rent(newSize);
                destination.CopyTo(newBuffer);
 
                if (rentedBuffer is not null)
                {
                    destination.Slice(0, charsWritten).Clear();
                    ArrayPool<char>.Shared.Return(rentedBuffer);
                }
 
                rentedBuffer = newBuffer;
                destination = rentedBuffer;
            }
        }
 
        private enum SeparatorState
        {
            NotStarted,
            UppercaseLetter,
            LowercaseLetterOrDigit,
            SpaceSeparator,
        }
    }
}