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.

275 lines
11 KiB

4 years ago
  1. #region License
  2. // Copyright (c) 2007 James Newton-King
  3. //
  4. // Permission is hereby granted, free of charge, to any person
  5. // obtaining a copy of this software and associated documentation
  6. // files (the "Software"), to deal in the Software without
  7. // restriction, including without limitation the rights to use,
  8. // copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. // copies of the Software, and to permit persons to whom the
  10. // Software is furnished to do so, subject to the following
  11. // conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be
  14. // included in all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  17. // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
  18. // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  19. // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  20. // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  21. // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  22. // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  23. // OTHER DEALINGS IN THE SOFTWARE.
  24. #endregion
  25. #if !NET20
  26. using Newtonsoft.Json.Linq;
  27. using System;
  28. using System.Collections;
  29. using System.Collections.Generic;
  30. #if NET20
  31. using Newtonsoft.Json.Utilities.LinqBridge;
  32. #else
  33. using System.Linq;
  34. #endif
  35. using System.Reflection;
  36. using Newtonsoft.Json.Serialization;
  37. using System.Globalization;
  38. using Newtonsoft.Json.Utilities;
  39. namespace Newtonsoft.Json.Converters
  40. {
  41. /// <summary>
  42. /// Converts a F# discriminated union type to and from JSON.
  43. /// </summary>
  44. internal class DiscriminatedUnionConverter : JsonConverter
  45. {
  46. #region UnionDefinition
  47. internal class Union
  48. {
  49. public List<UnionCase> Cases;
  50. public FSharpFunction TagReader { get; set; }
  51. }
  52. internal class UnionCase
  53. {
  54. public int Tag;
  55. public string Name;
  56. public PropertyInfo[] Fields;
  57. public FSharpFunction FieldReader;
  58. public FSharpFunction Constructor;
  59. }
  60. #endregion
  61. private const string CasePropertyName = "Case";
  62. private const string FieldsPropertyName = "Fields";
  63. private static readonly ThreadSafeStore<Type, Union> UnionCache = new ThreadSafeStore<Type, Union>(CreateUnion);
  64. private static readonly ThreadSafeStore<Type, Type> UnionTypeLookupCache = new ThreadSafeStore<Type, Type>(CreateUnionTypeLookup);
  65. private static Type CreateUnionTypeLookup(Type t)
  66. {
  67. // this lookup is because cases with fields are derived from union type
  68. // need to get declaring type to avoid duplicate Unions in cache
  69. // hacky but I can't find an API to get the declaring type without GetUnionCases
  70. object[] cases = (object[])FSharpUtils.GetUnionCases(null, t, null);
  71. object caseInfo = Enumerable.First(cases);
  72. Type unionType = (Type)FSharpUtils.GetUnionCaseInfoDeclaringType(caseInfo);
  73. return unionType;
  74. }
  75. private static Union CreateUnion(Type t)
  76. {
  77. Union u = new Union();
  78. u.TagReader = (FSharpFunction)FSharpUtils.PreComputeUnionTagReader(null, t, null);
  79. u.Cases = new List<UnionCase>();
  80. object[] cases = (object[])FSharpUtils.GetUnionCases(null, t, null);
  81. foreach (object unionCaseInfo in cases)
  82. {
  83. UnionCase unionCase = new UnionCase();
  84. unionCase.Tag = (int)FSharpUtils.GetUnionCaseInfoTag(unionCaseInfo);
  85. unionCase.Name = (string)FSharpUtils.GetUnionCaseInfoName(unionCaseInfo);
  86. unionCase.Fields = (PropertyInfo[])FSharpUtils.GetUnionCaseInfoFields(unionCaseInfo);
  87. unionCase.FieldReader = (FSharpFunction)FSharpUtils.PreComputeUnionReader(null, unionCaseInfo, null);
  88. unionCase.Constructor = (FSharpFunction)FSharpUtils.PreComputeUnionConstructor(null, unionCaseInfo, null);
  89. u.Cases.Add(unionCase);
  90. }
  91. return u;
  92. }
  93. /// <summary>
  94. /// Writes the JSON representation of the object.
  95. /// </summary>
  96. /// <param name="writer">The <see cref="JsonWriter"/> to write to.</param>
  97. /// <param name="value">The value.</param>
  98. /// <param name="serializer">The calling serializer.</param>
  99. public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
  100. {
  101. DefaultContractResolver resolver = serializer.ContractResolver as DefaultContractResolver;
  102. Type unionType = UnionTypeLookupCache.Get(value.GetType());
  103. Union union = UnionCache.Get(unionType);
  104. int tag = (int)union.TagReader.Invoke(value);
  105. UnionCase caseInfo = union.Cases.Single(c => c.Tag == tag);
  106. writer.WriteStartObject();
  107. writer.WritePropertyName((resolver != null) ? resolver.GetResolvedPropertyName(CasePropertyName) : CasePropertyName);
  108. writer.WriteValue(caseInfo.Name);
  109. if (caseInfo.Fields != null && caseInfo.Fields.Length > 0)
  110. {
  111. object[] fields = (object[])caseInfo.FieldReader.Invoke(value);
  112. writer.WritePropertyName((resolver != null) ? resolver.GetResolvedPropertyName(FieldsPropertyName) : FieldsPropertyName);
  113. writer.WriteStartArray();
  114. foreach (object field in fields)
  115. {
  116. serializer.Serialize(writer, field);
  117. }
  118. writer.WriteEndArray();
  119. }
  120. writer.WriteEndObject();
  121. }
  122. /// <summary>
  123. /// Reads the JSON representation of the object.
  124. /// </summary>
  125. /// <param name="reader">The <see cref="JsonReader"/> to read from.</param>
  126. /// <param name="objectType">Type of the object.</param>
  127. /// <param name="existingValue">The existing value of object being read.</param>
  128. /// <param name="serializer">The calling serializer.</param>
  129. /// <returns>The object value.</returns>
  130. public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
  131. {
  132. if (reader.TokenType == JsonToken.Null)
  133. {
  134. return null;
  135. }
  136. UnionCase caseInfo = null;
  137. string caseName = null;
  138. JArray fields = null;
  139. // start object
  140. reader.ReadAndAssert();
  141. while (reader.TokenType == JsonToken.PropertyName)
  142. {
  143. string propertyName = reader.Value.ToString();
  144. if (string.Equals(propertyName, CasePropertyName, StringComparison.OrdinalIgnoreCase))
  145. {
  146. reader.ReadAndAssert();
  147. Union union = UnionCache.Get(objectType);
  148. caseName = reader.Value.ToString();
  149. caseInfo = union.Cases.SingleOrDefault(c => c.Name == caseName);
  150. if (caseInfo == null)
  151. {
  152. throw JsonSerializationException.Create(reader, "No union type found with the name '{0}'.".FormatWith(CultureInfo.InvariantCulture, caseName));
  153. }
  154. }
  155. else if (string.Equals(propertyName, FieldsPropertyName, StringComparison.OrdinalIgnoreCase))
  156. {
  157. reader.ReadAndAssert();
  158. if (reader.TokenType != JsonToken.StartArray)
  159. {
  160. throw JsonSerializationException.Create(reader, "Union fields must been an array.");
  161. }
  162. fields = (JArray)JToken.ReadFrom(reader);
  163. }
  164. else
  165. {
  166. throw JsonSerializationException.Create(reader, "Unexpected property '{0}' found when reading union.".FormatWith(CultureInfo.InvariantCulture, propertyName));
  167. }
  168. reader.ReadAndAssert();
  169. }
  170. if (caseInfo == null)
  171. {
  172. throw JsonSerializationException.Create(reader, "No '{0}' property with union name found.".FormatWith(CultureInfo.InvariantCulture, CasePropertyName));
  173. }
  174. object[] typedFieldValues = new object[caseInfo.Fields.Length];
  175. if (caseInfo.Fields.Length > 0 && fields == null)
  176. {
  177. throw JsonSerializationException.Create(reader, "No '{0}' property with union fields found.".FormatWith(CultureInfo.InvariantCulture, FieldsPropertyName));
  178. }
  179. if (fields != null)
  180. {
  181. if (caseInfo.Fields.Length != fields.Count)
  182. {
  183. throw JsonSerializationException.Create(reader, "The number of field values does not match the number of properties defined by union '{0}'.".FormatWith(CultureInfo.InvariantCulture, caseName));
  184. }
  185. for (int i = 0; i < fields.Count; i++)
  186. {
  187. JToken t = fields[i];
  188. PropertyInfo fieldProperty = caseInfo.Fields[i];
  189. typedFieldValues[i] = t.ToObject(fieldProperty.PropertyType, serializer);
  190. }
  191. }
  192. object[] args = { typedFieldValues };
  193. return caseInfo.Constructor.Invoke(args);
  194. }
  195. /// <summary>
  196. /// Determines whether this instance can convert the specified object type.
  197. /// </summary>
  198. /// <param name="objectType">Type of the object.</param>
  199. /// <returns>
  200. /// <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
  201. /// </returns>
  202. public override bool CanConvert(Type objectType)
  203. {
  204. if (typeof(IEnumerable).IsAssignableFrom(objectType))
  205. {
  206. return false;
  207. }
  208. // all fsharp objects have CompilationMappingAttribute
  209. // get the fsharp assembly from the attribute and initialize latebound methods
  210. object[] attributes;
  211. attributes = objectType.GetCustomAttributes(true);
  212. bool isFSharpType = false;
  213. foreach (object attribute in attributes)
  214. {
  215. Type attributeType = attribute.GetType();
  216. if (attributeType.FullName == "Microsoft.FSharp.Core.CompilationMappingAttribute")
  217. {
  218. FSharpUtils.EnsureInitialized(attributeType.Assembly());
  219. isFSharpType = true;
  220. break;
  221. }
  222. }
  223. if (!isFSharpType)
  224. {
  225. return false;
  226. }
  227. return (bool)FSharpUtils.IsUnion(null, objectType, null);
  228. }
  229. }
  230. }
  231. #endif