-
Notifications
You must be signed in to change notification settings - Fork 40
/
Copy pathCompact.fs
511 lines (447 loc) · 26.3 KB
/
Compact.fs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
namespace Microsoft.FSharpLu.Json
open Newtonsoft.Json
open Newtonsoft.Json.Serialization
open Microsoft.FSharp.Reflection
open System.Reflection
open System.Collections.Concurrent
open System
module private ConverterHelpers =
let inline stringEq (a:string) (b:string) =
a.Equals(b, System.StringComparison.OrdinalIgnoreCase)
let inline isOptionType (t:System.Type) =
t.GetTypeInfo().IsGenericType && t.GetGenericTypeDefinition() = typedefof<option<_>>
let inline isTupleType (t:System.Type) =
FSharpType.IsTuple t
let inline isTupleItemProperty (prop:System.Reflection.PropertyInfo) =
// Item1, Item2, etc. excluding Items[n] indexer. Valid only on tuple types.
(prop.Name.StartsWith("Item") || prop.Name = "Rest") && (Seq.isEmpty <| prop.GetIndexParameters())
module Memorised =
let inline memorise (f: 'key -> 'result) =
let d = ConcurrentDictionary<'key, 'result>()
fun key -> d.GetOrAdd(key, f)
let getUnionCaseFields = memorise FSharpValue.PreComputeUnionReader
let getUnionTag = memorise FSharpValue.PreComputeUnionTagReader
let getUnionCasesByTag = memorise (fun t -> FSharpType.GetUnionCases(t) |> Array.map (fun x -> x.Tag, x) |> dict)
let getUnionCases = memorise FSharpType.GetUnionCases
let constructUnionCase = memorise FSharpValue.PreComputeUnionConstructor
let getUnionCaseProperyInfoFields = memorise (fun (case: UnionCaseInfo) -> case.GetFields())
let findNoFieldsMatchingUnionCaseByNameAndType =
memorise <| fun (objectType, caseName) ->
let cases = getUnionCases objectType
cases |> Array.tryFind (fun case -> ConverterHelpers.stringEq case.Name caseName && (getUnionCaseProperyInfoFields case |> Array.isEmpty))
let findMatchingUnionCaseByNameAndType =
memorise <| fun (objectType, caseName) ->
let cases = getUnionCases objectType
cases |> Array.tryFind (fun case -> ConverterHelpers.stringEq case.Name caseName)
let getUnionTagOfValue v =
let t = v.GetType()
getUnionTag t v
let inline getUnionFields v =
let cases = getUnionCasesByTag (v.GetType())
let tag = getUnionTagOfValue v
let case = cases.[tag]
let unionReader = getUnionCaseFields case
(case, unionReader v)
let SomeFieldIdentifier = "Some"
/// Determine if a given type has a field named 'Some' which would cause
/// ambiguity if nested under an option type without being boxed
let hasFieldNamedSome =
memorise
(fun (t:System.Type) ->
ConverterHelpers.isOptionType t // the option type itself has a 'Some' field
|| (FSharpType.IsRecord t && FSharpType.GetRecordFields t |> Seq.exists (fun r -> ConverterHelpers.stringEq r.Name SomeFieldIdentifier))
|| (FSharpType.IsUnion t && FSharpType.GetUnionCases t |> Seq.exists (fun r -> ConverterHelpers.stringEq r.Name SomeFieldIdentifier)))
open ConverterHelpers
open Memorised
/// Serializers for F# discriminated unions improving upon the stock implementation by JSon.Net
/// The default formatting used by Json.Net to serialize F# discriminated unions
/// and Option types is too verbose. This module implements a more succinct serialization
/// for those data types.
type CompactUnionJsonConverter(?tupleAsHeterogeneousArray:bool, ?usePropertyFormatterForValues:bool) =
inherit Newtonsoft.Json.JsonConverter()
/// By default tuples are serialized as heterogeneous arrays.
let tupleAsHeterogeneousArray = defaultArg tupleAsHeterogeneousArray true
/// By default formatting is used for values
let usePropertyFormatterForValues = defaultArg usePropertyFormatterForValues true
let canConvertMemorised =
memorise
(fun objectType ->
( // Include F# discriminated unions
FSharpType.IsUnion objectType
// and exclude the standard FSharp lists (which are implemented as discriminated unions)
&& not (objectType.GetTypeInfo().IsGenericType && objectType.GetGenericTypeDefinition() = typedefof<_ list>)
)
// include tuples
|| tupleAsHeterogeneousArray && FSharpType.IsTuple objectType
)
override __.CanConvert(objectType:System.Type) = canConvertMemorised objectType
override __.WriteJson(writer:JsonWriter, value:obj, serializer:JsonSerializer) =
let t = value.GetType()
let convertName =
match serializer.ContractResolver with
| :? DefaultContractResolver as resolver -> resolver.GetResolvedPropertyName
| _ -> id
// Option type?
if isOptionType t then
let cases = getUnionCases t
let none, some = cases.[0], cases.[1]
let case, fields = getUnionFields value
if case = none then
() // None is serialized as just null
// Some _
else
// Handle cases `Some None` and `Some null`
let innerType = (getUnionCaseProperyInfoFields some).[0].PropertyType
let innerValue = fields.[0]
if isNull innerValue then
writer.WriteStartObject()
writer.WritePropertyName(convertName SomeFieldIdentifier)
writer.WriteNull()
writer.WriteEndObject()
// Some v with v <> null && v <> None
else
// Is it nesting another option: `(e.g., "Some (Some ... Some ( ... )))"`
// or any other type with a field named 'Some'?
if hasFieldNamedSome innerType then
// Preserved the nested structure through boxing
writer.WriteStartObject()
writer.WritePropertyName(convertName SomeFieldIdentifier)
serializer.Serialize(writer, innerValue)
writer.WriteEndObject()
else
// Type is option<'a> where 'a does not have a field named 'Some
// (and therfore in particular is NOT an option type itself)
// => we can simplify the Json by omitting the `Some` boxing
// and serializing the nested object directly
serializer.Serialize(writer, innerValue)
// Tuple
else if tupleAsHeterogeneousArray && isTupleType t then
let v = FSharpValue.GetTupleFields value
serializer.Serialize(writer, v)
// Discriminated union
else
let case, fields = getUnionFields value
match fields with
// Field-less union case
| [||] when usePropertyFormatterForValues -> writer.WriteValue(convertName case.Name)
| [||] when not usePropertyFormatterForValues -> writer.WriteValue(case.Name)
// Case with single field
| [|onefield|] ->
writer.WriteStartObject()
writer.WritePropertyName(convertName case.Name)
serializer.Serialize(writer, onefield)
writer.WriteEndObject()
// Case with list of fields
| _ ->
writer.WriteStartObject()
writer.WritePropertyName(convertName case.Name)
serializer.Serialize(writer, fields)
writer.WriteEndObject()
override __.ReadJson(reader:JsonReader, objectType:System.Type, existingValue:obj, serializer:JsonSerializer) =
let failreadwith s = raise (JsonReaderException s)
let failreadwithf format = Printf.ksprintf failreadwith format
// Option type?
if isOptionType objectType then
let cases = getUnionCases objectType
let caseNone, caseSome = cases.[0], cases.[1]
let jToken = Linq.JToken.ReadFrom(reader)
// Json Null maps to `None`
if jToken.Type = Linq.JTokenType.Null then
FSharpValue.MakeUnion(caseNone, [||])
// Json that is not null must map to `Some _`
else
let nestedType = objectType.GetTypeInfo().GetGenericArguments().[0]
// Try to retrieve the 'Some' attribute:
// if the specified Json an object of the form `{ "Some" = token }`
// then return `Some token`, otherwise returns `None`.
let tryGetSomeAttributeValue (jToken:Linq.JToken) =
if jToken.Type = Linq.JTokenType.Object then
let jObject = jToken :?> Linq.JObject
match jObject.TryGetValue (SomeFieldIdentifier, System.StringComparison.OrdinalIgnoreCase) with
| true, token -> Some token
| false, _ -> None
else
None
let nestedValue =
match tryGetSomeAttributeValue jToken with
| Some someAttributeValue when someAttributeValue.Type = Linq.JTokenType.Null ->
// The Json object is { "Some" : null } for type option<'a>
// where 'a is nullable => deserialized to `Some null`
null
| Some someAttributeValue when hasFieldNamedSome nestedType ->
// Case of Json { "Some" : <obj> } where <obj> is not null
// => we just deserialize the nested object recursively
someAttributeValue.ToObject(nestedType, serializer)
| Some someAttributeValue ->
failreadwithf "Unexpected 'Some' Json attribute. Attribute value: %O" someAttributeValue
| None when hasFieldNamedSome nestedType ->
failreadwith "Types with a field named 'Some' and nested under an option type must be boxed under a 'Some' attribute when serialized to Json."
| None ->
// type is option<'a> where 'a is not an option type and not a
// type that would be serialized as a Json object.
// i.e. 'a is either a base Json type (e.g. integer or string) or
// a Json array.
// This means that the Json is not boxed under the `Some` attribute and we can therefore
// deserialize the object of type 'a directly without performing any unboxing.
jToken.ToObject(nestedType, serializer)
constructUnionCase caseSome [| nestedValue |]
// Tuple type?
else if tupleAsHeterogeneousArray && isTupleType objectType then
match reader.TokenType with
// JSON is an object with one field per element of the tuple
| JsonToken.StartObject ->
// backward-compat with legacy tuple serialization:
// if reader.TokenType is StartObject then we should expecte legacy JSON format for tuples
let jToken = Linq.JObject.Load(reader)
if isNull jToken then
failreadwithf "Expecting a legacy tuple, got null"
else
let readProperty (prop: PropertyInfo) =
match jToken.TryGetValue(prop.Name) with
| false,_ ->
failreadwithf "Cannot parse legacy tuple value: %O. Missing property: %s" jToken prop.Name
| true, jsonProp ->
jsonProp.ToObject(prop.PropertyType, serializer)
let tupleValues =
objectType.GetTypeInfo().DeclaredProperties
|> Seq.filter isTupleItemProperty
|> Seq.map readProperty
|> Array.ofSeq
System.Activator.CreateInstance(objectType, tupleValues)
// JSON is an heterogeneous array
| JsonToken.StartArray ->
let tupleType = objectType
let elementTypes = FSharpType.GetTupleElements(tupleType)
let readElement elementType =
let more = reader.Read()
if not more then
failreadwith "Missing array element in deserialized JSON"
let jToken = Linq.JToken.ReadFrom(reader)
jToken.ToObject(elementType, serializer)
let deserializedAsUntypedArray =
elementTypes
|> Array.map readElement
let more = reader.Read()
if reader.TokenType <> JsonToken.EndArray then
failreadwith "Expecting end of array token in deserialized JSON"
FSharpValue.MakeTuple(deserializedAsUntypedArray, tupleType)
| _ ->
failreadwithf "Expecting a JSON array or a JSON object, got something else: %A" reader.TokenType
// Discriminated union
else
// There are three types of union cases:
// | Case1 | Case2 of 'a | Case3 of 'a1 * 'a2 ... * 'an
// Those are respectively serialized to Json as
// "Case1"
// { "Case2" : value }
// { "Case3" : [v1, v2, ... vn] }
// Load JObject from stream
let jToken = Linq.JToken.Load(reader)
if isNull jToken then
null
// Type1: field-less union case
elif jToken.Type = Linq.JTokenType.String then
let caseName = jToken.ToString()
let matchingCase = findNoFieldsMatchingUnionCaseByNameAndType (objectType, caseName)
match matchingCase with
| Some case -> constructUnionCase case [||]
| None ->
let cases = getUnionCases objectType
failreadwithf "Cannot parse DU field-less value: %O. Expected names: %O" caseName (System.String.Join(", ", cases |> Seq.map(fun c->c.Name)))
// Type 2 or 3: Case with fields
elif jToken.Type = Linq.JTokenType.Object then
let jObject = jToken :?> Linq.JObject
let jObjectProperties = jObject.Properties()
if Seq.length jObjectProperties <> 1 then
failreadwith "Incorrect Json format for discriminated union. A DU value with fields must be serialized to a Json object with a single Json attribute"
let caseProperty = jObjectProperties |> Seq.head
/// Lookup the DU case by name
let matchingCase = findMatchingUnionCaseByNameAndType (objectType, caseProperty.Name)
match matchingCase with
| None ->
failreadwithf "Case with fields '%s' does not exist for discriminated union %s" caseProperty.Name objectType.Name
| Some case ->
let propertyInfosForCase = getUnionCaseProperyInfoFields case
// Type 2: A union case with a single field: Case2 of 'a
if propertyInfosForCase.Length = 1 then
let fieldType = propertyInfosForCase.[0].PropertyType
let field = caseProperty.Value.ToObject(fieldType, serializer)
constructUnionCase case [|field|]
// Type 3: A union case with more than one field: Case3 of 'a1 * 'a2 ... * 'an
else
// Here there could be an ambiguity:
// the Json values are either the fields of the case
// or if the array is Use target type to resolve ambiguity
let fields =
propertyInfosForCase
|> Seq.zip caseProperty.Value
|> Seq.map (fun (v,t) -> v.ToObject(t.PropertyType, serializer))
|> Seq.toArray
constructUnionCase case fields
else
failreadwithf "Unexpected Json token type %O: %O" jToken.Type jToken
/// Compact serializer
module Compact =
open System.Runtime.CompilerServices
/// Compact serialization where tuples are serialized as heterogeneous arrays
type TupleAsArraySettings =
static member formatting = Formatting.Indented
static member settings =
let settings =
JsonSerializerSettings(
NullValueHandling = NullValueHandling.Ignore,
// MissingMemberHandling is not technically needed for
// compact serialization but it avoids certain ambiguities
// that guarantee that deserialization coincides with the
// default Json.Net deserialization.
// (where 'coincides' means 'if the deserialization succeeds they both return the same object')
// This allows us to easily define the BackwardCompatible
// serializer (that handles both Compact and Default Json format) by reusing
// the Compact deserializer.
MissingMemberHandling = MissingMemberHandling.Error
)
settings.Converters.Add(CompactUnionJsonConverter(true, true))
settings
type private S = With<TupleAsArraySettings>
/// Serialize an object to Json with the specified converter
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline serialize< ^T> x = S.serialize x
/// Serialize an object to Json with the specified converter and save the result to a file
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline serializeToFile< ^T> file obj = S.serializeToFile file obj
/// Serialize an object to Json with the specified converter and write the result to a stream
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline serializeToStream< ^T> stream obj = S.serializeToStream stream obj
/// Try to deserialize json to an object of type ^T
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline tryDeserialize< ^T> json = S.tryDeserialize< ^T> json
/// Try to read Json from a file and desrialized it to an object of type ^T
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline tryDeserializeFile< ^T> file = S.tryDeserializeFile< ^T> file
/// Try to deserialize a stream to an object of type ^T
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline tryDeserializeStream< ^T> stream = S.tryDeserializeStream< ^T> stream
/// Deserialize a Json to an object of type ^T
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline deserialize< ^T> json : ^T = S.deserialize< ^T> json
/// Read Json from a file and desrialized it to an object of type ^T
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline deserializeFile< ^T> file = S.deserializeFile< ^T> file
/// Deserialize a stream to an object of type ^T
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline deserializeStream< ^T> stream = S.deserializeStream< ^T> stream
/// Legacy compact serialization where tuples are serialized as objects instead of arrays
module Legacy =
/// Compact serialization where tuples are serialized as JSON objects
type TupleAsObjectSettings =
static member formatting = Formatting.Indented
static member settings =
let settings =
JsonSerializerSettings(
NullValueHandling = NullValueHandling.Ignore,
MissingMemberHandling = MissingMemberHandling.Error
)
settings.Converters.Add(CompactUnionJsonConverter(false, true))
settings
type private S = With<TupleAsObjectSettings>
/// Serialize an object to Json with the specified converter
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline serialize< ^T> x = S.serialize x
/// Serialize an object to Json with the specified converter and save the result to a file
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline serializeToFile< ^T> file obj = S.serializeToFile file obj
/// Serialize an object to Json with the specified converter and write the result to a stream
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline serializeToStream< ^T> stream obj = S.serializeToStream stream obj
/// Try to deserialize json to an object of type ^T
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline tryDeserialize< ^T> json = S.tryDeserialize< ^T> json
/// Try to read Json from a file and desrialized it to an object of type ^T
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline tryDeserializeFile< ^T> file = S.tryDeserializeFile< ^T> file
/// Try to deserialize a stream to an object of type ^T
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline tryDeserializeStream< ^T> stream = S.tryDeserializeStream< ^T> stream
/// Deserialize a Json to an object of type ^T
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline deserialize< ^T> json : ^T = S.deserialize< ^T> json
/// Read Json from a file and desrialized it to an object of type ^T
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline deserializeFile< ^T> file = S.deserializeFile< ^T> file
/// Deserialize a stream to an object of type ^T
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline deserializeStream< ^T> stream = S.deserializeStream< ^T> stream
/// Compact serializer where desearilization requires presence of all properties
/// expect optional ones (of type option<_>)
module Strict =
/// A contract resolver that requires presence of all properties
/// that are not of type option<_>
type RequireNonOptionalPropertiesContractResolver() =
inherit Newtonsoft.Json.Serialization.DefaultContractResolver()
override __.CreateProperty(_member, memberSerialization) =
let property = base.CreateProperty(_member, memberSerialization)
let isRequired = not (property.PropertyType.GetTypeInfo().IsGenericType
&& property.PropertyType.GetGenericTypeDefinition() = typedefof<option<_>>)
if isRequired then
property.Required <- Required.Always
property.NullValueHandling <- System.Nullable NullValueHandling.Ignore
property
/// Compact serialization where tuples are serialized as JSON objects
type CompactStrictSettings =
static member formatting = Formatting.Indented
static member settings =
let settings =
JsonSerializerSettings(
ContractResolver =
RequireNonOptionalPropertiesContractResolver(
NamingStrategy = CamelCaseNamingStrategy(
ProcessDictionaryKeys = false,
OverrideSpecifiedNames = true
))
)
settings.Converters.Add(CompactUnionJsonConverter(true, true))
settings
type private S = With<CompactStrictSettings>
/// Serialize an object to Json with the specified converter
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline serialize< ^T> x = S.serialize x
/// Serialize an object to Json with the specified converter and save the result to a file
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline serializeToFile< ^T> file obj = S.serializeToFile file obj
/// Serialize an object to Json with the specified converter and write the result to a stream
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline serializeToStream< ^T> stream obj = S.serializeToStream stream obj
/// Try to deserialize json to an object of type ^T
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline tryDeserialize< ^T> json = S.tryDeserialize< ^T> json
/// Try to read Json from a file and desrialized it to an object of type ^T
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline tryDeserializeFile< ^T> file = S.tryDeserializeFile< ^T> file
/// Try to deserialize a stream to an object of type ^T
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline tryDeserializeStream< ^T> stream = S.tryDeserializeStream< ^T> stream
/// Deserialize a Json to an object of type ^T
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline deserialize< ^T> json : ^T = S.deserialize< ^T> json
/// Read Json from a file and desrialized it to an object of type ^T
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline deserializeFile< ^T> file = S.deserializeFile< ^T> file
/// Deserialize a stream to an object of type ^T
[<MethodImplAttribute(MethodImplOptions.NoInlining)>]
let inline deserializeStream< ^T> stream = S.deserializeStream< ^T> stream
module CamelCaseNoFormatting =
type CompactCamelCaseNoFormattingSettings =
static member formatting = Formatting.None
static member settings =
let settings =
JsonSerializerSettings(
NullValueHandling = NullValueHandling.Ignore,
MissingMemberHandling = MissingMemberHandling.Error,
ContractResolver = CamelCasePropertyNamesContractResolver(
NamingStrategy = CamelCaseNamingStrategy(
ProcessDictionaryKeys = false,
OverrideSpecifiedNames = true
)))
settings.Converters.Add(CompactUnionJsonConverter(true, false))
settings
type CamelCaseSerializer = With<CompactCamelCaseNoFormattingSettings>