Skip to content

Commit

Permalink
refactor: add FieldHelper
Browse files Browse the repository at this point in the history
  • Loading branch information
Tarmil committed Jul 30, 2023
1 parent d6205a8 commit e136134
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 177 deletions.
87 changes: 71 additions & 16 deletions src/FSharp.SystemTextJson/Helpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,6 @@ let isSkippableType (fsOptions: JsonFSharpOptionsRecord) (ty: Type) =
let isValueOptionType (ty: Type) =
ty.IsGenericType && ty.GetGenericTypeDefinition() = typedefof<ValueOption<_>>

let isSkip (fsOptions: JsonFSharpOptionsRecord) (ty: Type) =
if isSkippableType fsOptions ty then
let getTag = FSharpValue.PreComputeUnionTagReader(ty)
fun x -> getTag x = 0
else
fun _ -> false

[<AutoOpen>]
type Helper =
static member tryGetUnionCases(ty: Type, cases: UnionCaseInfo[] outref) =
Expand All @@ -82,6 +75,11 @@ type Helper =
&& cases.Length = 1
&& tryGetUnionCaseSingleProperty (cases[0], &property)

let isClass ty =
not (FSharpType.IsUnion(ty, true))
&& not (FSharpType.IsRecord(ty, true))
&& not (FSharpType.IsTuple(ty))

/// If null is a valid JSON representation for ty,
/// then return ValueSome with the value represented by null,
/// else return ValueNone.
Expand All @@ -96,12 +94,7 @@ let rec tryGetNullValue (fsOptions: JsonFSharpOptionsRecord) (ty: Type) : obj vo
elif isSkippableType fsOptions ty then
tryGetNullValue fsOptions (ty.GetGenericArguments()[0])
|> ValueOption.map (fun x -> FSharpValue.MakeUnion(FSharpType.GetUnionCases(ty, true)[1], [| x |], true))
elif
fsOptions.AllowNullFields
&& not (FSharpType.IsUnion(ty, true))
&& not (FSharpType.IsRecord(ty, true))
&& not (FSharpType.IsTuple(ty))
then
elif fsOptions.AllowNullFields && isClass ty then
ValueSome(if ty.IsValueType then Activator.CreateInstance(ty) else null)
else
match tryGetUnwrappedSingleCaseField (fsOptions, ty) with
Expand Down Expand Up @@ -139,9 +132,71 @@ let overrideOptions (ty: Type) (defaultOptions: JsonFSharpOptions) =
| true, options -> options |> inheritUnionEncoding
| false, _ -> applyAttributeOverride ()

let ignoreNullValues (options: JsonSerializerOptions) =
options.IgnoreNullValues
|| options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
type FieldHelper
(
options: JsonSerializerOptions,
fsOptions: JsonFSharpOptionsRecord,
ty: Type,
nullDeserializeError: string
) =

let nullValue = tryGetNullValue fsOptions ty
let isSkippableWrapperType = isSkippableType fsOptions ty
let ignoreNullValues =
options.IgnoreNullValues
|| options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
let canBeSkipped =
(ignoreNullValues && (nullValue.IsSome || isClass ty)) || isSkippableWrapperType
let deserializeType =
if isSkippableWrapperType then ty.GenericTypeArguments[0] else ty

let wrapDeserialized =
if isSkippableWrapperType then
let case = FSharpType.GetUnionCases(ty)[1]
let f = FSharpValue.PreComputeUnionConstructor(case, true)
fun x -> f [| box x |]
else
id

let isSkip =
if isSkippableWrapperType then
let getTag = FSharpValue.PreComputeUnionTagReader(ty)
fun x -> getTag x = 0
else
fun _ -> false

let ignoreOnWrite (v: obj) =
isSkip v || (ignoreNullValues && isNull v)

let defaultValue =
if isSkippableWrapperType || isValueOptionType ty then
let case = FSharpType.GetUnionCases(ty)[0]
ValueSome(FSharpValue.MakeUnion(case, [||]))
else
ValueNone


member _.NullValue = nullValue
member _.DefaultValue = defaultValue
member _.IsSkippableWrapperType = isSkippableWrapperType
member _.CanBeSkipped = canBeSkipped
member _.IgnoreOnWrite = ignoreOnWrite
member _.DeserializeType = deserializeType
member _.IsSkip = isSkip
member _.WrapDeserialized = wrapDeserialized
member _.NullDeserializeError = nullDeserializeError
member _.Options = options

member this.IsNullable = this.NullValue.IsNone

member this.Deserialize(reader: byref<Utf8JsonReader>) =
if reader.TokenType = JsonTokenType.Null && not this.IsSkippableWrapperType then
match this.NullValue with
| ValueSome v -> v
| ValueNone -> raise (JsonException this.NullDeserializeError)
else
JsonSerializer.Deserialize(&reader, this.DeserializeType, this.Options)
|> this.WrapDeserialized

let convertName (policy: JsonNamingPolicy) (name: string) =
match policy with
Expand Down
90 changes: 30 additions & 60 deletions src/FSharp.SystemTextJson/Record.fs
Original file line number Diff line number Diff line change
Expand Up @@ -13,52 +13,42 @@ type private RecordField
options: JsonSerializerOptions,
i: int,
p: PropertyInfo,
fieldOrderIndices: int[] voption
fieldOrderIndices: int[] voption,
names: string[]
) =
let names =
match getJsonNames "field" (fun ty -> p.GetCustomAttributes(ty, true)) with
| ValueSome names -> names |> Array.map (fun n -> n.AsString())
| ValueNone -> [| convertName options.PropertyNamingPolicy p.Name |]
inherit FieldHelper
(
options,
fsOptions,
p.PropertyType,
sprintf
"%s.%s was expected to be of type %s, but was null."
p.DeclaringType.Name
names[0]
p.PropertyType.Name
)

let ignore =
p.GetCustomAttributes(typeof<JsonIgnoreAttribute>, true) |> Array.isEmpty |> not

let nullValue = tryGetNullValue fsOptions p.PropertyType

let isSkippableType = isSkippableType fsOptions p.PropertyType

let canBeSkipped =
ignore || (ignoreNullValues options && nullValue.IsSome) || isSkippableType

let read =
let m = p.GetGetMethod()
fun o -> m.Invoke(o, Array.empty)

let deserializeType =
if isSkippableType then
p.PropertyType.GenericTypeArguments[0]
else
p.PropertyType

let wrapDeserialized =
if isSkippableType then
let case = FSharpType.GetUnionCases(p.PropertyType)[1]
let f = FSharpValue.PreComputeUnionConstructor(case, true)
fun x -> f [| box x |]
else
id
new(fsOptions, options: JsonSerializerOptions, i, p: PropertyInfo, fieldOrderIndices) =
let names =
match getJsonNames "field" (fun ty -> p.GetCustomAttributes(ty, true)) with
| ValueSome names -> names |> Array.map (fun n -> n.AsString())
| ValueNone -> [| convertName options.PropertyNamingPolicy p.Name |]
RecordField(fsOptions, options, i, p, fieldOrderIndices, names)

member _.Names = names

member _.Type = p.PropertyType

member _.Ignore = ignore

member _.NullValue = nullValue

member _.MustBePresent = not canBeSkipped

member _.IsSkip = isSkip fsOptions p.PropertyType
member this.MustBePresent = not (ignore || this.CanBeSkipped)

member _.Read(value: obj) =
read value
Expand All @@ -68,28 +58,8 @@ type private RecordField
| ValueSome a -> a[i]
| ValueNone -> i

member _.Deserialize(reader: byref<Utf8JsonReader>, recordType: Type) =
if reader.TokenType = JsonTokenType.Null && not isSkippableType then
match nullValue with
| ValueSome v -> v
| ValueNone ->
failf "%s.%s was expected to be of type %s, but was null." recordType.Name names[0] p.PropertyType.Name
else
JsonSerializer.Deserialize(&reader, deserializeType, options)
|> wrapDeserialized

type private RecordField1 =
{ Names: string[]
Type: Type
Ignore: bool
NullValue: obj voption
MustBePresent: bool
IsSkip: obj -> bool
Read: obj -> obj
WriteOrder: int }

type internal IRecordConverter =
abstract ReadRestOfObject: byref<Utf8JsonReader> * JsonSerializerOptions * skipFirstRead: bool -> obj
abstract ReadRestOfObject: byref<Utf8JsonReader> * skipFirstRead: bool -> obj
abstract WriteRestOfObject: Utf8JsonWriter * obj * JsonSerializerOptions -> unit
abstract FieldNames: string[]

Expand Down Expand Up @@ -166,9 +136,9 @@ type JsonRecordConverter<'T> internal (options: JsonSerializerOptions, fsOptions
let arr = Array.zeroCreate fieldCount
fieldProps
|> Array.iteri (fun i field ->
if isSkippableType fsOptions field.Type || isValueOptionType field.Type then
let case = FSharpType.GetUnionCases(field.Type)[0]
arr[i] <- FSharpValue.MakeUnion(case, [||])
match field.DefaultValue with
| ValueSome v -> arr[i] <- v
| ValueNone -> ()
)
arr

Expand Down Expand Up @@ -207,9 +177,9 @@ type JsonRecordConverter<'T> internal (options: JsonSerializerOptions, fsOptions

override this.Read(reader, typeToConvert, options) =
expectAlreadyRead JsonTokenType.StartObject "JSON object" &reader typeToConvert
this.ReadRestOfObject(&reader, options, false)
this.ReadRestOfObject(&reader, false)

member internal _.ReadRestOfObject(reader, options, skipFirstRead) =
member internal _.ReadRestOfObject(reader, skipFirstRead) =
let fields = Array.copy defaultFields
let mutable cont = true
let mutable requiredFieldCount = 0
Expand All @@ -223,7 +193,7 @@ type JsonRecordConverter<'T> internal (options: JsonSerializerOptions, fsOptions
| ValueSome (i, p) when not p.Ignore ->
if p.MustBePresent then requiredFieldCount <- requiredFieldCount + 1
reader.Read() |> ignore
fields[i] <- p.Deserialize(&reader, recordType)
fields[i] <- p.Deserialize(&reader)
| _ -> reader.Skip()
| _ -> ()

Expand All @@ -244,14 +214,14 @@ type JsonRecordConverter<'T> internal (options: JsonSerializerOptions, fsOptions
let values = dector value
for struct (i, p) in writeOrderedFieldProps do
let v = if i < fieldCount then values[i] else p.Read value
if not p.Ignore && not (ignoreNullValues options && isNull v) && not (p.IsSkip v) then
if not (p.Ignore || p.IgnoreOnWrite v) then
writer.WritePropertyName(p.Names[0])
JsonSerializer.Serialize(writer, v, p.Type, options)
writer.WriteEndObject()

interface IRecordConverter with
member this.ReadRestOfObject(reader, options, skipFirstRead) =
box (this.ReadRestOfObject(&reader, options, skipFirstRead))
member this.ReadRestOfObject(reader, skipFirstRead) =
box (this.ReadRestOfObject(&reader, skipFirstRead))
member this.WriteRestOfObject(writer, value, options) =
this.WriteRestOfObject(writer, unbox value, options)
member _.FieldNames = fieldProps |> Array.collect (fun p -> p.Names)
Expand Down
Loading

0 comments on commit e136134

Please sign in to comment.