You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Darwinism/Google.Protobuf/JSON/JsonFormatter.vb

865 lines
39 KiB

#Region "Microsoft.VisualBasic::b3c1b9e44f4b060529b848b800c95e2f, Google.Protobuf\JSON\JsonFormatter.vb"
' Author:
'
' asuka (amethyst.asuka@gcmodeller.org)
' xie (genetics@smrucc.org)
' xieguigang (xie.guigang@live.com)
'
' Copyright (c) 2018 GPL3 Licensed
'
'
' GNU GENERAL PUBLIC LICENSE (GPL3)
'
'
' This program is free software: you can redistribute it and/or modify
' it under the terms of the GNU General Public License as published by
' the Free Software Foundation, either version 3 of the License, or
' (at your option) any later version.
'
' This program is distributed in the hope that it will be useful,
' but WITHOUT ANY WARRANTY; without even the implied warranty of
' MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
' GNU General Public License for more details.
'
' You should have received a copy of the GNU General Public License
' along with this program. If not, see <http://www.gnu.org/licenses/>.
' /********************************************************************************/
' Summaries:
' Class JsonFormatter
'
' Properties: [Default], DiagnosticOnly
'
' Constructor: (+2 Overloads) Sub New
'
' Function: Format, IsDefaultValue, ToCamelCase, ToCamelCaseForFieldMask, ToDiagnosticString
' WriteMessageFields
'
' Sub: Format, HexEncodeUtf16CodeUnit, WriteAny, WriteDiagnosticOnlyAny, WriteDictionary
' WriteDuration, WriteFieldMask, WriteList, WriteMessage, WriteNull
' WriteString, WriteStruct, WriteStructFieldValue, WriteTimestamp, WriteValue
' WriteWellKnownTypeValue
' Class Settings
'
' Properties: [Default], FormatDefaultValues, TypeRegistry
'
' Constructor: (+3 Overloads) Sub New
'
' Class OriginalEnumValueHelper
'
' Function: GetNameMapping, GetOriginalName
'
'
'
'
' /********************************************************************************/
#End Region
#Region "Copyright notice and license"
' Protocol Buffers - Google's data interchange format
' Copyright 2015 Google Inc. All rights reserved.
' https://developers.google.com/protocol-buffers/
'
' Redistribution and use in source and binary forms, with or without
' modification, are permitted provided that the following conditions are
' met:
'
' * Redistributions of source code must retain the above copyright
' notice, this list of conditions and the following disclaimer.
' * Redistributions in binary form must reproduce the above
' copyright notice, this list of conditions and the following disclaimer
' in the documentation and/or other materials provided with the
' distribution.
' * Neither the name of Google Inc. nor the names of its
' contributors may be used to endorse or promote products derived from
' this software without specific prior written permission.
'
' THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
' "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
' LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
' A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
' OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
' SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
' LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
' DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
' THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
' (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
' OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#End Region
Imports System
Imports System.Collections
Imports System.Globalization
Imports System.Text
Imports Google.Protobuf.Reflection
Imports Google.Protobuf.WellKnownTypes
Imports System.IO
Imports System.Linq
Imports System.Collections.Generic
Imports System.Reflection
Imports Microsoft.VisualBasic.Language
Namespace Google.Protobuf
''' <summary>
''' Reflection-based converter from messages to JSON.
''' </summary>
''' <remarks>
''' <para>
''' Instances of this class are thread-safe, with no mutable state.
''' </para>
''' <para>
''' This is a simple start to get JSON formatting working. As it's reflection-based,
''' it's not as quick as baking calls into generated messages - but is a simpler implementation.
''' (This code is generally not heavily optimized.)
''' </para>
''' </remarks>
Public NotInheritable Class JsonFormatter
Friend Const AnyTypeUrlField As String = "@type"
Friend Const AnyDiagnosticValueField As String = "@value"
Friend Const AnyWellKnownTypeValueField As String = "value"
Private Const TypeUrlPrefix As String = "type.googleapis.com"
Private Const NameValueSeparator As String = ": "
Private Const PropertySeparator As String = ", "
''' <summary>
''' Returns a formatter using the default settings.
''' </summary>
Public Shared ReadOnly Property [Default] As JsonFormatter = New JsonFormatter(Settings.Default)
' A JSON formatter which *only* exists
Private Shared ReadOnly diagnosticFormatter As JsonFormatter = New JsonFormatter(Settings.Default)
''' <summary>
''' The JSON representation of the first 160 characters of Unicode.
''' Empty strings are replaced by the static constructor.
''' </summary>
' C0 (ASCII and derivatives) control characters
' Escaping of " and \ are required by www.json.org string definition.
' Escaping of < and > are required for HTML security.
' C1 (ISO 8859 and Unicode) extended control characters
Private Shared ReadOnly CommonRepresentations As String() = {"\u0000", "\u0001", "\u0002", "\u0003", "\u0004", "\u0005", "\u0006", "\u0007", "\b", "\t", "\n", "\u000b", "\f", "\r", "\u000e", "\u000f", "\u0010", "\u0011", "\u0012", "\u0013", "\u0014", "\u0015", "\u0016", "\u0017", "\u0018", "\u0019", "\u001a", "\u001b", "\u001c", "\u001d", "\u001e", "\u001f", "", "", "\""", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "\u003c", "", "\u003e", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "\\", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "\u007f", "\u0080", "\u0081", "\u0082", "\u0083", "\u0084", "\u0085", "\u0086", "\u0087", "\u0088", "\u0089", "\u008a", "\u008b", "\u008c", "\u008d", "\u008e", "\u008f", "\u0090", "\u0091", "\u0092", "\u0093", "\u0094", "\u0095", "\u0096", "\u0097", "\u0098", "\u0099", "\u009a", "\u009b", "\u009c", "\u009d", "\u009e", "\u009f"} ' 0x00
' 0x10
' 0x20
' 0x30
' 0x40
' 0x50
' 0x60
' 0x70
' 0x80
' 0x90
Shared Sub New()
For i = 0 To CommonRepresentations.Length - 1
If Equals(CommonRepresentations(i), "") Then
CommonRepresentations(i) = Microsoft.VisualBasic.ChrW(i).ToString()
End If
Next
End Sub
Private ReadOnly settingsField As Settings
Private ReadOnly Property DiagnosticOnly As Boolean
Get
Return ReferenceEquals(Me, diagnosticFormatter)
End Get
End Property
''' <summary>
''' Creates a new formatted with the given settings.
''' </summary>
''' <param name="settings">The settings.</param>
Public Sub New(settings As Settings)
settingsField = settings
End Sub
''' <summary>
''' Formats the specified message as JSON.
''' </summary>
''' <param name="message">The message to format.</param>
''' <returns>The formatted message.</returns>
Public Function Format(message As IMessage) As String
Dim writer = New StringWriter()
Format(message, writer)
Return writer.ToString()
End Function
''' <summary>
''' Formats the specified message as JSON.
''' </summary>
''' <param name="message">The message to format.</param>
''' <param name="writer">The TextWriter to write the formatted message to.</param>
''' <returns>The formatted message.</returns>
Public Sub Format(message As IMessage, writer As TextWriter)
CheckNotNull(message, NameOf(message))
CheckNotNull(writer, NameOf(writer))
If message.Descriptor.IsWellKnownType Then
WriteWellKnownTypeValue(writer, message.Descriptor, message)
Else
WriteMessage(writer, message)
End If
End Sub
''' <summary>
''' Converts a message to JSON for diagnostic purposes with no extra context.
''' </summary>
''' <remarks>
''' <para>
''' This differs from calling <see cref="Format(IMessage)"/> on the default JSON
''' formatter in its handling of <see cref="Any"/>. As no type registry is available
''' in <see cref="Object.ToString"/> calls, the normal way of resolving the type of
''' an <c>Any</c> message cannot be applied. Instead, a JSON property named <c>@value</c>
''' is included with the base64 data from the <see cref="Any.Value"/> property of the message.
''' </para>
''' <para>The value returned by this method is only designed to be used for diagnostic
''' purposes. It may not be parsable by <see cref="JsonParser"/>, and may not be parsable
''' by other Protocol Buffer implementations.</para>
''' </remarks>
''' <param name="message">The message to format for diagnostic purposes.</param>
''' <returns>The diagnostic-only JSON representation of the message</returns>
Public Shared Function ToDiagnosticString(message As IMessage) As String
CheckNotNull(message, NameOf(message))
Return diagnosticFormatter.Format(message)
End Function
Private Sub WriteMessage(writer As TextWriter, message As IMessage)
If message Is Nothing Then
WriteNull(writer)
Return
End If
If DiagnosticOnly Then
Dim customDiagnosticMessage As ICustomDiagnosticMessage = TryCast(message, ICustomDiagnosticMessage)
If customDiagnosticMessage IsNot Nothing Then
writer.Write(customDiagnosticMessage.ToDiagnosticString())
Return
End If
End If
writer.Write("{ ")
Dim writtenFields = WriteMessageFields(writer, message, False)
writer.Write(If(writtenFields, " }", "}"))
End Sub
Private Function WriteMessageFields(writer As TextWriter, message As IMessage, assumeFirstFieldWritten As Boolean) As Boolean
Dim fields = message.Descriptor.Fields
Dim first = Not assumeFirstFieldWritten
' First non-oneof fields
For Each field In fields.InFieldNumberOrder()
Dim accessor = field.Accessor
If field.ContainingOneof IsNot Nothing AndAlso field.ContainingOneof.Accessor.GetCaseFieldDescriptor(message) IsNot field Then
Continue For
End If
' Omit default values unless we're asked to format them, or they're oneofs (where the default
' value is still formatted regardless, because that's how we preserve the oneof case).
Dim value = accessor.GetValue(message)
If field.ContainingOneof Is Nothing AndAlso Not settingsField.FormatDefaultValues AndAlso IsDefaultValue(accessor, value) Then
Continue For
End If
' Okay, all tests complete: let's write the field value...
If Not first Then
writer.Write(PropertySeparator)
End If
WriteString(writer, accessor.Descriptor.JsonName)
writer.Write(NameValueSeparator)
WriteValue(writer, value)
first = False
Next
Return Not first
End Function
''' <summary>
''' Camel-case converter with added strictness for field mask formatting.
''' </summary>
''' <exception cref="InvalidOperationException">The field mask is invalid for JSON representation</exception>
Private Shared Function ToCamelCaseForFieldMask(input As String) As String
For i = 0 To input.Length - 1
Dim c = input(i)
If c >= "A"c AndAlso c <= "Z"c Then
Throw New InvalidOperationException($"Invalid field mask to be converted to JSON: {input}")
End If
If c = "_"c AndAlso i < input.Length - 1 Then
Dim [next] = input(i + 1)
If [next] < "a"c OrElse [next] > "z"c Then
Throw New InvalidOperationException($"Invalid field mask to be converted to JSON: {input}")
End If
End If
Next
Return ToCamelCase(input)
End Function
' Converted from src/google/protobuf/util/internal/utility.cc ToCamelCase
' TODO: Use the new field in FieldDescriptor.
Friend Shared Function ToCamelCase(input As String) As String
Dim capitalizeNext = False
Dim wasCap = True
Dim isCap = False
Dim firstWord = True
Dim result As StringBuilder = New StringBuilder(input.Length)
Dim i = 0
While i < input.Length
isCap = Char.IsUpper(input(i))
If input(i) = "_"c Then
capitalizeNext = True
If result.Length <> 0 Then
firstWord = False
End If
Continue While
ElseIf firstWord Then
' Consider when the current character B is capitalized,
' first word ends when:
' 1) following a lowercase: "...aB..."
' 2) followed by a lowercase: "...ABc..."
If result.Length <> 0 AndAlso isCap AndAlso (Not wasCap OrElse i + 1 < input.Length AndAlso Char.IsLower(input(i + 1))) Then
firstWord = False
Else
result.Append(Char.ToLowerInvariant(input(i)))
Continue While
End If
ElseIf capitalizeNext Then
capitalizeNext = False
If Char.IsLower(input(i)) Then
result.Append(Char.ToUpperInvariant(input(i)))
Continue While
End If
End If
result.Append(input(i))
i += 1
wasCap = isCap
End While
Return result.ToString()
End Function
Private Shared Sub WriteNull(writer As TextWriter)
writer.Write("null")
End Sub
Private Shared Function IsDefaultValue(accessor As IFieldAccessor, value As Object) As Boolean
If accessor.Descriptor.IsMap Then
Dim dictionary = CType(value, IDictionary)
Return dictionary.Count = 0
End If
If accessor.Descriptor.IsRepeated Then
Dim list = CType(value, IList)
Return list.Count = 0
End If
Select Case accessor.Descriptor.FieldType
Case FieldType.Bool
Return CBool(value) = False
Case FieldType.Bytes
Return CType(value, ByteString) Is ByteString.Empty
Case FieldType.String
Return Equals(CStr(value), "")
Case FieldType.Double
Return CDbl(value) = 0.0
Case FieldType.SInt32, FieldType.Int32, FieldType.SFixed32, FieldType.Enum
Return CInt(value) = 0
Case FieldType.Fixed32, FieldType.UInt32
Return CUInt(value) = 0
Case FieldType.Fixed64, FieldType.UInt64
Return CULng(value) = 0
Case FieldType.SFixed64, FieldType.Int64, FieldType.SInt64
Return CLng(value) = 0
Case FieldType.Float
Return CSng(value) = 0F
Case FieldType.Message, FieldType.Group ' Never expect to get this, but...
Return value Is Nothing
Case Else
Throw New ArgumentException("Invalid field type")
End Select
End Function
''' <summary>
''' Writes a single value to the given writer as JSON. Only types understood by
''' Protocol Buffers can be written in this way. This method is only exposed for
''' advanced use cases; most users should be using <see cref="Format(IMessage)"/>
''' or <see cref="Format(IMessage,TextWriter)"/>.
''' </summary>
''' <param name="writer">The writer to write the value to. Must not be null.</param>
''' <param name="value">The value to write. May be null.</param>
Public Sub WriteValue(writer As TextWriter, value As Object)
If value Is Nothing Then
WriteNull(writer)
ElseIf TypeOf value Is Boolean Then
writer.Write(If(value, "true", "false"))
ElseIf TypeOf value Is ByteString Then
' Nothing in Base64 needs escaping
writer.Write(""""c)
writer.Write(CType(value, ByteString).ToBase64())
writer.Write(""""c)
ElseIf TypeOf value Is String Then
WriteString(writer, CStr(value))
ElseIf TypeOf value Is IDictionary Then
WriteDictionary(writer, CType(value, IDictionary))
ElseIf TypeOf value Is IList Then
WriteList(writer, CType(value, IList))
ElseIf TypeOf value Is Integer OrElse TypeOf value Is UInteger Then
Dim formattable = CType(value, IFormattable)
writer.Write(formattable.ToString("d", CultureInfo.InvariantCulture))
ElseIf TypeOf value Is Long OrElse TypeOf value Is ULong Then
writer.Write(""""c)
Dim formattable = CType(value, IFormattable)
writer.Write(formattable.ToString("d", CultureInfo.InvariantCulture))
writer.Write(""""c)
ElseIf TypeOf value Is System.Enum Then
Dim name = OriginalEnumValueHelper.GetOriginalName(value)
If Not Equals(name, Nothing) Then
WriteString(writer, name)
Else
WriteValue(writer, CInt(value))
End If
ElseIf TypeOf value Is Single OrElse TypeOf value Is Double Then
Dim text = CType(value, IFormattable).ToString("r", CultureInfo.InvariantCulture)
If Equals(text, "NaN") OrElse Equals(text, "Infinity") OrElse Equals(text, "-Infinity") Then
writer.Write(""""c)
writer.Write(text)
writer.Write(""""c)
Else
writer.Write(text)
End If
ElseIf TypeOf value Is IMessage Then
Format(CType(value, IMessage), writer)
Else
Throw New ArgumentException("Unable to format value of type " & value.GetType().ToString)
End If
End Sub
''' <summary>
''' Central interception point for well-known type formatting. Any well-known types which
''' don't need special handling can fall back to WriteMessage. We avoid assuming that the
''' values are using the embedded well-known types, in order to allow for dynamic messages
''' in the future.
''' </summary>
Private Sub WriteWellKnownTypeValue(writer As TextWriter, descriptor As MessageDescriptor, value As Object)
' Currently, we can never actually get here, because null values are always handled by the caller. But if we *could*,
' this would do the right thing.
If value Is Nothing Then
WriteNull(writer)
Return
End If
' For wrapper types, the value will either be the (possibly boxed) "native" value,
' or the message itself if we're formatting it at the top level (e.g. just calling ToString on the object itself).
' If it's the message form, we can extract the value first, which *will* be the (possibly boxed) native value,
' and then proceed, writing it as if we were definitely in a field. (We never need to wrap it in an extra string...
' WriteValue will do the right thing.)
If descriptor.IsWrapperType Then
If TypeOf value Is IMessage Then
Dim message = CType(value, IMessage)
value = message.Descriptor.Fields(WrapperValueFieldNumber).Accessor.GetValue(message)
End If
WriteValue(writer, value)
Return
End If
If Equals(descriptor.FullName, Timestamp.DescriptorProp.FullName) Then
WriteTimestamp(writer, CType(value, IMessage))
Return
End If
If Equals(descriptor.FullName, Duration.DescriptorProp.FullName) Then
WriteDuration(writer, CType(value, IMessage))
Return
End If
If Equals(descriptor.FullName, FieldMask.DescriptorProp.FullName) Then
WriteFieldMask(writer, CType(value, IMessage))
Return
End If
If Equals(descriptor.FullName, Struct.DescriptorProp.FullName) Then
WriteStruct(writer, CType(value, IMessage))
Return
End If
If Equals(descriptor.FullName, ListValue.DescriptorProp.FullName) Then
Dim fieldAccessor = descriptor.Fields(ListValue.ValuesFieldNumber).Accessor
WriteList(writer, CType(fieldAccessor.GetValue(CType(value, IMessage)), IList))
Return
End If
If Equals(descriptor.FullName, WellKnownTypes.Value.DescriptorProp.FullName) Then
WriteStructFieldValue(writer, CType(value, IMessage))
Return
End If
If Equals(descriptor.FullName, Any.DescriptorProp.FullName) Then
WriteAny(writer, CType(value, IMessage))
Return
End If
WriteMessage(writer, CType(value, IMessage))
End Sub
Private Sub WriteTimestamp(writer As TextWriter, value As IMessage)
' TODO: In the common case where this *is* using the built-in Timestamp type, we could
' avoid all the reflection at this point, by casting to Timestamp. In the interests of
' avoiding subtle bugs, don't do that until we've implemented DynamicMessage so that we can prove
' it still works in that case.
Dim nanos As Integer = value.Descriptor.Fields(Timestamp.NanosFieldNumber).Accessor.GetValue(value)
Dim seconds As Long = value.Descriptor.Fields(Timestamp.SecondsFieldNumber).Accessor.GetValue(value)
writer.Write(Timestamp.ToJson(seconds, nanos, DiagnosticOnly))
End Sub
Private Sub WriteDuration(writer As TextWriter, value As IMessage)
' TODO: Same as for WriteTimestamp
Dim nanos As Integer = value.Descriptor.Fields(Duration.NanosFieldNumber).Accessor.GetValue(value)
Dim seconds As Long = value.Descriptor.Fields(Duration.SecondsFieldNumber).Accessor.GetValue(value)
writer.Write(Duration.ToJson(seconds, nanos, DiagnosticOnly))
End Sub
Private Sub WriteFieldMask(writer As TextWriter, value As IMessage)
Dim paths = CType(value.Descriptor.Fields(FieldMask.PathsFieldNumber).Accessor.GetValue(value), IList(Of String))
writer.Write(FieldMask.ToJson(paths, DiagnosticOnly))
End Sub
Private Sub WriteAny(writer As TextWriter, value As IMessage)
If DiagnosticOnly Then
WriteDiagnosticOnlyAny(writer, value)
Return
End If
Dim typeUrl = CStr(value.Descriptor.Fields(Any.TypeUrlFieldNumber).Accessor.GetValue(value))
Dim data = CType(value.Descriptor.Fields(Any.ValueFieldNumber).Accessor.GetValue(value), ByteString)
Dim typeName = Any.GetTypeName(typeUrl)
Dim descriptor = settingsField.TypeRegistry.Find(typeName)
If descriptor Is Nothing Then
Throw New InvalidOperationException($"Type registry has no descriptor for type name '{typeName}'")
End If
Dim message = descriptor.Parser.ParseFrom(data)
writer.Write("{ ")
WriteString(writer, AnyTypeUrlField)
writer.Write(NameValueSeparator)
WriteString(writer, typeUrl)
If descriptor.IsWellKnownType Then
writer.Write(PropertySeparator)
WriteString(writer, AnyWellKnownTypeValueField)
writer.Write(NameValueSeparator)
WriteWellKnownTypeValue(writer, descriptor, message)
Else
WriteMessageFields(writer, message, True)
End If
writer.Write(" }")
End Sub
Private Sub WriteDiagnosticOnlyAny(writer As TextWriter, value As IMessage)
Dim typeUrl = CStr(value.Descriptor.Fields(Any.TypeUrlFieldNumber).Accessor.GetValue(value))
Dim data = CType(value.Descriptor.Fields(Any.ValueFieldNumber).Accessor.GetValue(value), ByteString)
writer.Write("{ ")
WriteString(writer, AnyTypeUrlField)
writer.Write(NameValueSeparator)
WriteString(writer, typeUrl)
writer.Write(PropertySeparator)
WriteString(writer, AnyDiagnosticValueField)
writer.Write(NameValueSeparator)
writer.Write(""""c)
writer.Write(data.ToBase64())
writer.Write(""""c)
writer.Write(" }")
End Sub
Private Sub WriteStruct(writer As TextWriter, message As IMessage)
writer.Write("{ ")
Dim fields = CType(message.Descriptor.Fields(Struct.FieldsFieldNumber).Accessor.GetValue(message), IDictionary)
Dim first = True
For Each entry As DictionaryEntry In fields
Dim key = CStr(entry.Key)
Dim value = CType(entry.Value, IMessage)
If String.IsNullOrEmpty(key) OrElse value Is Nothing Then
Throw New InvalidOperationException("Struct fields cannot have an empty key or a null value.")
End If
If Not first Then
writer.Write(PropertySeparator)
End If
WriteString(writer, key)
writer.Write(NameValueSeparator)
WriteStructFieldValue(writer, value)
first = False
Next
writer.Write(If(first, "}", " }"))
End Sub
Private Sub WriteStructFieldValue(writer As TextWriter, message As IMessage)
Dim specifiedField = message.Descriptor.Oneofs(0).Accessor.GetCaseFieldDescriptor(message)
If specifiedField Is Nothing Then
Throw New InvalidOperationException("Value message must contain a value for the oneof.")
End If
Dim value = specifiedField.Accessor.GetValue(message)
Select Case specifiedField.FieldNumber
Case WellKnownTypes.Value.BoolValueFieldNumber, WellKnownTypes.Value.StringValueFieldNumber, WellKnownTypes.Value.NumberValueFieldNumber
WriteValue(writer, value)
Return
Case WellKnownTypes.Value.StructValueFieldNumber, WellKnownTypes.Value.ListValueFieldNumber
' Structs and ListValues are nested messages, and already well-known types.
Dim nestedMessage = CType(specifiedField.Accessor.GetValue(message), IMessage)
WriteWellKnownTypeValue(writer, nestedMessage.Descriptor, nestedMessage)
Return
Case WellKnownTypes.Value.NullValueFieldNumber
WriteNull(writer)
Return
Case Else
Throw New InvalidOperationException("Unexpected case in struct field: " & specifiedField.FieldNumber)
End Select
End Sub
Friend Sub WriteList(writer As TextWriter, list As IList)
writer.Write("[ ")
Dim first = True
For Each value In list
If Not first Then
writer.Write(PropertySeparator)
End If
WriteValue(writer, value)
first = False
Next
writer.Write(If(first, "]", " ]"))
End Sub
Friend Sub WriteDictionary(writer As TextWriter, dictionary As IDictionary)
writer.Write("{ ")
Dim first = True
' This will box each pair. Could use IDictionaryEnumerator, but that's ugly in terms of disposal.
For Each pair As DictionaryEntry In dictionary
If Not first Then
writer.Write(PropertySeparator)
End If
Dim keyText As String
If TypeOf pair.Key Is String Then
keyText = CStr(pair.Key)
ElseIf TypeOf pair.Key Is Boolean Then
keyText = If(pair.Key, "true", "false")
ElseIf TypeOf pair.Key Is Integer OrElse TypeOf pair.Key Is UInteger Or TypeOf pair.Key Is Long OrElse TypeOf pair.Key Is ULong Then
keyText = CType(pair.Key, IFormattable).ToString("d", CultureInfo.InvariantCulture)
Else
If pair.Key Is Nothing Then
Throw New ArgumentException("Dictionary has entry with null key")
End If
Throw New ArgumentException("Unhandled dictionary key type: " & pair.Key.GetType().ToString)
End If
WriteString(writer, keyText)
writer.Write(NameValueSeparator)
WriteValue(writer, pair.Value)
first = False
Next
writer.Write(If(first, "}", " }"))
End Sub
''' <summary>
''' Writes a string (including leading and trailing double quotes) to a builder, escaping as required.
''' </summary>
''' <remarks>
''' Other than surrogate pair handling, this code is mostly taken from src/google/protobuf/util/internal/json_escaping.cc.
''' </remarks>
Friend Shared Sub WriteString(writer As TextWriter, text As String)
writer.Write(""""c)
For i = 0 To text.Length - 1
Dim c As chr = text(i)
If c < &HA0 Then
writer.Write(CommonRepresentations(c))
Continue For
End If
If Char.IsHighSurrogate(c) Then
' Encountered first part of a surrogate pair.
' Check that we have the whole pair, and encode both parts as hex.
i += 1
If i = text.Length OrElse Not Char.IsLowSurrogate(text(i)) Then
Throw New ArgumentException("String contains low surrogate not followed by high surrogate")
End If
HexEncodeUtf16CodeUnit(writer, c)
HexEncodeUtf16CodeUnit(writer, text(i))
Continue For
ElseIf Char.IsLowSurrogate(c) Then
Throw New ArgumentException("String contains high surrogate not preceded by low surrogate")
End If
Select Case CInt(c)
' These are not required by json spec
' but used to prevent security bugs in javascript.
Case &HFEFF, &HFFF9, &HFFFA, &HFFFB, &HAD, &H6DD, &H70F, &H17B4, &H17B5 ' Zero width no-break space
' Interlinear annotation anchor
' Interlinear annotation separator
' Interlinear annotation terminator
' Soft-hyphen
' Arabic end of ayah
' Syriac abbreviation mark
' Khmer vowel inherent Aq
' Khmer vowel inherent Aa
HexEncodeUtf16CodeUnit(writer, c)
Case Else
If c >= &H600 AndAlso c <= &H603 OrElse c >= &H200B AndAlso c <= &H200F OrElse c >= &H2028 AndAlso c <= &H202E OrElse c >= &H2060 AndAlso c <= &H2064 OrElse c >= &H206A AndAlso c <= &H206F Then ' Arabic signs
' Zero width etc.
' Separators etc.
' Invisible etc.
HexEncodeUtf16CodeUnit(writer, c)
Else
' No handling of surrogates here - that's done earlier
writer.Write(c)
End If
End Select
Next
writer.Write(""""c)
End Sub
Private Const Hex As String = "0123456789abcdef"
Private Shared Sub HexEncodeUtf16CodeUnit(writer As TextWriter, c As chr)
writer.Write("\u")
writer.Write(Hex(c >> 12 And &HF))
writer.Write(Hex(c >> 8 And &HF))
writer.Write(Hex(c >> 4 And &HF))
writer.Write(Hex(c >> 0 And &HF))
End Sub
''' <summary>
''' Settings controlling JSON formatting.
''' </summary>
Public NotInheritable Class Settings
''' <summary>
''' Default settings, as used by <see cref="JsonFormatter.Default"/>
''' </summary>
Public Shared ReadOnly Property [Default] As Settings
' Workaround for the Mono compiler complaining about XML comments not being on
' valid language elements.
Shared Sub New()
[Default] = New Settings(False)
End Sub
''' <summary>
''' Whether fields whose values are the default for the field type (e.g. 0 for integers)
''' should be formatted (true) or omitted (false).
''' </summary>
Public ReadOnly Property FormatDefaultValues As Boolean
''' <summary>
''' The type registry used to format <see cref="Any"/> messages.
''' </summary>
Public ReadOnly Property TypeRegistry As TypeRegistry
' TODO: Work out how we're going to scale this to multiple settings. "WithXyz" methods?
''' <summary>
''' Creates a new <see cref="Settings"/> object with the specified formatting of default values
''' and an empty type registry.
''' </summary>
''' <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc) should be formatted; <c>false</c> otherwise.</param>
Public Sub New(formatDefaultValues As Boolean)
Me.New(formatDefaultValues, TypeRegistry.Empty)
End Sub
''' <summary>
''' Creates a new <see cref="Settings"/> object with the specified formatting of default values
''' and type registry.
''' </summary>
''' <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc) should be formatted; <c>false</c> otherwise.</param>
''' <param name="typeRegistry">The <see cref="TypeRegistry"/> to use when formatting <see cref="Any"/> messages.</param>
Public Sub New(formatDefaultValues As Boolean, typeRegistry As TypeRegistry)
Me.FormatDefaultValues = formatDefaultValues
Me.TypeRegistry = CheckNotNull(typeRegistry, NameOf(typeRegistry))
End Sub
End Class
' Effectively a cache of mapping from enum values to the original name as specified in the proto file,
' fetched by reflection.
' The need for this is unfortunate, as is its unbounded size, but realistically it shouldn't cause issues.
Private NotInheritable Class OriginalEnumValueHelper
' TODO: In the future we might want to use ConcurrentDictionary, at the point where all
' the platforms we target have it.
Private Shared ReadOnly dictionaries As Dictionary(Of System.Type, Dictionary(Of Object, String)) = New Dictionary(Of System.Type, Dictionary(Of Object, String))()
Friend Shared Function GetOriginalName(value As Object) As String
Dim enumType = value.GetType()
Dim nameMapping As Dictionary(Of Object, String)
SyncLock dictionaries
If Not dictionaries.TryGetValue(enumType, nameMapping) Then
nameMapping = GetNameMapping(enumType)
dictionaries(enumType) = nameMapping
End If
End SyncLock
Dim originalName As String
' If this returns false, originalName will be null, which is what we want.
nameMapping.TryGetValue(value, originalName)
Return originalName
End Function
#If DOTNET35
// TODO: Consider adding functionality to TypeExtensions to avoid this difference.
private static Dictionary<object, string> GetNameMapping(System.Type enumType) =>
enumType.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static)
.ToDictionary(f => f.GetValue(null),
f => (f.GetCustomAttributes(typeof(OriginalNameAttribute), false)
.FirstOrDefault() as OriginalNameAttribute)
// If the attribute hasn't been applied, fall back to the name of the field.
?.Name ?? f.Name);
#Else
Private Shared Function GetNameMapping(enumType As System.Type) As Dictionary(Of Object, String)
' If the attribute hasn't been applied, fall back to the name of the field.
Return enumType.GetTypeInfo().DeclaredFields.Where(Function(f) f.IsStatic).ToDictionary(Function(f) f.GetValue(Nothing), Function(f) If(f.GetCustomAttributes(Of OriginalNameAttribute)().FirstOrDefault()?.Name, f.Name))
End Function
#End If
End Class
End Class
End Namespace