blob: 73a4f64bf9ba3826b6a99f9a89b9289d3020a9ee [file] [log] [blame]
Jon Skeetf8c151f2015-07-03 11:56:29 +01001#region Copyright notice and license
2// Protocol Buffers - Google's data interchange format
3// Copyright 2015 Google Inc. All rights reserved.
4// https://developers.google.com/protocol-buffers/
5//
6// Redistribution and use in source and binary forms, with or without
7// modification, are permitted provided that the following conditions are
8// met:
9//
10// * Redistributions of source code must retain the above copyright
11// notice, this list of conditions and the following disclaimer.
12// * Redistributions in binary form must reproduce the above
13// copyright notice, this list of conditions and the following disclaimer
14// in the documentation and/or other materials provided with the
15// distribution.
16// * Neither the name of Google Inc. nor the names of its
17// contributors may be used to endorse or promote products derived from
18// this software without specific prior written permission.
19//
20// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31#endregion
32
33using System;
34using System.Collections;
35using System.Globalization;
36using System.Text;
Jon Skeet9f37de92015-07-14 10:24:52 +010037using Google.Protobuf.Reflection;
Jon Skeetc9fd53a2015-07-20 11:48:24 +010038using Google.Protobuf.WellKnownTypes;
avgwebad2d7752016-03-06 17:50:02 -080039using System.IO;
Jon Skeet0e30de32015-08-03 11:43:07 +010040using System.Linq;
Jon Skeetdd43dcc2016-01-20 18:43:00 +000041using System.Collections.Generic;
Jon Skeet790f4c82016-04-08 13:22:42 +010042using System.Reflection;
Jon Skeetf8c151f2015-07-03 11:56:29 +010043
44namespace Google.Protobuf
45{
46 /// <summary>
47 /// Reflection-based converter from messages to JSON.
48 /// </summary>
49 /// <remarks>
50 /// <para>
51 /// Instances of this class are thread-safe, with no mutable state.
52 /// </para>
53 /// <para>
54 /// This is a simple start to get JSON formatting working. As it's reflection-based,
55 /// it's not as quick as baking calls into generated messages - but is a simpler implementation.
56 /// (This code is generally not heavily optimized.)
57 /// </para>
58 /// </remarks>
59 public sealed class JsonFormatter
60 {
Jon Skeet567579b2015-11-23 12:43:54 +000061 internal const string AnyTypeUrlField = "@type";
Jon Skeetaabc6c42015-12-15 09:23:38 +000062 internal const string AnyDiagnosticValueField = "@value";
Jon Skeet567579b2015-11-23 12:43:54 +000063 internal const string AnyWellKnownTypeValueField = "value";
64 private const string TypeUrlPrefix = "type.googleapis.com";
65 private const string NameValueSeparator = ": ";
66 private const string PropertySeparator = ", ";
67
Jon Skeetf8c151f2015-07-03 11:56:29 +010068 /// <summary>
69 /// Returns a formatter using the default settings.
70 /// </summary>
Jon Skeetaabc6c42015-12-15 09:23:38 +000071 public static JsonFormatter Default { get; } = new JsonFormatter(Settings.Default);
72
73 // A JSON formatter which *only* exists
74 private static readonly JsonFormatter diagnosticFormatter = new JsonFormatter(Settings.Default);
Jon Skeetf8c151f2015-07-03 11:56:29 +010075
76 /// <summary>
77 /// The JSON representation of the first 160 characters of Unicode.
78 /// Empty strings are replaced by the static constructor.
79 /// </summary>
80 private static readonly string[] CommonRepresentations = {
81 // C0 (ASCII and derivatives) control characters
82 "\\u0000", "\\u0001", "\\u0002", "\\u0003", // 0x00
83 "\\u0004", "\\u0005", "\\u0006", "\\u0007",
84 "\\b", "\\t", "\\n", "\\u000b",
85 "\\f", "\\r", "\\u000e", "\\u000f",
86 "\\u0010", "\\u0011", "\\u0012", "\\u0013", // 0x10
87 "\\u0014", "\\u0015", "\\u0016", "\\u0017",
88 "\\u0018", "\\u0019", "\\u001a", "\\u001b",
89 "\\u001c", "\\u001d", "\\u001e", "\\u001f",
90 // Escaping of " and \ are required by www.json.org string definition.
91 // Escaping of < and > are required for HTML security.
92 "", "", "\\\"", "", "", "", "", "", // 0x20
93 "", "", "", "", "", "", "", "",
94 "", "", "", "", "", "", "", "", // 0x30
95 "", "", "", "", "\\u003c", "", "\\u003e", "",
96 "", "", "", "", "", "", "", "", // 0x40
97 "", "", "", "", "", "", "", "",
98 "", "", "", "", "", "", "", "", // 0x50
99 "", "", "", "", "\\\\", "", "", "",
100 "", "", "", "", "", "", "", "", // 0x60
101 "", "", "", "", "", "", "", "",
102 "", "", "", "", "", "", "", "", // 0x70
103 "", "", "", "", "", "", "", "\\u007f",
104 // C1 (ISO 8859 and Unicode) extended control characters
105 "\\u0080", "\\u0081", "\\u0082", "\\u0083", // 0x80
106 "\\u0084", "\\u0085", "\\u0086", "\\u0087",
107 "\\u0088", "\\u0089", "\\u008a", "\\u008b",
108 "\\u008c", "\\u008d", "\\u008e", "\\u008f",
109 "\\u0090", "\\u0091", "\\u0092", "\\u0093", // 0x90
110 "\\u0094", "\\u0095", "\\u0096", "\\u0097",
111 "\\u0098", "\\u0099", "\\u009a", "\\u009b",
112 "\\u009c", "\\u009d", "\\u009e", "\\u009f"
113 };
114
115 static JsonFormatter()
116 {
117 for (int i = 0; i < CommonRepresentations.Length; i++)
118 {
119 if (CommonRepresentations[i] == "")
120 {
121 CommonRepresentations[i] = ((char) i).ToString();
122 }
123 }
124 }
125
126 private readonly Settings settings;
127
Jon Skeetdd43dcc2016-01-20 18:43:00 +0000128 private bool DiagnosticOnly => ReferenceEquals(this, diagnosticFormatter);
129
Jon Skeet811fc892015-08-04 15:58:39 +0100130 /// <summary>
131 /// Creates a new formatted with the given settings.
132 /// </summary>
133 /// <param name="settings">The settings.</param>
Jon Skeetf8c151f2015-07-03 11:56:29 +0100134 public JsonFormatter(Settings settings)
135 {
136 this.settings = settings;
137 }
138
Jon Skeet811fc892015-08-04 15:58:39 +0100139 /// <summary>
140 /// Formats the specified message as JSON.
141 /// </summary>
142 /// <param name="message">The message to format.</param>
143 /// <returns>The formatted message.</returns>
Jon Skeet53c399a2015-07-20 19:24:31 +0100144 public string Format(IMessage message)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100145 {
avgwebad2d7752016-03-06 17:50:02 -0800146 var writer = new StringWriter();
147 Format(message, writer);
148 return writer.ToString();
149 }
150
151 /// <summary>
152 /// Formats the specified message as JSON.
153 /// </summary>
154 /// <param name="message">The message to format.</param>
155 /// <param name="writer">The TextWriter to write the formatted message to.</param>
156 /// <returns>The formatted message.</returns>
157 public void Format(IMessage message, TextWriter writer)
158 {
Jon Skeet7762f162016-02-04 06:51:54 +0000159 ProtoPreconditions.CheckNotNull(message, nameof(message));
avgwebad2d7752016-03-06 17:50:02 -0800160 ProtoPreconditions.CheckNotNull(writer, nameof(writer));
161
Jon Skeet16e272e2015-07-31 13:22:15 +0100162 if (message.Descriptor.IsWellKnownType)
163 {
avgwebad2d7752016-03-06 17:50:02 -0800164 WriteWellKnownTypeValue(writer, message.Descriptor, message);
Jon Skeet16e272e2015-07-31 13:22:15 +0100165 }
166 else
167 {
avgwebad2d7752016-03-06 17:50:02 -0800168 WriteMessage(writer, message);
Jon Skeet16e272e2015-07-31 13:22:15 +0100169 }
Jon Skeetf8c151f2015-07-03 11:56:29 +0100170 }
171
Jon Skeetaabc6c42015-12-15 09:23:38 +0000172 /// <summary>
173 /// Converts a message to JSON for diagnostic purposes with no extra context.
174 /// </summary>
175 /// <remarks>
176 /// <para>
177 /// This differs from calling <see cref="Format(IMessage)"/> on the default JSON
178 /// formatter in its handling of <see cref="Any"/>. As no type registry is available
179 /// in <see cref="object.ToString"/> calls, the normal way of resolving the type of
180 /// an <c>Any</c> message cannot be applied. Instead, a JSON property named <c>@value</c>
181 /// is included with the base64 data from the <see cref="Any.Value"/> property of the message.
182 /// </para>
183 /// <para>The value returned by this method is only designed to be used for diagnostic
184 /// purposes. It may not be parsable by <see cref="JsonParser"/>, and may not be parsable
185 /// by other Protocol Buffer implementations.</para>
186 /// </remarks>
187 /// <param name="message">The message to format for diagnostic purposes.</param>
188 /// <returns>The diagnostic-only JSON representation of the message</returns>
189 public static string ToDiagnosticString(IMessage message)
190 {
Jon Skeet7762f162016-02-04 06:51:54 +0000191 ProtoPreconditions.CheckNotNull(message, nameof(message));
Jon Skeetaabc6c42015-12-15 09:23:38 +0000192 return diagnosticFormatter.Format(message);
193 }
194
avgwebad2d7752016-03-06 17:50:02 -0800195 private void WriteMessage(TextWriter writer, IMessage message)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100196 {
197 if (message == null)
198 {
avgwebad2d7752016-03-06 17:50:02 -0800199 WriteNull(writer);
Jon Skeetf8c151f2015-07-03 11:56:29 +0100200 return;
201 }
Jon Skeetdd43dcc2016-01-20 18:43:00 +0000202 if (DiagnosticOnly)
Jon Skeet5dba7d72016-01-06 11:12:56 +0000203 {
204 ICustomDiagnosticMessage customDiagnosticMessage = message as ICustomDiagnosticMessage;
205 if (customDiagnosticMessage != null)
206 {
avgwebad2d7752016-03-06 17:50:02 -0800207 writer.Write(customDiagnosticMessage.ToDiagnosticString());
Jon Skeet5dba7d72016-01-06 11:12:56 +0000208 return;
209 }
210 }
avgwebad2d7752016-03-06 17:50:02 -0800211 writer.Write("{ ");
212 bool writtenFields = WriteMessageFields(writer, message, false);
213 writer.Write(writtenFields ? " }" : "}");
Jon Skeet567579b2015-11-23 12:43:54 +0000214 }
215
avgwebad2d7752016-03-06 17:50:02 -0800216 private bool WriteMessageFields(TextWriter writer, IMessage message, bool assumeFirstFieldWritten)
Jon Skeet567579b2015-11-23 12:43:54 +0000217 {
Jon Skeet53c399a2015-07-20 19:24:31 +0100218 var fields = message.Descriptor.Fields;
Jon Skeet567579b2015-11-23 12:43:54 +0000219 bool first = !assumeFirstFieldWritten;
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100220 // First non-oneof fields
Jon Skeetc1c6b2d2015-07-22 19:57:29 +0100221 foreach (var field in fields.InFieldNumberOrder())
Jon Skeetf8c151f2015-07-03 11:56:29 +0100222 {
Jon Skeet53c399a2015-07-20 19:24:31 +0100223 var accessor = field.Accessor;
Jon Skeet4fed0b52015-07-31 10:33:31 +0100224 if (field.ContainingOneof != null && field.ContainingOneof.Accessor.GetCaseFieldDescriptor(message) != field)
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100225 {
226 continue;
227 }
Jon Skeet4fed0b52015-07-31 10:33:31 +0100228 // Omit default values unless we're asked to format them, or they're oneofs (where the default
229 // value is still formatted regardless, because that's how we preserve the oneof case).
Jon Skeetf8c151f2015-07-03 11:56:29 +0100230 object value = accessor.GetValue(message);
Jon Skeet4fed0b52015-07-31 10:33:31 +0100231 if (field.ContainingOneof == null && !settings.FormatDefaultValues && IsDefaultValue(accessor, value))
Jon Skeetf8c151f2015-07-03 11:56:29 +0100232 {
233 continue;
234 }
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100235
236 // Okay, all tests complete: let's write the field value...
Jon Skeetf8c151f2015-07-03 11:56:29 +0100237 if (!first)
238 {
avgwebad2d7752016-03-06 17:50:02 -0800239 writer.Write(PropertySeparator);
Jon Skeetf8c151f2015-07-03 11:56:29 +0100240 }
alien0d32ab32016-03-17 15:19:04 +0300241
alien6f8dd212016-03-29 20:56:32 +0300242 WriteString(writer, accessor.Descriptor.JsonName);
avgwebad2d7752016-03-06 17:50:02 -0800243 writer.Write(NameValueSeparator);
244 WriteValue(writer, value);
alien0d32ab32016-03-17 15:19:04 +0300245
Jon Skeetf8c151f2015-07-03 11:56:29 +0100246 first = false;
alien0d32ab32016-03-17 15:19:04 +0300247 }
Jon Skeet567579b2015-11-23 12:43:54 +0000248 return !first;
Jon Skeetf8c151f2015-07-03 11:56:29 +0100249 }
250
Jon Skeetf437b672016-01-15 12:02:07 +0000251 /// <summary>
252 /// Camel-case converter with added strictness for field mask formatting.
253 /// </summary>
254 /// <exception cref="InvalidOperationException">The field mask is invalid for JSON representation</exception>
255 private static string ToCamelCaseForFieldMask(string input)
256 {
257 for (int i = 0; i < input.Length; i++)
258 {
259 char c = input[i];
260 if (c >= 'A' && c <= 'Z')
261 {
262 throw new InvalidOperationException($"Invalid field mask to be converted to JSON: {input}");
263 }
264 if (c == '_' && i < input.Length - 1)
265 {
266 char next = input[i + 1];
267 if (next < 'a' || next > 'z')
268 {
269 throw new InvalidOperationException($"Invalid field mask to be converted to JSON: {input}");
270 }
271 }
272 }
273 return ToCamelCase(input);
274 }
275
Jon Skeetf8c151f2015-07-03 11:56:29 +0100276 // Converted from src/google/protobuf/util/internal/utility.cc ToCamelCase
Jon Skeetfb248822015-09-04 12:41:14 +0100277 // TODO: Use the new field in FieldDescriptor.
Jon Skeetf8c151f2015-07-03 11:56:29 +0100278 internal static string ToCamelCase(string input)
279 {
280 bool capitalizeNext = false;
281 bool wasCap = true;
282 bool isCap = false;
283 bool firstWord = true;
284 StringBuilder result = new StringBuilder(input.Length);
285
286 for (int i = 0; i < input.Length; i++, wasCap = isCap)
287 {
288 isCap = char.IsUpper(input[i]);
289 if (input[i] == '_')
290 {
291 capitalizeNext = true;
292 if (result.Length != 0)
293 {
294 firstWord = false;
295 }
296 continue;
297 }
298 else if (firstWord)
299 {
300 // Consider when the current character B is capitalized,
301 // first word ends when:
302 // 1) following a lowercase: "...aB..."
303 // 2) followed by a lowercase: "...ABc..."
304 if (result.Length != 0 && isCap &&
305 (!wasCap || (i + 1 < input.Length && char.IsLower(input[i + 1]))))
306 {
307 firstWord = false;
308 }
309 else
310 {
311 result.Append(char.ToLowerInvariant(input[i]));
312 continue;
313 }
314 }
315 else if (capitalizeNext)
316 {
317 capitalizeNext = false;
318 if (char.IsLower(input[i]))
319 {
320 result.Append(char.ToUpperInvariant(input[i]));
321 continue;
322 }
323 }
324 result.Append(input[i]);
325 }
326 return result.ToString();
327 }
328
avgwebad2d7752016-03-06 17:50:02 -0800329 private static void WriteNull(TextWriter writer)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100330 {
avgwebad2d7752016-03-06 17:50:02 -0800331 writer.Write("null");
Jon Skeetf8c151f2015-07-03 11:56:29 +0100332 }
333
334 private static bool IsDefaultValue(IFieldAccessor accessor, object value)
335 {
336 if (accessor.Descriptor.IsMap)
337 {
338 IDictionary dictionary = (IDictionary) value;
339 return dictionary.Count == 0;
340 }
341 if (accessor.Descriptor.IsRepeated)
342 {
343 IList list = (IList) value;
344 return list.Count == 0;
345 }
346 switch (accessor.Descriptor.FieldType)
347 {
348 case FieldType.Bool:
349 return (bool) value == false;
350 case FieldType.Bytes:
351 return (ByteString) value == ByteString.Empty;
352 case FieldType.String:
353 return (string) value == "";
354 case FieldType.Double:
355 return (double) value == 0.0;
356 case FieldType.SInt32:
357 case FieldType.Int32:
358 case FieldType.SFixed32:
359 case FieldType.Enum:
360 return (int) value == 0;
361 case FieldType.Fixed32:
362 case FieldType.UInt32:
363 return (uint) value == 0;
364 case FieldType.Fixed64:
365 case FieldType.UInt64:
366 return (ulong) value == 0;
367 case FieldType.SFixed64:
368 case FieldType.Int64:
369 case FieldType.SInt64:
370 return (long) value == 0;
371 case FieldType.Float:
372 return (float) value == 0f;
373 case FieldType.Message:
374 case FieldType.Group: // Never expect to get this, but...
375 return value == null;
376 default:
377 throw new ArgumentException("Invalid field type");
378 }
379 }
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100380
avgwebad2d7752016-03-06 17:50:02 -0800381 private void WriteValue(TextWriter writer, object value)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100382 {
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100383 if (value == null)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100384 {
avgwebad2d7752016-03-06 17:50:02 -0800385 WriteNull(writer);
Jon Skeetf8c151f2015-07-03 11:56:29 +0100386 }
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100387 else if (value is bool)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100388 {
avgwebad2d7752016-03-06 17:50:02 -0800389 writer.Write((bool)value ? "true" : "false");
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100390 }
391 else if (value is ByteString)
392 {
393 // Nothing in Base64 needs escaping
avgwebad2d7752016-03-06 17:50:02 -0800394 writer.Write('"');
395 writer.Write(((ByteString)value).ToBase64());
396 writer.Write('"');
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100397 }
398 else if (value is string)
399 {
avgwebad2d7752016-03-06 17:50:02 -0800400 WriteString(writer, (string)value);
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100401 }
402 else if (value is IDictionary)
403 {
avgwebad2d7752016-03-06 17:50:02 -0800404 WriteDictionary(writer, (IDictionary)value);
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100405 }
406 else if (value is IList)
407 {
avgwebad2d7752016-03-06 17:50:02 -0800408 WriteList(writer, (IList)value);
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100409 }
410 else if (value is int || value is uint)
411 {
412 IFormattable formattable = (IFormattable) value;
avgwebad2d7752016-03-06 17:50:02 -0800413 writer.Write(formattable.ToString("d", CultureInfo.InvariantCulture));
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100414 }
415 else if (value is long || value is ulong)
416 {
avgwebad2d7752016-03-06 17:50:02 -0800417 writer.Write('"');
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100418 IFormattable formattable = (IFormattable) value;
avgwebad2d7752016-03-06 17:50:02 -0800419 writer.Write(formattable.ToString("d", CultureInfo.InvariantCulture));
420 writer.Write('"');
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100421 }
422 else if (value is System.Enum)
423 {
Jon Skeet790f4c82016-04-08 13:22:42 +0100424 string name = OriginalEnumValueHelper.GetOriginalName(value);
425 if (name != null)
Jon Skeet52db5132016-01-15 13:45:53 +0000426 {
Jon Skeet790f4c82016-04-08 13:22:42 +0100427 WriteString(writer, name);
Jon Skeet52db5132016-01-15 13:45:53 +0000428 }
429 else
430 {
avgwebad2d7752016-03-06 17:50:02 -0800431 WriteValue(writer, (int)value);
Jon Skeet52db5132016-01-15 13:45:53 +0000432 }
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100433 }
434 else if (value is float || value is double)
435 {
436 string text = ((IFormattable) value).ToString("r", CultureInfo.InvariantCulture);
437 if (text == "NaN" || text == "Infinity" || text == "-Infinity")
438 {
avgwebad2d7752016-03-06 17:50:02 -0800439 writer.Write('"');
440 writer.Write(text);
441 writer.Write('"');
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100442 }
443 else
444 {
avgwebad2d7752016-03-06 17:50:02 -0800445 writer.Write(text);
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100446 }
447 }
448 else if (value is IMessage)
449 {
450 IMessage message = (IMessage) value;
451 if (message.Descriptor.IsWellKnownType)
452 {
avgwebad2d7752016-03-06 17:50:02 -0800453 WriteWellKnownTypeValue(writer, message.Descriptor, value);
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100454 }
455 else
456 {
avgwebad2d7752016-03-06 17:50:02 -0800457 WriteMessage(writer, (IMessage)value);
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100458 }
Jon Skeetf8c151f2015-07-03 11:56:29 +0100459 }
460 else
461 {
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100462 throw new ArgumentException("Unable to format value of type " + value.GetType());
Jon Skeetf8c151f2015-07-03 11:56:29 +0100463 }
464 }
465
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100466 /// <summary>
467 /// Central interception point for well-known type formatting. Any well-known types which
Jon Skeet16e272e2015-07-31 13:22:15 +0100468 /// don't need special handling can fall back to WriteMessage. We avoid assuming that the
469 /// values are using the embedded well-known types, in order to allow for dynamic messages
470 /// in the future.
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100471 /// </summary>
avgwebad2d7752016-03-06 17:50:02 -0800472 private void WriteWellKnownTypeValue(TextWriter writer, MessageDescriptor descriptor, object value)
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100473 {
Jon Skeet567579b2015-11-23 12:43:54 +0000474 // Currently, we can never actually get here, because null values are always handled by the caller. But if we *could*,
475 // this would do the right thing.
Jon Skeete7caf152015-07-31 15:07:50 +0100476 if (value == null)
477 {
avgwebad2d7752016-03-06 17:50:02 -0800478 WriteNull(writer);
Jon Skeete7caf152015-07-31 15:07:50 +0100479 return;
480 }
Jon Skeetfb248822015-09-04 12:41:14 +0100481 // For wrapper types, the value will either be the (possibly boxed) "native" value,
482 // or the message itself if we're formatting it at the top level (e.g. just calling ToString on the object itself).
483 // If it's the message form, we can extract the value first, which *will* be the (possibly boxed) native value,
484 // and then proceed, writing it as if we were definitely in a field. (We never need to wrap it in an extra string...
485 // WriteValue will do the right thing.)
Jon Skeet72ec3362015-11-19 17:13:38 +0000486 if (descriptor.IsWrapperType)
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100487 {
Jon Skeetfb248822015-09-04 12:41:14 +0100488 if (value is IMessage)
489 {
490 var message = (IMessage) value;
Jon Skeet284bb452015-11-05 09:13:53 +0000491 value = message.Descriptor.Fields[WrappersReflection.WrapperValueFieldNumber].Accessor.GetValue(message);
Jon Skeetfb248822015-09-04 12:41:14 +0100492 }
avgwebad2d7752016-03-06 17:50:02 -0800493 WriteValue(writer, value);
Jon Skeet16e272e2015-07-31 13:22:15 +0100494 return;
495 }
Jon Skeete7caf152015-07-31 15:07:50 +0100496 if (descriptor.FullName == Timestamp.Descriptor.FullName)
Jon Skeet16e272e2015-07-31 13:22:15 +0100497 {
avgwebad2d7752016-03-06 17:50:02 -0800498 WriteTimestamp(writer, (IMessage)value);
Jon Skeet16e272e2015-07-31 13:22:15 +0100499 return;
500 }
Jon Skeete7caf152015-07-31 15:07:50 +0100501 if (descriptor.FullName == Duration.Descriptor.FullName)
Jon Skeet16e272e2015-07-31 13:22:15 +0100502 {
avgwebad2d7752016-03-06 17:50:02 -0800503 WriteDuration(writer, (IMessage)value);
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100504 return;
505 }
Jon Skeet0e30de32015-08-03 11:43:07 +0100506 if (descriptor.FullName == FieldMask.Descriptor.FullName)
507 {
avgwebad2d7752016-03-06 17:50:02 -0800508 WriteFieldMask(writer, (IMessage)value);
Jon Skeet0e30de32015-08-03 11:43:07 +0100509 return;
510 }
Jon Skeete7caf152015-07-31 15:07:50 +0100511 if (descriptor.FullName == Struct.Descriptor.FullName)
512 {
avgwebad2d7752016-03-06 17:50:02 -0800513 WriteStruct(writer, (IMessage)value);
Jon Skeete7caf152015-07-31 15:07:50 +0100514 return;
515 }
516 if (descriptor.FullName == ListValue.Descriptor.FullName)
517 {
518 var fieldAccessor = descriptor.Fields[ListValue.ValuesFieldNumber].Accessor;
avgwebad2d7752016-03-06 17:50:02 -0800519 WriteList(writer, (IList)fieldAccessor.GetValue((IMessage)value));
Jon Skeete7caf152015-07-31 15:07:50 +0100520 return;
521 }
522 if (descriptor.FullName == Value.Descriptor.FullName)
523 {
avgwebad2d7752016-03-06 17:50:02 -0800524 WriteStructFieldValue(writer, (IMessage)value);
Jon Skeete7caf152015-07-31 15:07:50 +0100525 return;
526 }
Jon Skeet567579b2015-11-23 12:43:54 +0000527 if (descriptor.FullName == Any.Descriptor.FullName)
528 {
avgwebad2d7752016-03-06 17:50:02 -0800529 WriteAny(writer, (IMessage)value);
Jon Skeet567579b2015-11-23 12:43:54 +0000530 return;
531 }
avgwebad2d7752016-03-06 17:50:02 -0800532 WriteMessage(writer, (IMessage)value);
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100533 }
534
avgwebad2d7752016-03-06 17:50:02 -0800535 private void WriteTimestamp(TextWriter writer, IMessage value)
Jon Skeet16e272e2015-07-31 13:22:15 +0100536 {
537 // TODO: In the common case where this *is* using the built-in Timestamp type, we could
538 // avoid all the reflection at this point, by casting to Timestamp. In the interests of
539 // avoiding subtle bugs, don't do that until we've implemented DynamicMessage so that we can prove
540 // it still works in that case.
541 int nanos = (int) value.Descriptor.Fields[Timestamp.NanosFieldNumber].Accessor.GetValue(value);
542 long seconds = (long) value.Descriptor.Fields[Timestamp.SecondsFieldNumber].Accessor.GetValue(value);
avgwebad2d7752016-03-06 17:50:02 -0800543 writer.Write(Timestamp.ToJson(seconds, nanos, DiagnosticOnly));
Jon Skeet16e272e2015-07-31 13:22:15 +0100544 }
545
avgwebad2d7752016-03-06 17:50:02 -0800546 private void WriteDuration(TextWriter writer, IMessage value)
Jon Skeet16e272e2015-07-31 13:22:15 +0100547 {
Jon Skeet801b1692015-08-03 08:45:48 +0100548 // TODO: Same as for WriteTimestamp
Jon Skeet16e272e2015-07-31 13:22:15 +0100549 int nanos = (int) value.Descriptor.Fields[Duration.NanosFieldNumber].Accessor.GetValue(value);
550 long seconds = (long) value.Descriptor.Fields[Duration.SecondsFieldNumber].Accessor.GetValue(value);
avgwebad2d7752016-03-06 17:50:02 -0800551 writer.Write(Duration.ToJson(seconds, nanos, DiagnosticOnly));
Jon Skeet801b1692015-08-03 08:45:48 +0100552 }
553
avgwebad2d7752016-03-06 17:50:02 -0800554 private void WriteFieldMask(TextWriter writer, IMessage value)
Jon Skeet0e30de32015-08-03 11:43:07 +0100555 {
Jon Skeetdd43dcc2016-01-20 18:43:00 +0000556 var paths = (IList<string>) value.Descriptor.Fields[FieldMask.PathsFieldNumber].Accessor.GetValue(value);
avgwebad2d7752016-03-06 17:50:02 -0800557 writer.Write(FieldMask.ToJson(paths, DiagnosticOnly));
Jon Skeet0e30de32015-08-03 11:43:07 +0100558 }
559
avgwebad2d7752016-03-06 17:50:02 -0800560 private void WriteAny(TextWriter writer, IMessage value)
Jon Skeet567579b2015-11-23 12:43:54 +0000561 {
Jon Skeetdd43dcc2016-01-20 18:43:00 +0000562 if (DiagnosticOnly)
Jon Skeetaabc6c42015-12-15 09:23:38 +0000563 {
avgwebad2d7752016-03-06 17:50:02 -0800564 WriteDiagnosticOnlyAny(writer, value);
Jon Skeetaabc6c42015-12-15 09:23:38 +0000565 return;
566 }
567
Jon Skeet567579b2015-11-23 12:43:54 +0000568 string typeUrl = (string) value.Descriptor.Fields[Any.TypeUrlFieldNumber].Accessor.GetValue(value);
569 ByteString data = (ByteString) value.Descriptor.Fields[Any.ValueFieldNumber].Accessor.GetValue(value);
570 string typeName = GetTypeName(typeUrl);
571 MessageDescriptor descriptor = settings.TypeRegistry.Find(typeName);
572 if (descriptor == null)
573 {
574 throw new InvalidOperationException($"Type registry has no descriptor for type name '{typeName}'");
575 }
576 IMessage message = descriptor.Parser.ParseFrom(data);
avgwebad2d7752016-03-06 17:50:02 -0800577 writer.Write("{ ");
578 WriteString(writer, AnyTypeUrlField);
579 writer.Write(NameValueSeparator);
580 WriteString(writer, typeUrl);
Jon Skeet567579b2015-11-23 12:43:54 +0000581
582 if (descriptor.IsWellKnownType)
583 {
avgwebad2d7752016-03-06 17:50:02 -0800584 writer.Write(PropertySeparator);
585 WriteString(writer, AnyWellKnownTypeValueField);
586 writer.Write(NameValueSeparator);
587 WriteWellKnownTypeValue(writer, descriptor, message);
Jon Skeet567579b2015-11-23 12:43:54 +0000588 }
589 else
590 {
avgwebad2d7752016-03-06 17:50:02 -0800591 WriteMessageFields(writer, message, true);
Jon Skeet567579b2015-11-23 12:43:54 +0000592 }
avgwebad2d7752016-03-06 17:50:02 -0800593 writer.Write(" }");
Jon Skeet567579b2015-11-23 12:43:54 +0000594 }
595
avgwebad2d7752016-03-06 17:50:02 -0800596 private void WriteDiagnosticOnlyAny(TextWriter writer, IMessage value)
Jon Skeetaabc6c42015-12-15 09:23:38 +0000597 {
598 string typeUrl = (string) value.Descriptor.Fields[Any.TypeUrlFieldNumber].Accessor.GetValue(value);
599 ByteString data = (ByteString) value.Descriptor.Fields[Any.ValueFieldNumber].Accessor.GetValue(value);
avgwebad2d7752016-03-06 17:50:02 -0800600 writer.Write("{ ");
601 WriteString(writer, AnyTypeUrlField);
602 writer.Write(NameValueSeparator);
603 WriteString(writer, typeUrl);
604 writer.Write(PropertySeparator);
605 WriteString(writer, AnyDiagnosticValueField);
606 writer.Write(NameValueSeparator);
607 writer.Write('"');
608 writer.Write(data.ToBase64());
609 writer.Write('"');
610 writer.Write(" }");
Jon Skeetaabc6c42015-12-15 09:23:38 +0000611 }
612
Jon Skeet567579b2015-11-23 12:43:54 +0000613 internal static string GetTypeName(String typeUrl)
614 {
615 string[] parts = typeUrl.Split('/');
616 if (parts.Length != 2 || parts[0] != TypeUrlPrefix)
617 {
618 throw new InvalidProtocolBufferException($"Invalid type url: {typeUrl}");
619 }
620 return parts[1];
621 }
622
avgwebad2d7752016-03-06 17:50:02 -0800623 private void WriteStruct(TextWriter writer, IMessage message)
Jon Skeete7caf152015-07-31 15:07:50 +0100624 {
avgwebad2d7752016-03-06 17:50:02 -0800625 writer.Write("{ ");
Jon Skeete7caf152015-07-31 15:07:50 +0100626 IDictionary fields = (IDictionary) message.Descriptor.Fields[Struct.FieldsFieldNumber].Accessor.GetValue(message);
627 bool first = true;
628 foreach (DictionaryEntry entry in fields)
629 {
630 string key = (string) entry.Key;
631 IMessage value = (IMessage) entry.Value;
632 if (string.IsNullOrEmpty(key) || value == null)
633 {
634 throw new InvalidOperationException("Struct fields cannot have an empty key or a null value.");
635 }
636
637 if (!first)
638 {
avgwebad2d7752016-03-06 17:50:02 -0800639 writer.Write(PropertySeparator);
Jon Skeete7caf152015-07-31 15:07:50 +0100640 }
avgwebad2d7752016-03-06 17:50:02 -0800641 WriteString(writer, key);
642 writer.Write(NameValueSeparator);
643 WriteStructFieldValue(writer, value);
Jon Skeete7caf152015-07-31 15:07:50 +0100644 first = false;
645 }
avgwebad2d7752016-03-06 17:50:02 -0800646 writer.Write(first ? "}" : " }");
Jon Skeete7caf152015-07-31 15:07:50 +0100647 }
648
avgwebad2d7752016-03-06 17:50:02 -0800649 private void WriteStructFieldValue(TextWriter writer, IMessage message)
Jon Skeete7caf152015-07-31 15:07:50 +0100650 {
651 var specifiedField = message.Descriptor.Oneofs[0].Accessor.GetCaseFieldDescriptor(message);
652 if (specifiedField == null)
653 {
654 throw new InvalidOperationException("Value message must contain a value for the oneof.");
655 }
656
657 object value = specifiedField.Accessor.GetValue(message);
658
659 switch (specifiedField.FieldNumber)
660 {
661 case Value.BoolValueFieldNumber:
662 case Value.StringValueFieldNumber:
663 case Value.NumberValueFieldNumber:
avgwebad2d7752016-03-06 17:50:02 -0800664 WriteValue(writer, value);
Jon Skeete7caf152015-07-31 15:07:50 +0100665 return;
666 case Value.StructValueFieldNumber:
667 case Value.ListValueFieldNumber:
668 // Structs and ListValues are nested messages, and already well-known types.
669 var nestedMessage = (IMessage) specifiedField.Accessor.GetValue(message);
avgwebad2d7752016-03-06 17:50:02 -0800670 WriteWellKnownTypeValue(writer, nestedMessage.Descriptor, nestedMessage);
Jon Skeete7caf152015-07-31 15:07:50 +0100671 return;
672 case Value.NullValueFieldNumber:
avgwebad2d7752016-03-06 17:50:02 -0800673 WriteNull(writer);
Jon Skeete7caf152015-07-31 15:07:50 +0100674 return;
675 default:
676 throw new InvalidOperationException("Unexpected case in struct field: " + specifiedField.FieldNumber);
677 }
678 }
679
avgwebad2d7752016-03-06 17:50:02 -0800680 internal void WriteList(TextWriter writer, IList list)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100681 {
avgwebad2d7752016-03-06 17:50:02 -0800682 writer.Write("[ ");
Jon Skeetf8c151f2015-07-03 11:56:29 +0100683 bool first = true;
684 foreach (var value in list)
685 {
686 if (!first)
687 {
avgwebad2d7752016-03-06 17:50:02 -0800688 writer.Write(PropertySeparator);
Jon Skeetf8c151f2015-07-03 11:56:29 +0100689 }
avgwebad2d7752016-03-06 17:50:02 -0800690 WriteValue(writer, value);
Jon Skeetf8c151f2015-07-03 11:56:29 +0100691 first = false;
692 }
avgwebad2d7752016-03-06 17:50:02 -0800693 writer.Write(first ? "]" : " ]");
Jon Skeetf8c151f2015-07-03 11:56:29 +0100694 }
695
avgwebad2d7752016-03-06 17:50:02 -0800696 internal void WriteDictionary(TextWriter writer, IDictionary dictionary)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100697 {
avgwebad2d7752016-03-06 17:50:02 -0800698 writer.Write("{ ");
Jon Skeetf8c151f2015-07-03 11:56:29 +0100699 bool first = true;
Jon Skeetf8c151f2015-07-03 11:56:29 +0100700 // This will box each pair. Could use IDictionaryEnumerator, but that's ugly in terms of disposal.
701 foreach (DictionaryEntry pair in dictionary)
702 {
703 if (!first)
704 {
avgwebad2d7752016-03-06 17:50:02 -0800705 writer.Write(PropertySeparator);
Jon Skeetf8c151f2015-07-03 11:56:29 +0100706 }
707 string keyText;
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100708 if (pair.Key is string)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100709 {
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100710 keyText = (string) pair.Key;
711 }
712 else if (pair.Key is bool)
713 {
714 keyText = (bool) pair.Key ? "true" : "false";
715 }
716 else if (pair.Key is int || pair.Key is uint | pair.Key is long || pair.Key is ulong)
717 {
718 keyText = ((IFormattable) pair.Key).ToString("d", CultureInfo.InvariantCulture);
719 }
720 else
721 {
722 if (pair.Key == null)
723 {
724 throw new ArgumentException("Dictionary has entry with null key");
725 }
726 throw new ArgumentException("Unhandled dictionary key type: " + pair.Key.GetType());
Jon Skeetf8c151f2015-07-03 11:56:29 +0100727 }
avgwebad2d7752016-03-06 17:50:02 -0800728 WriteString(writer, keyText);
729 writer.Write(NameValueSeparator);
730 WriteValue(writer, pair.Value);
Jon Skeetf8c151f2015-07-03 11:56:29 +0100731 first = false;
732 }
avgwebad2d7752016-03-06 17:50:02 -0800733 writer.Write(first ? "}" : " }");
Jon Skeetf8c151f2015-07-03 11:56:29 +0100734 }
735
736 /// <summary>
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100737 /// Returns whether or not a singular value can be represented in JSON.
738 /// Currently only relevant for enums, where unknown values can't be represented.
739 /// For repeated/map fields, this always returns true.
740 /// </summary>
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100741 private bool CanWriteSingleValue(object value)
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100742 {
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100743 if (value is System.Enum)
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100744 {
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100745 return System.Enum.IsDefined(value.GetType(), value);
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100746 }
747 return true;
748 }
749
750 /// <summary>
Jon Skeetf8c151f2015-07-03 11:56:29 +0100751 /// Writes a string (including leading and trailing double quotes) to a builder, escaping as required.
752 /// </summary>
753 /// <remarks>
754 /// Other than surrogate pair handling, this code is mostly taken from src/google/protobuf/util/internal/json_escaping.cc.
755 /// </remarks>
avgwebad2d7752016-03-06 17:50:02 -0800756 internal static void WriteString(TextWriter writer, string text)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100757 {
avgwebad2d7752016-03-06 17:50:02 -0800758 writer.Write('"');
Jon Skeetf8c151f2015-07-03 11:56:29 +0100759 for (int i = 0; i < text.Length; i++)
760 {
761 char c = text[i];
762 if (c < 0xa0)
763 {
avgwebad2d7752016-03-06 17:50:02 -0800764 writer.Write(CommonRepresentations[c]);
Jon Skeetf8c151f2015-07-03 11:56:29 +0100765 continue;
766 }
767 if (char.IsHighSurrogate(c))
768 {
769 // Encountered first part of a surrogate pair.
770 // Check that we have the whole pair, and encode both parts as hex.
771 i++;
772 if (i == text.Length || !char.IsLowSurrogate(text[i]))
773 {
774 throw new ArgumentException("String contains low surrogate not followed by high surrogate");
775 }
avgwebad2d7752016-03-06 17:50:02 -0800776 HexEncodeUtf16CodeUnit(writer, c);
777 HexEncodeUtf16CodeUnit(writer, text[i]);
Jon Skeetf8c151f2015-07-03 11:56:29 +0100778 continue;
779 }
780 else if (char.IsLowSurrogate(c))
781 {
782 throw new ArgumentException("String contains high surrogate not preceded by low surrogate");
783 }
784 switch ((uint) c)
785 {
786 // These are not required by json spec
787 // but used to prevent security bugs in javascript.
788 case 0xfeff: // Zero width no-break space
789 case 0xfff9: // Interlinear annotation anchor
790 case 0xfffa: // Interlinear annotation separator
791 case 0xfffb: // Interlinear annotation terminator
792
793 case 0x00ad: // Soft-hyphen
794 case 0x06dd: // Arabic end of ayah
795 case 0x070f: // Syriac abbreviation mark
796 case 0x17b4: // Khmer vowel inherent Aq
797 case 0x17b5: // Khmer vowel inherent Aa
avgwebad2d7752016-03-06 17:50:02 -0800798 HexEncodeUtf16CodeUnit(writer, c);
Jon Skeetf8c151f2015-07-03 11:56:29 +0100799 break;
800
801 default:
802 if ((c >= 0x0600 && c <= 0x0603) || // Arabic signs
803 (c >= 0x200b && c <= 0x200f) || // Zero width etc.
804 (c >= 0x2028 && c <= 0x202e) || // Separators etc.
805 (c >= 0x2060 && c <= 0x2064) || // Invisible etc.
806 (c >= 0x206a && c <= 0x206f))
807 {
avgwebad2d7752016-03-06 17:50:02 -0800808 HexEncodeUtf16CodeUnit(writer, c);
Jon Skeetf8c151f2015-07-03 11:56:29 +0100809 }
810 else
811 {
812 // No handling of surrogates here - that's done earlier
avgwebad2d7752016-03-06 17:50:02 -0800813 writer.Write(c);
Jon Skeetf8c151f2015-07-03 11:56:29 +0100814 }
815 break;
816 }
817 }
avgwebad2d7752016-03-06 17:50:02 -0800818 writer.Write('"');
Jon Skeet801b1692015-08-03 08:45:48 +0100819 }
Jon Skeetf8c151f2015-07-03 11:56:29 +0100820
821 private const string Hex = "0123456789abcdef";
avgwebad2d7752016-03-06 17:50:02 -0800822 private static void HexEncodeUtf16CodeUnit(TextWriter writer, char c)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100823 {
avgwebad2d7752016-03-06 17:50:02 -0800824 writer.Write("\\u");
825 writer.Write(Hex[(c >> 12) & 0xf]);
826 writer.Write(Hex[(c >> 8) & 0xf]);
827 writer.Write(Hex[(c >> 4) & 0xf]);
828 writer.Write(Hex[(c >> 0) & 0xf]);
Jon Skeetf8c151f2015-07-03 11:56:29 +0100829 }
830
831 /// <summary>
832 /// Settings controlling JSON formatting.
833 /// </summary>
834 public sealed class Settings
835 {
Jon Skeetf8c151f2015-07-03 11:56:29 +0100836 /// <summary>
837 /// Default settings, as used by <see cref="JsonFormatter.Default"/>
838 /// </summary>
Jon Skeet3de2fce2015-11-23 16:21:47 +0000839 public static Settings Default { get; }
840
841 // Workaround for the Mono compiler complaining about XML comments not being on
842 // valid language elements.
843 static Settings()
844 {
845 Default = new Settings(false);
846 }
Jon Skeetf8c151f2015-07-03 11:56:29 +0100847
Jon Skeetf8c151f2015-07-03 11:56:29 +0100848 /// <summary>
849 /// Whether fields whose values are the default for the field type (e.g. 0 for integers)
850 /// should be formatted (true) or omitted (false).
851 /// </summary>
Jon Skeet567579b2015-11-23 12:43:54 +0000852 public bool FormatDefaultValues { get; }
Jon Skeetf8c151f2015-07-03 11:56:29 +0100853
Jon Skeet811fc892015-08-04 15:58:39 +0100854 /// <summary>
Jon Skeet567579b2015-11-23 12:43:54 +0000855 /// The type registry used to format <see cref="Any"/> messages.
856 /// </summary>
857 public TypeRegistry TypeRegistry { get; }
858
859 // TODO: Work out how we're going to scale this to multiple settings. "WithXyz" methods?
860
861 /// <summary>
862 /// Creates a new <see cref="Settings"/> object with the specified formatting of default values
863 /// and an empty type registry.
Jon Skeet811fc892015-08-04 15:58:39 +0100864 /// </summary>
865 /// <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc) should be formatted; <c>false</c> otherwise.</param>
Jon Skeet567579b2015-11-23 12:43:54 +0000866 public Settings(bool formatDefaultValues) : this(formatDefaultValues, TypeRegistry.Empty)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100867 {
Jon Skeet567579b2015-11-23 12:43:54 +0000868 }
869
870 /// <summary>
871 /// Creates a new <see cref="Settings"/> object with the specified formatting of default values
872 /// and type registry.
873 /// </summary>
874 /// <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc) should be formatted; <c>false</c> otherwise.</param>
875 /// <param name="typeRegistry">The <see cref="TypeRegistry"/> to use when formatting <see cref="Any"/> messages.</param>
876 public Settings(bool formatDefaultValues, TypeRegistry typeRegistry)
877 {
878 FormatDefaultValues = formatDefaultValues;
Jon Skeet7762f162016-02-04 06:51:54 +0000879 TypeRegistry = ProtoPreconditions.CheckNotNull(typeRegistry, nameof(typeRegistry));
Jon Skeetf8c151f2015-07-03 11:56:29 +0100880 }
881 }
Jon Skeet790f4c82016-04-08 13:22:42 +0100882
883 // Effectively a cache of mapping from enum values to the original name as specified in the proto file,
884 // fetched by reflection.
885 // The need for this is unfortunate, as is its unbounded size, but realistically it shouldn't cause issues.
886 private static class OriginalEnumValueHelper
887 {
888 // TODO: In the future we might want to use ConcurrentDictionary, at the point where all
889 // the platforms we target have it.
890 private static readonly Dictionary<System.Type, Dictionary<object, string>> dictionaries
891 = new Dictionary<System.Type, Dictionary<object, string>>();
892
893 internal static string GetOriginalName(object value)
894 {
895 var enumType = value.GetType();
896 Dictionary<object, string> nameMapping;
897 lock (dictionaries)
898 {
899 if (!dictionaries.TryGetValue(enumType, out nameMapping))
900 {
901 nameMapping = GetNameMapping(enumType);
902 dictionaries[enumType] = nameMapping;
903 }
904 }
905
906 string originalName;
907 // If this returns false, originalName will be null, which is what we want.
908 nameMapping.TryGetValue(value, out originalName);
909 return originalName;
910 }
911
912 private static Dictionary<object, string> GetNameMapping(System.Type enumType) =>
913 enumType.GetTypeInfo().DeclaredFields
914 .Where(f => f.IsStatic)
915 .ToDictionary(f => f.GetValue(null),
916 f => f.GetCustomAttributes<OriginalNameAttribute>()
917 .FirstOrDefault()
918 // If the attribute hasn't been applied, fall back to the name of the field.
919 ?.Name ?? f.Name);
920 }
Jon Skeetf8c151f2015-07-03 11:56:29 +0100921 }
922}