|
| 1 | +namespace FsCodec.SystemTextJson.Core |
| 2 | + |
| 3 | +open System.Text.Json |
| 4 | + |
| 5 | +/// System.Text.Json implementation of TypeShape.UnionContractEncoder's IEncoder that encodes to a `JsonElement` |
| 6 | +type JsonElementEncoder(options : JsonSerializerOptions) = |
| 7 | + interface TypeShape.UnionContract.IEncoder<JsonElement> with |
| 8 | + member __.Empty = Unchecked.defaultof<_> |
| 9 | + |
| 10 | + member __.Encode(value : 'T) = |
| 11 | + JsonSerializer.SerializeToElement(value, options) |
| 12 | + |
| 13 | + member __.Decode(json : JsonElement) = |
| 14 | + JsonSerializer.DeserializeElement(json, options) |
| 15 | + |
| 16 | +namespace FsCodec.SystemTextJson |
| 17 | + |
| 18 | +open System |
| 19 | +open System.Runtime.InteropServices |
| 20 | +open System.Text.Json |
| 21 | + |
| 22 | +/// Provides Codecs that render to a JsonElement suitable for storage in Event Stores based using <c>System.Text.Json</c> and the conventions implied by using |
| 23 | +/// <c>TypeShape.UnionContract.UnionContractEncoder</c> - if you need full control and/or have have your own codecs, see <c>FsCodec.Codec.Create</c> instead |
| 24 | +/// See <a href=""https://github.com/eiriktsarpalis/TypeShape/blob/master/tests/TypeShape.Tests/UnionContractTests.fs"></a> for example usage. |
| 25 | +type Codec private () = |
| 26 | + |
| 27 | + static let defaultOptions = lazy Options.Create() |
| 28 | + |
| 29 | + /// Generate an <code>IEventCodec</code> using the supplied <c>System.Text.Json<c/> <c>options</c>. |
| 30 | + /// Uses <c>up</c> and <c>down</c> functions to facilitate upconversion/downconversion |
| 31 | + /// and/or surfacing metadata to the Programming Model by including it in the emitted <c>'Event</c> |
| 32 | + /// The Event Type Names are inferred based on either explicit <c>DataMember(Name=</c> Attributes, or (if unspecified) the Discriminated Union Case Name |
| 33 | + /// <c>Contract</c> must be tagged with </c>interface TypeShape.UnionContract.IUnionContract</c> to signify this scheme applies. |
| 34 | + static member Create<'Event, 'Contract, 'Meta, 'Context when 'Contract :> TypeShape.UnionContract.IUnionContract> |
| 35 | + ( /// Maps from the TypeShape <c>UnionConverter</c> <c>'Contract</c> case the Event has been mapped to (with the raw event data as context) |
| 36 | + /// to the <c>'Event</c> representation (typically a Discriminated Union) that is to be presented to the programming model. |
| 37 | + up : FsCodec.ITimelineEvent<JsonElement> * 'Contract -> 'Event, |
| 38 | + /// Maps a fresh Event resulting from a Decision in the Domain representation type down to the TypeShape <c>UnionConverter</c> <c>'Contract</c> |
| 39 | + /// The function is also expected to derive |
| 40 | + /// a <c>meta</c> object that will be serialized with the same options (if it's not <c>None</c>) |
| 41 | + /// and an Event Creation <c>timestamp</c>. |
| 42 | + down : 'Context option * 'Event -> 'Contract * 'Meta option * Guid * string * string * DateTimeOffset option, |
| 43 | + /// Configuration to be used by the underlying <c>System.Text.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Options.Create()</c> |
| 44 | + [<Optional; DefaultParameterValue(null)>] ?options, |
| 45 | + /// Enables one to fail encoder generation if union contains nullary cases. Defaults to <c>false</c>, i.e. permitting them |
| 46 | + [<Optional; DefaultParameterValue(null)>] ?rejectNullaryCases) |
| 47 | + : FsCodec.IEventCodec<'Event, JsonElement, 'Context> = |
| 48 | + |
| 49 | + let options = match options with Some x -> x | None -> defaultOptions.Value |
| 50 | + let elementEncoder : TypeShape.UnionContract.IEncoder<_> = Core.JsonElementEncoder(options) :> _ |
| 51 | + let dataCodec = |
| 52 | + TypeShape.UnionContract.UnionContractEncoder.Create<'Contract, JsonElement>( |
| 53 | + elementEncoder, |
| 54 | + requireRecordFields = true, // See JsonConverterTests - round-tripping UTF-8 correctly with Json.net is painful so for now we lock up the dragons |
| 55 | + allowNullaryCases = not (defaultArg rejectNullaryCases false)) |
| 56 | + |
| 57 | + { new FsCodec.IEventCodec<'Event, JsonElement, 'Context> with |
| 58 | + member __.Encode(context, event) = |
| 59 | + let (c, meta : 'Meta option, eventId, correlationId, causationId, timestamp : DateTimeOffset option) = down (context, event) |
| 60 | + let enc = dataCodec.Encode c |
| 61 | + let metaUtf8 = meta |> Option.map elementEncoder.Encode<'Meta> |
| 62 | + FsCodec.Core.EventData.Create(enc.CaseName, enc.Payload, Unchecked.defaultof<_>, eventId, correlationId, causationId, ?timestamp = timestamp) |
| 63 | + |
| 64 | + member __.TryDecode encoded = |
| 65 | + match dataCodec.TryDecode { CaseName = encoded.EventType; Payload = encoded.Data } with |
| 66 | + | None -> None |
| 67 | + | Some contract -> up (encoded, contract) |> Some } |
| 68 | + |
| 69 | + /// Generate an <code>IEventCodec</code> using the supplied <c>System.Text.Json<c/> <c>options</c>. |
| 70 | + /// Uses <c>up</c> and <c>down</c> and <c>mapCausation</c> functions to facilitate upconversion/downconversion and correlation/causationId mapping |
| 71 | + /// and/or surfacing metadata to the Programming Model by including it in the emitted <c>'Event</c> |
| 72 | + /// The Event Type Names are inferred based on either explicit <c>DataMember(Name=</c> Attributes, or (if unspecified) the Discriminated Union Case Name |
| 73 | + /// <c>Contract</c> must be tagged with </c>interface TypeShape.UnionContract.IUnionContract</c> to signify this scheme applies. |
| 74 | + static member Create<'Event, 'Contract, 'Meta, 'Context when 'Contract :> TypeShape.UnionContract.IUnionContract> |
| 75 | + ( /// Maps from the TypeShape <c>UnionConverter</c> <c>'Contract</c> case the Event has been mapped to (with the raw event data as context) |
| 76 | + /// to the representation (typically a Discriminated Union) that is to be presented to the programming model. |
| 77 | + up : FsCodec.ITimelineEvent<JsonElement> * 'Contract -> 'Event, |
| 78 | + /// Maps a fresh Event resulting from a Decision in the Domain representation type down to the TypeShape <c>UnionConverter</c> <c>'Contract</c> |
| 79 | + /// The function is also expected to derive |
| 80 | + /// a <c>meta</c> object that will be serialized with the same options (if it's not <c>None</c>) |
| 81 | + /// and an Event Creation <c>timestamp</c>. |
| 82 | + down : 'Event -> 'Contract * 'Meta option * DateTimeOffset option, |
| 83 | + /// Uses the 'Context passed to the Encode call and the 'Meta emitted by <c>down</c> to a) the final metadata b) the <c>correlationId</c> and c) the correlationId |
| 84 | + mapCausation : 'Context option * 'Meta option -> 'Meta option * Guid * string * string, |
| 85 | + /// Configuration to be used by the underlying <c>System.Text.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Options.Create()</c> |
| 86 | + [<Optional; DefaultParameterValue(null)>] ?options, |
| 87 | + /// Enables one to fail encoder generation if union contains nullary cases. Defaults to <c>false</c>, i.e. permitting them |
| 88 | + [<Optional; DefaultParameterValue(null)>] ?rejectNullaryCases) |
| 89 | + : FsCodec.IEventCodec<'Event, JsonElement, 'Context> = |
| 90 | + |
| 91 | + let down (context, union) = |
| 92 | + let c, m, t = down union |
| 93 | + let m', eventId, correlationId, causationId = mapCausation (context, m) |
| 94 | + c, m', eventId, correlationId, causationId, t |
| 95 | + Codec.Create(up = up, down = down, ?options = options, ?rejectNullaryCases = rejectNullaryCases) |
| 96 | + |
| 97 | + /// Generate an <code>IEventCodec</code> using the supplied <c>System.Text.Json<c/> <c>options</c>. |
| 98 | + /// Uses <c>up</c> and <c>down</c> and <c>mapCausation</c> functions to facilitate upconversion/downconversion and correlation/causationId mapping |
| 99 | + /// and/or surfacing metadata to the Programming Model by including it in the emitted <c>'Event</c> |
| 100 | + /// The Event Type Names are inferred based on either explicit <c>DataMember(Name=</c> Attributes, or (if unspecified) the Discriminated Union Case Name |
| 101 | + /// <c>Contract</c> must be tagged with </c>interface TypeShape.UnionContract.IUnionContract</c> to signify this scheme applies. |
| 102 | + static member Create<'Event, 'Contract, 'Meta when 'Contract :> TypeShape.UnionContract.IUnionContract> |
| 103 | + ( /// Maps from the TypeShape <c>UnionConverter</c> <c>'Contract</c> case the Event has been mapped to (with the raw event data as context) |
| 104 | + /// to the representation (typically a Discriminated Union) that is to be presented to the programming model. |
| 105 | + up : FsCodec.ITimelineEvent<JsonElement> * 'Contract -> 'Event, |
| 106 | + /// Maps a fresh <c>'Event</c> resulting from a Decision in the Domain representation type down to the TypeShape <c>UnionConverter</c> <c>'Contract</c> |
| 107 | + /// The function is also expected to derive |
| 108 | + /// a <c>meta</c> object that will be serialized with the same options (if it's not <c>None</c>) |
| 109 | + /// and an Event Creation <c>timestamp</c>. |
| 110 | + down : 'Event -> 'Contract * 'Meta option * DateTimeOffset option, |
| 111 | + /// Configuration to be used by the underlying <c>System.Text.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Options.Create()</c> |
| 112 | + [<Optional; DefaultParameterValue(null)>] ?options, |
| 113 | + /// Enables one to fail encoder generation if union contains nullary cases. Defaults to <c>false</c>, i.e. permitting them |
| 114 | + [<Optional; DefaultParameterValue(null)>] ?rejectNullaryCases) |
| 115 | + : FsCodec.IEventCodec<'Event, JsonElement, obj> = |
| 116 | + |
| 117 | + let mapCausation (_context : obj, m : 'Meta option) = m, Guid.NewGuid(), null, null |
| 118 | + Codec.Create(up = up, down = down, mapCausation = mapCausation, ?options = options, ?rejectNullaryCases = rejectNullaryCases) |
| 119 | + |
| 120 | + /// Generate an <code>IEventCodec</code> using the supplied <c>System.Text.Json</c> <c>options</c>. |
| 121 | + /// The Event Type Names are inferred based on either explicit <c>DataMember(Name=</c> Attributes, or (if unspecified) the Discriminated Union Case Name |
| 122 | + /// <c>'Union</c> must be tagged with <c>interface TypeShape.UnionContract.IUnionContract</c> to signify this scheme applies. |
| 123 | + static member Create<'Union when 'Union :> TypeShape.UnionContract.IUnionContract> |
| 124 | + ( // Configuration to be used by the underlying <c>System.Text.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Options.Create()</c> |
| 125 | + [<Optional; DefaultParameterValue(null)>] ?options, |
| 126 | + /// Enables one to fail encoder generation if union contains nullary cases. Defaults to <c>false</c>, i.e. permitting them |
| 127 | + [<Optional; DefaultParameterValue(null)>] ?rejectNullaryCases) |
| 128 | + : FsCodec.IEventCodec<'Union, JsonElement, obj> = |
| 129 | + |
| 130 | + let up : FsCodec.ITimelineEvent<_> * 'Union -> 'Union = snd |
| 131 | + let down (event : 'Union) = event, None, None |
| 132 | + Codec.Create(up = up, down = down, ?options = options, ?rejectNullaryCases = rejectNullaryCases) |
0 commit comments