/// <summary> /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="NullSchema" />. /// </summary> /// <returns> /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" /> /// is a <see cref="NullSchema" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <inheritdoc /> public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context) { if (schema is NullSchema) { var tokenType = typeof(Utf8JsonReader) .GetProperty(nameof(Utf8JsonReader.TokenType)); var getUnexpectedTokenException = typeof(JsonExceptionHelper) .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException)); return(JsonDeserializerBuilderCaseResult.FromExpression( Expression.Block( Expression.IfThen( Expression.NotEqual( Expression.Property(context.Reader, tokenType), Expression.Constant(JsonTokenType.Null)), Expression.Throw( Expression.Call( null, getUnexpectedTokenException, context.Reader, Expression.Constant(new[] { JsonTokenType.Null })))), Expression.Default(type)))); } else { return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonNullDeserializerBuilderCase)} can only be applied to {nameof(NullSchema)}s."))); } }
/// <summary> /// Creates a new <see cref="JsonDeserializerBuilderCaseResult" /> for an unsuccessful /// outcome. /// </summary> /// <param name="exception"> /// An exception describing the inapplicability of the case. /// </param> /// <returns> /// A <see cref="JsonDeserializerBuilderCaseResult" /> with <see cref="Exceptions" /> /// populated and <see cref="Expression" /> <c>null</c>. /// </returns> public static JsonDeserializerBuilderCaseResult FromException(Exception exception) { var result = new JsonDeserializerBuilderCaseResult(); result.Exceptions.Add(exception); return(result); }
/// <summary> /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="TimestampLogicalType" />. /// </summary> /// <returns> /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" /> /// has a <see cref="TimestampLogicalType" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedSchemaException"> /// Thrown when <paramref name="schema" /> is not a <see cref="LongSchema" /> or when /// <paramref name="schema" /> does not have a <see cref="MicrosecondTimestampLogicalType" /> /// or a <see cref="MillisecondTimestampLogicalType" />. /// </exception> /// <exception cref="UnsupportedTypeException"> /// Thrown when <see cref="DateTime" /> cannot be converted to <paramref name="type" />. /// </exception> /// <inheritdoc /> public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context) { if (schema.LogicalType is TimestampLogicalType) { if (schema is not LongSchema) { throw new UnsupportedSchemaException(schema); } Expression epoch = Expression.Constant(Epoch); Expression factor; if (schema.LogicalType is MicrosecondTimestampLogicalType) { factor = Expression.Constant(TimeSpan.TicksPerMillisecond / 1000); } else if (schema.LogicalType is MillisecondTimestampLogicalType) { factor = Expression.Constant(TimeSpan.TicksPerMillisecond); } else { throw new UnsupportedSchemaException(schema); } var getInt64 = typeof(Utf8JsonReader) .GetMethod(nameof(Utf8JsonReader.GetInt64), Type.EmptyTypes); Expression expression = Expression.Call(context.Reader, getInt64); var addTicks = typeof(DateTime) .GetMethod(nameof(DateTime.AddTicks)); try { // return Epoch.AddTicks(value * factor); return(JsonDeserializerBuilderCaseResult.FromExpression( BuildConversion( Expression.Call(epoch, addTicks, Expression.Multiply(expression, factor)), type))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {schema} to {type}.", exception); } } else { return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonTimestampDeserializerBuilderCase)} can only be applied schemas with a {nameof(TimestampLogicalType)}."))); } }
/// <summary> /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="BytesSchema" />. /// </summary> /// <returns> /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" /> /// is a <see cref="BytesSchema" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <see cref="T:System.Byte[]" /> cannot be converted to <paramref name="type" />. /// </exception> /// <inheritdoc /> public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context) { if (schema is BytesSchema bytesSchema) { var tokenType = typeof(Utf8JsonReader) .GetProperty(nameof(Utf8JsonReader.TokenType)); var getUnexpectedTokenException = typeof(JsonExceptionHelper) .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException)); var getString = typeof(Utf8JsonReader) .GetMethod(nameof(Utf8JsonReader.GetString), Type.EmptyTypes); var getBytes = typeof(Encoding) .GetMethod(nameof(Encoding.GetBytes), new[] { typeof(string) }); try { return(JsonDeserializerBuilderCaseResult.FromExpression( BuildConversion( Expression.Block( Expression.IfThen( Expression.NotEqual( Expression.Property(context.Reader, tokenType), Expression.Constant(JsonTokenType.String)), Expression.Throw( Expression.Call( null, getUnexpectedTokenException, context.Reader, Expression.Constant(new[] { JsonTokenType.String })))), Expression.Call( Expression.Constant(JsonEncoding.Bytes), getBytes, Expression.Call(context.Reader, getString))), type))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {bytesSchema} to {type}.", exception); } } else { return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonBytesDeserializerBuilderCase)} can only be applied to {nameof(BytesSchema)}s."))); } }
/// <summary> /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="LongSchema" />. /// </summary> /// <returns> /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" /> /// is a <see cref="LongSchema" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <see cref="long" /> cannot be converted to <paramref name="type" />. /// </exception> /// <inheritdoc /> public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context) { if (schema is LongSchema longSchema) { var getInt64 = typeof(Utf8JsonReader) .GetMethod(nameof(Utf8JsonReader.GetInt64), Type.EmptyTypes); try { return(JsonDeserializerBuilderCaseResult.FromExpression( BuildConversion(Expression.Call(context.Reader, getInt64), type))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {longSchema} to {type}.", exception); } } else { return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonLongDeserializerBuilderCase)} can only be applied to {nameof(LongSchema)}s."))); } }
/// <summary> /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="DecimalLogicalType" />. /// </summary> /// <returns> /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" /> /// has a <see cref="DecimalLogicalType" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedSchemaException"> /// Thrown when <paramref name="schema" /> is not a <see cref="BytesSchema" /> or a /// <see cref="FixedSchema "/>. /// </exception> /// <exception cref="UnsupportedTypeException"> /// Thrown when <see cref="decimal" /> cannot be converted to <paramref name="type" />. /// </exception> /// <inheritdoc /> public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context) { if (schema.LogicalType is DecimalLogicalType decimalLogicalType) { var precision = decimalLogicalType.Precision; var scale = decimalLogicalType.Scale; var bytes = Expression.Variable(typeof(byte[])); var tokenType = typeof(Utf8JsonReader) .GetProperty(nameof(Utf8JsonReader.TokenType)); var getUnexpectedTokenException = typeof(JsonExceptionHelper) .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException)); var getString = typeof(Utf8JsonReader) .GetMethod(nameof(Utf8JsonReader.GetString), Type.EmptyTypes); var getBytes = typeof(Encoding) .GetMethod(nameof(Encoding.GetBytes), new[] { typeof(string) }); var getUnexpectedSizeException = typeof(JsonExceptionHelper) .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedSizeException)); Expression expression; if (schema is BytesSchema) { expression = Expression.Block( Expression.IfThen( Expression.NotEqual( Expression.Property(context.Reader, tokenType), Expression.Constant(JsonTokenType.String)), Expression.Throw( Expression.Call( null, getUnexpectedTokenException, context.Reader, Expression.Constant(new[] { JsonTokenType.String })))), Expression.Assign( bytes, Expression.Call( Expression.Constant(JsonEncoding.Bytes), getBytes, Expression.Call(context.Reader, getString))), bytes); } else if (schema is FixedSchema fixedSchema) { expression = Expression.Block( Expression.IfThen( Expression.NotEqual( Expression.Property(context.Reader, tokenType), Expression.Constant(JsonTokenType.String)), Expression.Throw( Expression.Call( null, getUnexpectedTokenException, context.Reader, Expression.Constant(new[] { JsonTokenType.String })))), Expression.Assign( bytes, Expression.Call( Expression.Constant(JsonEncoding.Bytes), getBytes, Expression.Call(context.Reader, getString))), Expression.IfThen( Expression.NotEqual( Expression.ArrayLength(bytes), Expression.Constant(fixedSchema.Size)), Expression.Throw( Expression.Call( null, getUnexpectedSizeException, context.Reader, Expression.Constant(fixedSchema.Size), Expression.ArrayLength(bytes)))), bytes); } else { throw new UnsupportedSchemaException(schema); } // declare variables for in-place transformation: var remainder = Expression.Variable(typeof(BigInteger)); var divide = typeof(BigInteger) .GetMethod(nameof(BigInteger.DivRem), new[] { typeof(BigInteger), typeof(BigInteger), typeof(BigInteger).MakeByRefType() }); var integerConstructor = typeof(BigInteger) .GetConstructor(new[] { typeof(byte[]) }); var reverse = typeof(Array) .GetMethod(nameof(Array.Reverse), new[] { typeof(Array) }); // var bytes = ...; // // // BigInteger is little-endian, so reverse: // Array.Reverse(bytes); // // var whole = BigInteger.DivRem(new BigInteger(bytes), BigInteger.Pow(10, scale), out var remainder); // var fraction = (decimal)remainder / (decimal)Math.Pow(10, scale); // // return whole + fraction; expression = Expression.Block( new[] { bytes, remainder }, Expression.Assign(bytes, expression), Expression.Call(null, reverse, bytes), Expression.Add( Expression.ConvertChecked( Expression.Call( null, divide, Expression.New(integerConstructor, bytes), Expression.Constant(BigInteger.Pow(10, scale)), remainder), typeof(decimal)), Expression.Divide( Expression.ConvertChecked(remainder, typeof(decimal)), Expression.Constant((decimal)Math.Pow(10, scale))))); try { return(JsonDeserializerBuilderCaseResult.FromExpression( BuildConversion(expression, type))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {schema} to {type}.", exception); } } else { return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonDecimalDeserializerBuilderCase)} can only be applied schemas with a {nameof(DecimalLogicalType)}."))); } }
/// <summary> /// Builds a <see cref="JsonDeserializer{T}" /> for an <see cref="ArraySchema" />. /// </summary> /// <returns> /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="type" /> /// is an enumerable type and <paramref name="schema" /> is an <see cref="ArraySchema" />; /// an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" /> otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> is not assignable from any supported array or /// collection <see cref="Type" /> and does not have a constructor that can be used to /// instantiate it. /// </exception> /// <inheritdoc /> public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context) { if (schema is ArraySchema arraySchema) { var enumerableType = GetEnumerableType(type); if (enumerableType is not null || type == typeof(object)) { // support dynamic mapping: var itemType = enumerableType ?? typeof(object); var instantiateCollection = BuildIntermediateCollection(type, itemType); var readItem = DeserializerBuilder .BuildExpression(itemType, arraySchema.Item, context); var collection = Expression.Parameter(instantiateCollection.Type); var loop = Expression.Label(); var tokenType = typeof(Utf8JsonReader) .GetProperty(nameof(Utf8JsonReader.TokenType)); var getUnexpectedTokenException = typeof(JsonExceptionHelper) .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException)); var read = typeof(Utf8JsonReader) .GetMethod(nameof(Utf8JsonReader.Read), Type.EmptyTypes); var add = collection.Type.GetMethod("Add", new[] { readItem.Type }); Expression expression = Expression.Block( new[] { collection }, Expression.IfThen( Expression.NotEqual( Expression.Property(context.Reader, tokenType), Expression.Constant(JsonTokenType.StartArray)), Expression.Throw( Expression.Call( null, getUnexpectedTokenException, context.Reader, Expression.Constant(new[] { JsonTokenType.StartArray })))), Expression.Assign(collection, instantiateCollection), Expression.Loop( Expression.Block( Expression.Call(context.Reader, read), Expression.IfThen( Expression.Equal( Expression.Property(context.Reader, tokenType), Expression.Constant(JsonTokenType.EndArray)), Expression.Break(loop)), Expression.Call(collection, add, readItem)), loop), collection); if (!type.IsAssignableFrom(expression.Type) && GetCollectionConstructor(type) is ConstructorInfo constructor) { expression = Expression.New(constructor, expression); } try { return(JsonDeserializerBuilderCaseResult.FromExpression( BuildConversion(expression, type))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {arraySchema} to {type}.", exception); } } else { return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedTypeException(type, $"{nameof(JsonArrayDeserializerBuilderCase)} can only be applied to enumerable types."))); } }
/// <summary> /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="DurationLogicalType" />. /// </summary> /// <returns> /// A successful result if <paramref name="type" /> the schema’s logical type is a /// <see cref="DurationLogicalType" />; an unsuccessful result otherwise. /// </returns> /// <exception cref="UnsupportedSchemaException"> /// Thrown when the schema is not a <see cref="FixedSchema" /> with size 12 and logical /// type <see cref="DurationLogicalType" />. /// </exception> /// <exception cref="UnsupportedTypeException"> /// Thrown when <see cref="TimeSpan" /> cannot be converted to <paramref name="type" />. /// </exception> /// <inheritdoc /> public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context) { if (schema.LogicalType is DurationLogicalType) { if (!(schema is FixedSchema fixedSchema && fixedSchema.Size == 12)) { throw new UnsupportedSchemaException(schema); } var bytes = Expression.Parameter(typeof(byte[])); var copy = typeof(Array) .GetMethod(nameof(Array.Copy), new[] { typeof(Array), typeof(int), typeof(Array), typeof(int), typeof(int) }); var tokenType = typeof(Utf8JsonReader) .GetProperty(nameof(Utf8JsonReader.TokenType)); var getUnexpectedTokenException = typeof(JsonExceptionHelper) .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException)); var getString = typeof(Utf8JsonReader) .GetMethod(nameof(Utf8JsonReader.GetString), Type.EmptyTypes); var getBytes = typeof(Encoding) .GetMethod(nameof(Encoding.GetBytes), new[] { typeof(string) }); var getUnexpectedSizeException = typeof(JsonExceptionHelper) .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedSizeException)); var reverse = typeof(Array) .GetMethod(nameof(Array.Reverse), new[] { typeof(Array) }); var toUInt32 = typeof(BitConverter) .GetMethod(nameof(BitConverter.ToUInt32), new[] { typeof(byte[]), typeof(int) }); Expression Read(Expression offset) { var component = Expression.Variable(typeof(byte[])); var expressions = new List <Expression> { Expression.Assign( component, Expression.NewArrayBounds(typeof(byte), Expression.Constant(4))), Expression.Call(null, copy, bytes, offset, component, Expression.Constant(0), Expression.ArrayLength(component)), }; if (!BitConverter.IsLittleEndian) { expressions.Add(Expression.Call(null, reverse, Expression.Convert(component, typeof(Array)))); } expressions.Add(component); return(Expression.ConvertChecked( Expression.Call( null, toUInt32, Expression.Block( new[] { component }, expressions), Expression.Constant(0)), typeof(long))); } var exceptionConstructor = typeof(OverflowException) .GetConstructor(new[] { typeof(string) }); var timeSpanConstructor = typeof(TimeSpan) .GetConstructor(new[] { typeof(long) }); try { return(JsonDeserializerBuilderCaseResult.FromExpression( BuildConversion( Expression.Block( new[] { bytes }, Expression.IfThen( Expression.NotEqual( Expression.Property(context.Reader, tokenType), Expression.Constant(JsonTokenType.String)), Expression.Throw( Expression.Call( null, getUnexpectedTokenException, context.Reader, Expression.Constant(new[] { JsonTokenType.String })))), Expression.Assign( bytes, Expression.Call( Expression.Constant(JsonEncoding.Bytes), getBytes, Expression.Call(context.Reader, getString))), Expression.IfThen( Expression.NotEqual( Expression.ArrayLength(bytes), Expression.Constant(fixedSchema.Size)), Expression.Throw( Expression.Call( null, getUnexpectedSizeException, context.Reader, Expression.Constant(fixedSchema.Size), Expression.ArrayLength(bytes)))), Expression.IfThen( Expression.NotEqual(Read(Expression.Constant(0)), Expression.Constant(0L)), Expression.Throw( Expression.New( exceptionConstructor, Expression.Constant("Durations containing months cannot be accurately deserialized to a TimeSpan.")))), Expression.New( timeSpanConstructor, Expression.AddChecked( Expression.MultiplyChecked(Read(Expression.Constant(4)), Expression.Constant(TimeSpan.TicksPerDay)), Expression.MultiplyChecked(Read(Expression.Constant(8)), Expression.Constant(TimeSpan.TicksPerMillisecond))))), type))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {schema} to {type}.", exception); } } else { return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonDurationDeserializerBuilderCase)} can only be applied schemas with a {nameof(DurationLogicalType)}."))); } }
/// <summary> /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="MapSchema" />. /// </summary> /// <returns> /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="type" /> /// is a dictionary type and <paramref name="schema" /> is a <see cref="MapSchema" />; an /// unsuccessful <see cref="JsonDeserializerBuilderCaseResult" /> otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> is not assignable from any supported dictionary /// <see cref="Type" /> and does not have a constructor that can be used to instantiate it. /// </exception> /// <inheritdoc /> public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context) { if (schema is MapSchema mapSchema) { var dictionaryTypes = GetDictionaryTypes(type); if (dictionaryTypes is not null || type == typeof(object)) { // support dynamic mapping: var keyType = dictionaryTypes?.Key ?? typeof(string); var valueType = dictionaryTypes?.Value ?? typeof(object); var instantiateDictionary = BuildIntermediateDictionary(type, keyType, valueType); var readKey = DeserializerBuilder .BuildExpression(keyType, new StringSchema(), context); var readValue = DeserializerBuilder .BuildExpression(valueType, mapSchema.Value, context); var dictionary = Expression.Parameter(instantiateDictionary.Type); var key = Expression.Parameter(readKey.Type); var loop = Expression.Label(); var tokenType = typeof(Utf8JsonReader) .GetProperty(nameof(Utf8JsonReader.TokenType)); var getUnexpectedTokenException = typeof(JsonExceptionHelper) .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException)); var read = typeof(Utf8JsonReader) .GetMethod(nameof(Utf8JsonReader.Read), Type.EmptyTypes); var add = dictionary.Type.GetMethod("Add", new[] { readKey.Type, readValue.Type }); Expression expression = Expression.Block( new[] { dictionary }, Expression.IfThen( Expression.NotEqual( Expression.Property(context.Reader, tokenType), Expression.Constant(JsonTokenType.StartObject)), Expression.Throw( Expression.Call( null, getUnexpectedTokenException, context.Reader, Expression.Constant(new[] { JsonTokenType.StartObject })))), Expression.Assign(dictionary, instantiateDictionary), Expression.Loop( Expression.Block( new[] { key }, Expression.Call(context.Reader, read), Expression.IfThen( Expression.Equal( Expression.Property(context.Reader, tokenType), Expression.Constant(JsonTokenType.EndObject)), Expression.Break(loop)), Expression.Assign(key, readKey), Expression.Call(context.Reader, read), Expression.Call(dictionary, add, key, readValue)), loop), dictionary); if (!type.IsAssignableFrom(expression.Type) && GetDictionaryConstructor(type) is ConstructorInfo constructor) { expression = Expression.New(constructor, expression); } try { return(JsonDeserializerBuilderCaseResult.FromExpression( BuildConversion(expression, type))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {mapSchema} to {type}.", exception); } } else { return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedTypeException(type, $"{nameof(JsonMapDeserializerBuilderCase)} can only be applied to dictionary types."))); } }
/// <summary> /// Builds a <see cref="JsonDeserializer{T}" /> for an <see cref="EnumSchema" />. /// </summary> /// <returns> /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" /> /// is an <see cref="EnumSchema" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> is an enum type without a matching member for each /// symbol in <paramref name="schema" /> or when <see cref="string" /> cannot be converted /// to <paramref name="type" />. /// </exception> /// <inheritdoc /> public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context) { if (schema is EnumSchema enumSchema) { var getString = typeof(Utf8JsonReader) .GetMethod(nameof(Utf8JsonReader.GetString), Type.EmptyTypes); Expression expression = Expression.Call(context.Reader, getString); var underlying = Nullable.GetUnderlyingType(type) ?? type; // enum fields will always be public static, so no need to expose binding flags: var fields = underlying.GetFields(BindingFlags.Public | BindingFlags.Static); // find a match for each enum in the schema: var cases = underlying.IsEnum ? enumSchema.Symbols .Select(symbol => { var match = fields.SingleOrDefault(field => IsMatch(symbol, field)); if (enumSchema.Default != null) { match ??= fields.SingleOrDefault(field => IsMatch(enumSchema.Default, field)); } if (match == null) { throw new UnsupportedTypeException(type, $"{type} has no value that matches {symbol} and no default value is defined."); } return(Expression.SwitchCase( BuildConversion(Expression.Constant(Enum.Parse(underlying, match.Name)), type), Expression.Constant(symbol))); }) : enumSchema.Symbols .Select(symbol => { return(Expression.SwitchCase( BuildConversion(Expression.Constant(symbol), type), Expression.Constant(symbol))); }); var position = typeof(Utf8JsonReader) .GetProperty(nameof(Utf8JsonReader.TokenStartIndex)) .GetGetMethod(); var exceptionConstructor = typeof(InvalidEncodingException) .GetConstructor(new[] { typeof(long), typeof(string), typeof(Exception) }); // generate a switch on the symbol: return(JsonDeserializerBuilderCaseResult.FromExpression( Expression.Switch( expression, Expression.Throw( Expression.New( exceptionConstructor, Expression.Property(context.Reader, position), Expression.Constant($"Invalid enum symbol."), Expression.Constant(null, typeof(Exception))), type), cases.ToArray()))); } else { return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonEnumDeserializerBuilderCase)} can only be applied to {nameof(EnumSchema)}s."))); } }
/// <summary> /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="UnionSchema" />. /// </summary> /// <returns> /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" /> /// is a <see cref="UnionSchema" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedSchemaException"> /// Thrown when <paramref name="schema" /> has no <see cref="UnionSchema.Schemas" />. /// </exception> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> cannot be mapped to each <see cref="Schema" /> in /// <paramref name="schema" />. /// </exception> /// <inheritdoc /> public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context) { if (schema is UnionSchema unionSchema) { if (unionSchema.Schemas.Count < 1) { throw new UnsupportedSchemaException(schema, "A deserializer cannot be built for an empty union."); } var tokenType = typeof(Utf8JsonReader) .GetProperty(nameof(Utf8JsonReader.TokenType)); var getUnexpectedTokenException = typeof(JsonExceptionHelper) .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException)); var read = typeof(Utf8JsonReader) .GetMethod(nameof(Utf8JsonReader.Read), Type.EmptyTypes); var getString = typeof(Utf8JsonReader) .GetMethod(nameof(Utf8JsonReader.GetString), Type.EmptyTypes); var getUnknownUnionMemberException = typeof(JsonExceptionHelper) .GetMethod(nameof(JsonExceptionHelper.GetUnknownUnionMemberException)); var schemas = unionSchema.Schemas.ToList(); var candidates = schemas.Where(s => s is not NullSchema).ToList(); var @null = schemas.Find(s => s is NullSchema); var cases = candidates.Select(child => { var selected = SelectType(type, child); return(Expression.SwitchCase( BuildConversion( Expression.Block( Expression.Call(context.Reader, read), DeserializerBuilder.BuildExpression(selected, child, context)), type), Expression.Constant(GetSchemaName(child)))); }).ToArray(); var value = Expression.Parameter(type); Expression expression = Expression.Block( new[] { value }, Expression.IfThen( Expression.NotEqual( Expression.Property(context.Reader, tokenType), Expression.Constant(JsonTokenType.StartObject)), Expression.Throw( Expression.Call( null, getUnexpectedTokenException, context.Reader, Expression.Constant(new[] { JsonTokenType.StartObject })))), Expression.Call(context.Reader, read), Expression.Assign( value, Expression.Switch( Expression.Call(context.Reader, getString), Expression.Throw( Expression.Call( null, getUnknownUnionMemberException, context.Reader), type), cases)), Expression.Call(context.Reader, read), Expression.IfThen( Expression.NotEqual( Expression.Property(context.Reader, tokenType), Expression.Constant(JsonTokenType.EndObject)), Expression.Throw( Expression.Call( null, getUnexpectedTokenException, context.Reader, Expression.Constant(new[] { JsonTokenType.EndObject })))), value); if (@null != null) { var selected = SelectType(type, @null); var underlying = Nullable.GetUnderlyingType(selected); if (selected.IsValueType && underlying == null) { throw new UnsupportedTypeException(type, $"A deserializer for a union containing {typeof(NullSchema)} cannot be built for {selected}."); } expression = Expression.Condition( Expression.Equal( Expression.Property(context.Reader, tokenType), Expression.Constant(JsonTokenType.Null)), BuildConversion( DeserializerBuilder.BuildExpression(selected, @null, context), type), expression); } return(JsonDeserializerBuilderCaseResult.FromExpression(expression)); } else { return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonUnionDeserializerBuilderCase)} can only be applied to {nameof(UnionSchema)}s."))); } }