/// <exception cref="UnsupportedTypeException">
        /// Thrown when no case can map the <see cref="Expression.Type" /> of <paramref name="value" />
        /// to <paramref name="schema" />.
        /// </exception>
        /// <inheritdoc />
        public virtual Expression BuildExpression(Expression value, Schema schema, JsonSerializerBuilderContext context)
        {
            var exceptions = new List <Exception>();

            foreach (var @case in Cases)
            {
                var result = @case.BuildExpression(value, value.Type, schema, context);

                if (result.Expression != null)
                {
                    return(result.Expression);
                }

                exceptions.AddRange(result.Exceptions);
            }

            throw new UnsupportedTypeException(value.Type, $"No serializer builder case could be applied to {value.Type}.", new AggregateException(exceptions));
        }
        /// <summary>
        /// Builds a <see cref="JsonSerializer{T}" /> for a <see cref="LongSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="schema" />
        /// is a <see cref="LongSchema" />; an unsuccessful <see cref="JsonSerializerBuilderCaseResult" />
        /// otherwise.
        /// </returns>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <paramref name="type" /> cannot be converted to <see cref="long" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext context)
        {
            if (schema is LongSchema longSchema)
            {
                var writeNumber = typeof(Utf8JsonWriter)
                                  .GetMethod(nameof(Utf8JsonWriter.WriteNumberValue), new[] { typeof(long) });

                try
                {
                    return(JsonSerializerBuilderCaseResult.FromExpression(
                               Expression.Call(context.Writer, writeNumber, BuildConversion(value, typeof(long)))));
                }
                catch (InvalidOperationException exception)
                {
                    throw new UnsupportedTypeException(type, $"Failed to map {longSchema} to {type}.", exception);
                }
            }
            else
            {
                return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonLongSerializerBuilderCase)} can only be applied to {nameof(LongSchema)}s.")));
            }
        }
Ejemplo n.º 3
0
        /// <summary>
        /// Builds a <see cref="JsonSerializer{T}" /> for an <see cref="FixedSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonSerializerBuilderCaseResult" /> <paramref name="schema" />
        /// is a <see cref="FixedSchema" />; an unsuccessful <see cref="JsonSerializerBuilderCaseResult" />
        /// otherwise.
        /// </returns>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <paramref name="type" /> cannot be converted to <see cref="T:System.Byte[]" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext context)
        {
            if (schema is FixedSchema fixedSchema)
            {
                Expression expression;

                try
                {
                    expression = BuildConversion(value, typeof(byte[]));
                }
                catch (InvalidOperationException exception)
                {
                    throw new UnsupportedTypeException(type, $"Failed to map {fixedSchema} to {type}.", exception);
                }

                var bytes = Expression.Parameter(typeof(byte[]));
                var chars = Expression.Parameter(typeof(char[]));

                var exceptionConstructor = typeof(OverflowException)
                                           .GetConstructor(new[] { typeof(string) });

                var copyTo = typeof(Array)
                             .GetMethod(nameof(Array.CopyTo), new[] { typeof(Array), typeof(int) });

                var encode = typeof(JsonEncodedText)
                             .GetMethod(nameof(JsonEncodedText.Encode), new[] { typeof(ReadOnlySpan <char>), typeof(JavaScriptEncoder) });

                var writeString = typeof(Utf8JsonWriter)
                                  .GetMethod(nameof(Utf8JsonWriter.WriteStringValue), new[] { typeof(JsonEncodedText) });

                return(JsonSerializerBuilderCaseResult.FromExpression(
                           Expression.Block(
                               new[] { bytes, chars },
                               Expression.Assign(bytes, expression),
                               Expression.IfThen(
                                   Expression.NotEqual(
                                       Expression.ArrayLength(bytes),
                                       Expression.Constant(fixedSchema.Size)),
                                   Expression.Throw(Expression.New(exceptionConstructor, Expression.Constant($"Only arrays of size {fixedSchema.Size} can be serialized to {fixedSchema.Name}.")))),
                               Expression.Assign(chars, Expression.NewArrayBounds(typeof(char), Expression.ArrayLength(bytes))),
                               Expression.Call(bytes, copyTo, chars, Expression.Constant(0)),
                               Expression.Call(
                                   context.Writer,
                                   writeString,
                                   Expression.Call(
                                       null,
                                       encode,
                                       Expression.Convert(chars, typeof(ReadOnlySpan <char>)),
                                       Expression.Constant(JsonEncoder.Bytes))))));
            }
            else
            {
                return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonFixedSerializerBuilderCase)} can only be applied to {nameof(FixedSchema)}s.")));
            }
        }
Ejemplo n.º 4
0
        /// <summary>
        /// Builds a <see cref="JsonSerializer{T}" /> for an <see cref="ArraySchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="type" />
        /// is an enumerable type and <paramref name="schema" /> is an <see cref="ArraySchema" />;
        /// an unsuccessful <see cref="JsonSerializerBuilderCaseResult" /> otherwise.
        /// </returns>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <paramref name="type" /> does not implement <see cref="IEnumerable{T}" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext 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 enumerable = Expression.Variable(typeof(IEnumerable <>).MakeGenericType(itemType));
                    var enumerator = Expression.Variable(typeof(IEnumerator <>).MakeGenericType(itemType));
                    var loop       = Expression.Label();

                    var writeStartArray = typeof(Utf8JsonWriter)
                                          .GetMethod(nameof(Utf8JsonWriter.WriteStartArray), Type.EmptyTypes);

                    var getEnumerator = enumerable.Type
                                        .GetMethod("GetEnumerator", Type.EmptyTypes);

                    var getCurrent = enumerator.Type
                                     .GetProperty(nameof(IEnumerator.Current))
                                     .GetGetMethod();

                    var moveNext = typeof(IEnumerator)
                                   .GetMethod(nameof(IEnumerator.MoveNext), Type.EmptyTypes);

                    var writeItem = SerializerBuilder
                                    .BuildExpression(Expression.Property(enumerator, getCurrent), arraySchema.Item, context);

                    var writeEndArray = typeof(Utf8JsonWriter)
                                        .GetMethod(nameof(Utf8JsonWriter.WriteEndArray), Type.EmptyTypes);

                    var dispose = typeof(IDisposable)
                                  .GetMethod(nameof(IDisposable.Dispose), Type.EmptyTypes);

                    Expression expression;

                    try
                    {
                        expression = BuildConversion(value, enumerable.Type);
                    }
                    catch (Exception exception)
                    {
                        throw new UnsupportedTypeException(type, $"Failed to map {arraySchema} to {type}.", exception);
                    }

                    return(JsonSerializerBuilderCaseResult.FromExpression(
                               Expression.Block(
                                   new[] { enumerator },
                                   Expression.Call(context.Writer, writeStartArray),
                                   Expression.Assign(
                                       enumerator,
                                       Expression.Call(expression, getEnumerator)),
                                   Expression.TryFinally(
                                       Expression.Loop(
                                           Expression.IfThenElse(
                                               Expression.Call(enumerator, moveNext),
                                               writeItem,
                                               Expression.Break(loop)),
                                           loop),
                                       Expression.Call(enumerator, dispose)),
                                   Expression.Call(context.Writer, writeEndArray))));
                }
                else
                {
                    return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedTypeException(type, $"{nameof(JsonArraySerializerBuilderCase)} can only be applied to enumerable types.")));
                }
            }
        /// <summary>
        /// Builds a <see cref="JsonSerializer{T}" /> for a <see cref="DecimalLogicalType" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="schema" />
        /// has a <see cref="DecimalLogicalType" />; an unsuccessful <see cref="JsonSerializerBuilderCaseResult" />
        /// 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 <paramref name="type" /> cannot be converted to <see cref="decimal" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext context)
        {
            if (schema.LogicalType is DecimalLogicalType decimalLogicalType)
            {
                var precision = decimalLogicalType.Precision;
                var scale     = decimalLogicalType.Scale;

                Expression expression;

                try
                {
                    expression = BuildConversion(value, typeof(decimal));
                }
                catch (InvalidOperationException exception)
                {
                    throw new UnsupportedTypeException(type, $"Failed to map {schema} to {type}.", exception);
                }

                // declare variables for in-place transformation:
                var bytes = Expression.Variable(typeof(byte[]));
                var chars = Expression.Parameter(typeof(char[]));

                var integerConstructor = typeof(BigInteger)
                                         .GetConstructor(new[] { typeof(decimal) });

                var reverse = typeof(Array)
                              .GetMethod(nameof(Array.Reverse), new[] { typeof(Array) });

                var toByteArray = typeof(BigInteger)
                                  .GetMethod(nameof(BigInteger.ToByteArray), Type.EmptyTypes);

                var copyTo = typeof(Array)
                             .GetMethod(nameof(Array.CopyTo), new[] { typeof(Array), typeof(int) });

                // var fraction = new BigInteger(...) * BigInteger.Pow(10, scale);
                // var whole = new BigInteger((... % 1m) * (decimal)Math.Pow(10, scale));
                // var bytes = (fraction + whole).ToByteArray();
                //
                // // BigInteger is little-endian, so reverse:
                // Array.Reverse(bytes);
                //
                // var chars = new char[bytes.Length];
                // bytes.CopyTo(chars, 0);
                expression = Expression.Block(
                    Expression.Assign(
                        bytes,
                        Expression.Call(
                            Expression.Add(
                                Expression.Multiply(
                                    Expression.New(
                                        integerConstructor,
                                        expression),
                                    Expression.Constant(BigInteger.Pow(10, scale))),
                                Expression.New(
                                    integerConstructor,
                                    Expression.Multiply(
                                        Expression.Modulo(expression, Expression.Constant(1m)),
                                        Expression.Constant((decimal)Math.Pow(10, scale))))),
                            toByteArray)),
                    Expression.Call(null, reverse, bytes),
                    Expression.Assign(chars, Expression.NewArrayBounds(typeof(char), Expression.ArrayLength(bytes))),
                    Expression.Call(bytes, copyTo, chars, Expression.Constant(0)));

                var encode = typeof(JsonEncodedText)
                             .GetMethod(nameof(JsonEncodedText.Encode), new[] { typeof(ReadOnlySpan <char>), typeof(JavaScriptEncoder) });

                var writeString = typeof(Utf8JsonWriter)
                                  .GetMethod(nameof(Utf8JsonWriter.WriteStringValue), new[] { typeof(JsonEncodedText) });

                // figure out how to write:
                if (schema is BytesSchema)
                {
                    expression = Expression.Block(
                        new[] { bytes, chars },
                        expression,
                        Expression.Call(
                            context.Writer,
                            writeString,
                            Expression.Call(
                                null,
                                encode,
                                Expression.Convert(chars, typeof(ReadOnlySpan <char>)),
                                Expression.Constant(JsonEncoder.Bytes))));
                }
                else if (schema is FixedSchema fixedSchema)
                {
                    var exceptionConstructor = typeof(OverflowException)
                                               .GetConstructor(new[] { typeof(string) });

                    expression = Expression.Block(
                        new[] { bytes, chars },
                        expression,
                        Expression.IfThen(
                            Expression.NotEqual(Expression.ArrayLength(bytes), Expression.Constant(fixedSchema.Size)),
                            Expression.Throw(Expression.New(exceptionConstructor, Expression.Constant($"Size mismatch between {fixedSchema.Name} (size {fixedSchema.Size}) and decimal with precision {precision} and scale {scale}.")))),
                        Expression.Call(
                            context.Writer,
                            writeString,
                            Expression.Call(
                                null,
                                encode,
                                Expression.Convert(chars, typeof(ReadOnlySpan <char>)),
                                Expression.Constant(JsonEncoder.Bytes))));
                }
                else
                {
                    throw new UnsupportedSchemaException(schema);
                }

                return(JsonSerializerBuilderCaseResult.FromExpression(expression));
            }
            else
            {
                return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonDecimalSerializerBuilderCase)} can only be applied schemas with a {nameof(DecimalLogicalType)}.")));
            }
        }
        /// <summary>
        /// Builds a <see cref="JsonSerializer{T}" /> for a <see cref="NullSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="schema" />
        /// is a <see cref="NullSchema" />; an unsuccessful <see cref="JsonSerializerBuilderCaseResult" />
        /// otherwise.
        /// </returns>
        /// <inheritdoc />
        public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext context)
        {
            if (schema is NullSchema)
            {
                var writeNull = typeof(Utf8JsonWriter)
                                .GetMethod(nameof(Utf8JsonWriter.WriteNullValue), Type.EmptyTypes);

                return(JsonSerializerBuilderCaseResult.FromExpression(
                           Expression.Call(context.Writer, writeNull)));
            }
            else
            {
                return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonNullSerializerBuilderCase)} can only be applied to {nameof(NullSchema)}s.")));
            }
        }
        /// <summary>
        /// Builds a <see cref="JsonSerializer{T}" /> for a <see cref="BytesSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="schema" />
        /// is a <see cref="BytesSchema" />; an unsuccessful <see cref="JsonSerializerBuilderCaseResult" />
        /// otherwise.
        /// </returns>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <paramref name="type" /> cannot be converted to <see cref="T:System.Byte[]" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext context)
        {
            if (schema is BytesSchema bytesSchema)
            {
                var bytes = Expression.Parameter(typeof(byte[]));
                var chars = Expression.Parameter(typeof(char[]));

                var copyTo = typeof(Array)
                             .GetMethod(nameof(Array.CopyTo), new[] { typeof(Array), typeof(int) });

                var encode = typeof(JsonEncodedText)
                             .GetMethod(nameof(JsonEncodedText.Encode), new[] { typeof(ReadOnlySpan <char>), typeof(JavaScriptEncoder) });

                var writeString = typeof(Utf8JsonWriter)
                                  .GetMethod(nameof(Utf8JsonWriter.WriteStringValue), new[] { typeof(JsonEncodedText) });

                try
                {
                    return(JsonSerializerBuilderCaseResult.FromExpression(
                               Expression.Block(
                                   new[] { bytes, chars },
                                   Expression.Assign(bytes, BuildConversion(value, typeof(byte[]))),
                                   Expression.Assign(chars, Expression.NewArrayBounds(typeof(char), Expression.ArrayLength(bytes))),
                                   Expression.Call(bytes, copyTo, chars, Expression.Constant(0)),
                                   Expression.Call(
                                       context.Writer,
                                       writeString,
                                       Expression.Call(
                                           null,
                                           encode,
                                           Expression.Convert(chars, typeof(ReadOnlySpan <char>)),
                                           Expression.Constant(JsonEncoder.Bytes))))));
                }
                catch (InvalidOperationException exception)
                {
                    throw new UnsupportedTypeException(type, $"Failed to map {bytesSchema} to {type}.", exception);
                }
            }
            else
            {
                return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonBytesSerializerBuilderCase)} can only be applied to {nameof(BytesSchema)}s.")));
            }
        }
        /// <summary>
        /// Builds a <see cref="JsonSerializer{T}" /> for a <see cref="RecordSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="type" />
        /// is not an array or primitive type and <paramref name="schema" /> is a <see cref="RecordSchema" />;
        /// an unsuccessful <see cref="JsonSerializerBuilderCaseResult" /> otherwise.
        /// </returns>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <paramref name="type" /> does not have a matching member for each
        /// <see cref="RecordField" /> on <paramref name="schema" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext context)
        {
            if (schema is RecordSchema recordSchema)
            {
                if (!type.IsArray && !type.IsPrimitive)
                {
                    // since record serialization is potentially recursive, create a top-level
                    // reference:
                    var parameter = Expression.Parameter(
                        Expression.GetDelegateType(type, context.Writer.Type, typeof(void)));

                    if (!context.References.TryGetValue((recordSchema, type), out var reference))
                    {
                        context.References.Add((recordSchema, type), reference = parameter);
                    }

                    // then build/set the delegate if it hasn’t been built yet:
                    if (parameter == reference)
                    {
                        var members = type.GetMembers(MemberVisibility);

                        var writeStartObject = typeof(Utf8JsonWriter)
                                               .GetMethod(nameof(Utf8JsonWriter.WriteStartObject), Type.EmptyTypes);

                        var writePropertyName = typeof(Utf8JsonWriter)
                                                .GetMethod(nameof(Utf8JsonWriter.WritePropertyName), new[] { typeof(string) });

                        var writeEndObject = typeof(Utf8JsonWriter)
                                             .GetMethod(nameof(Utf8JsonWriter.WriteEndObject), Type.EmptyTypes);

                        var argument = Expression.Variable(type);
                        var writes   = new List <Expression>
                        {
                            Expression.Call(context.Writer, writeStartObject),
                        };

                        foreach (var field in recordSchema.Fields)
                        {
                            var match = members.SingleOrDefault(member => IsMatch(field, member));

                            Expression inner;

                            if (match == null)
                            {
                                // if the type could be dynamic, attempt to use a dynamic getter:
                                if (typeof(IDynamicMetaObjectProvider).IsAssignableFrom(type) || type == typeof(object))
                                {
                                    var flags  = CSharpBinderFlags.None;
                                    var infos  = new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) };
                                    var binder = Binder.GetMember(flags, field.Name, type, infos);
                                    inner = Expression.Dynamic(binder, typeof(object), value);
                                }
                                else
                                {
                                    if (field.Default is not null)
                                    {
                                        inner = Expression.Constant(field.Default.ToObject <dynamic>());
                                    }
                                    else
                                    {
                                        throw new UnsupportedTypeException(type, $"{type} does not have a field or property that matches the {field.Name} field on {recordSchema.FullName}.");
                                    }
                                }
                            }
                            else
                            {
                                inner = Expression.PropertyOrField(argument, match.Name);
                            }

                            try
                            {
                                writes.Add(Expression.Call(context.Writer, writePropertyName, Expression.Constant(field.Name)));
                                writes.Add(SerializerBuilder.BuildExpression(inner, field.Type, context));
                            }
                            catch (Exception exception)
                            {
                                throw new UnsupportedTypeException(type, $"{(match is null ? "A" : $"The {match.Name}")} member on {type} could not be mapped to the {field.Name} field on {recordSchema.FullName}.", exception);
                            }
                        }

                        writes.Add(Expression.Call(context.Writer, writeEndObject));

                        var expression = Expression.Lambda(
                            parameter.Type,
                            Expression.Block(writes),
                            $"{recordSchema.Name} serializer",
                            new[] { argument, context.Writer });

                        context.Assignments.Add(reference, expression);
                    }

                    return(JsonSerializerBuilderCaseResult.FromExpression(
                               Expression.Invoke(reference, value, context.Writer)));
                }
                else
                {
                    return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedTypeException(type, $"{nameof(JsonRecordSerializerBuilderCase)} cannot be applied to array or primitive types.")));
                }
            }
        /// <summary>
        /// Builds a <see cref="JsonSerializer{T}" /> for a <see cref="TimestampLogicalType" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="schema" />
        /// has a <see cref="TimestampLogicalType" />; an unsuccessful <see cref="JsonSerializerBuilderCaseResult" />
        /// 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 <paramref name="type" /> cannot be converted to <see cref="DateTimeOffset" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext context)
        {
            if (schema.LogicalType is TimestampLogicalType)
            {
                if (schema is not LongSchema)
                {
                    throw new UnsupportedSchemaException(schema);
                }

                Expression expression;

                try
                {
                    expression = BuildConversion(value, typeof(DateTimeOffset));
                }
                catch (InvalidOperationException exception)
                {
                    throw new UnsupportedTypeException(type, $"Failed to map {schema} to {type}.", exception);
                }

                var factor = schema.LogicalType switch
                {
                    MicrosecondTimestampLogicalType => TimeSpan.TicksPerMillisecond / 1000,
                    MillisecondTimestampLogicalType => TimeSpan.TicksPerMillisecond,
                    _ => throw new UnsupportedSchemaException(schema, $"{schema.LogicalType} is not a supported {nameof(TimestampLogicalType)}."),
                };

                var utcTicks = typeof(DateTimeOffset)
                               .GetProperty(nameof(DateTimeOffset.UtcTicks));

                var writeNumber = typeof(Utf8JsonWriter)
                                  .GetMethod(nameof(Utf8JsonWriter.WriteNumberValue), new[] { typeof(long) });

                // return writer.WriteNumber((value.UtcTicks - epoch) / factor);
                return(JsonSerializerBuilderCaseResult.FromExpression(
                           Expression.Call(
                               context.Writer,
                               writeNumber,
                               Expression.Divide(
                                   Expression.Subtract(
                                       Expression.Property(expression, utcTicks),
                                       Expression.Constant(Epoch.Ticks)),
                                   Expression.Constant(factor)))));
            }
            else
            {
                return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonTimestampSerializerBuilderCase)} can only be applied schemas with a {nameof(TimestampLogicalType)}.")));
            }
        }
        /// <summary>
        /// Builds a <see cref="JsonSerializer{T}" /> for a <see cref="UnionSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="schema" />
        /// is a <see cref="UnionSchema" />; an unsuccessful <see cref="JsonSerializerBuilderCaseResult" />
        /// 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 at least one <see cref="Schema" />
        /// in <paramref name="schema" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext context)
        {
            if (schema is UnionSchema unionSchema)
            {
                if (unionSchema.Schemas.Count < 1)
                {
                    throw new UnsupportedSchemaException(schema);
                }

                var schemas    = unionSchema.Schemas.ToList();
                var candidates = schemas.Where(s => s is not NullSchema).ToList();
                var @null      = schemas.Find(s => s is NullSchema);

                var writeNull = typeof(Utf8JsonWriter)
                                .GetMethod(nameof(Utf8JsonWriter.WriteNullValue), Type.EmptyTypes);

                Expression expression = null !;

                // if there are non-null schemas, select the first matching one for each possible type:
                if (candidates.Count > 0)
                {
                    var cases      = new Dictionary <Type, Expression>();
                    var exceptions = new List <Exception>();

                    var writeStartObject = typeof(Utf8JsonWriter)
                                           .GetMethod(nameof(Utf8JsonWriter.WriteStartObject), Type.EmptyTypes);

                    var writePropertyName = typeof(Utf8JsonWriter)
                                            .GetMethod(nameof(Utf8JsonWriter.WritePropertyName), new[] { typeof(string) });

                    var writeEndObject = typeof(Utf8JsonWriter)
                                         .GetMethod(nameof(Utf8JsonWriter.WriteEndObject), Type.EmptyTypes);

                    foreach (var candidate in candidates)
                    {
                        var selected = SelectType(type, candidate);

                        if (cases.ContainsKey(selected))
                        {
                            continue;
                        }

                        var underlying = Nullable.GetUnderlyingType(selected) ?? selected;

                        Expression body;

                        try
                        {
                            body = Expression.Block(
                                Expression.Call(context.Writer, writeStartObject),
                                Expression.Call(context.Writer, writePropertyName, Expression.Constant(GetSchemaName(candidate))),
                                SerializerBuilder.BuildExpression(Expression.Convert(value, underlying), candidate, context),
                                Expression.Call(context.Writer, writeEndObject));
                        }
                        catch (Exception exception)
                        {
                            exceptions.Add(exception);
                            continue;
                        }

                        if (@null != null && !(selected.IsValueType && Nullable.GetUnderlyingType(selected) == null))
                        {
                            body = Expression.IfThenElse(
                                Expression.Equal(value, Expression.Constant(null, selected)),
                                Expression.Call(context.Writer, writeNull),
                                body);
                        }

                        cases.Add(selected, body);
                    }

                    if (cases.Count == 0)
                    {
                        throw new UnsupportedTypeException(
                                  type,
                                  $"{type.Name} does not match any non-null members of {unionSchema}.",
                                  new AggregateException(exceptions));
                    }

                    if (cases.Count == 1 && cases.First() is var first && first.Key == type)
                    {
                        expression = first.Value;
                    }
                    else
                    {
                        var exceptionConstructor = typeof(InvalidOperationException)
                                                   .GetConstructor(new[] { typeof(string) });

                        expression = Expression.Throw(Expression.New(
                                                          exceptionConstructor,
                                                          Expression.Constant($"Unexpected type encountered serializing to {type}.")));

                        foreach (var @case in cases)
                        {
                            expression = Expression.IfThenElse(
                                Expression.TypeIs(value, @case.Key),
                                @case.Value,
                                expression);
                        }
                    }
                }

                // otherwise, we know that the schema is just ["null"]:
                else
                {
                    expression = Expression.Call(context.Writer, writeNull);
                }

                return(JsonSerializerBuilderCaseResult.FromExpression(expression));
            }
Ejemplo n.º 11
0
        /// <summary>
        /// Builds a <see cref="JsonSerializer{T}" /> for a <see cref="DurationLogicalType" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="schema" />
        /// has a <see cref="DurationLogicalType" />; an unsuccessful <see cref="JsonSerializerBuilderCaseResult" />
        /// otherwise.
        /// </returns>
        /// <exception cref="UnsupportedSchemaException">
        /// Thrown when <paramref name="schema" /> is not a <see cref="FixedSchema" /> with size
        /// <c>12</c>.
        /// </exception>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <paramref name="type" /> cannot be converted to <see cref="TimeSpan" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext context)
        {
            if (schema.LogicalType is DurationLogicalType)
            {
                if (!(schema is FixedSchema fixedSchema && fixedSchema.Size == 12))
                {
                    throw new UnsupportedSchemaException(schema);
                }

                var chars = Expression.Parameter(typeof(char[]));

                var getBytes = typeof(BitConverter)
                               .GetMethod(nameof(BitConverter.GetBytes), new[] { typeof(uint) });

                var reverse = typeof(Array)
                              .GetMethod(nameof(Array.Reverse), new[] { getBytes.ReturnType });

                var copyTo = typeof(Array)
                             .GetMethod(nameof(Array.CopyTo), new[] { typeof(Array), typeof(int) });

                Expression Write(Expression value, Expression offset)
                {
                    Expression component = Expression.Call(null, getBytes, value);

                    if (!BitConverter.IsLittleEndian)
                    {
                        var buffer = Expression.Variable(component.Type);

                        component = Expression.Block(
                            new[] { buffer },
                            Expression.Assign(buffer, component),
                            Expression.Call(null, reverse, buffer),
                            buffer);
                    }

                    return(Expression.Call(component, copyTo, chars, offset));
                }

                var totalDays = typeof(TimeSpan).GetProperty(nameof(TimeSpan.TotalDays));
                var totalMs   = typeof(TimeSpan).GetProperty(nameof(TimeSpan.TotalMilliseconds));

                var encode = typeof(JsonEncodedText)
                             .GetMethod(nameof(JsonEncodedText.Encode), new[] { typeof(ReadOnlySpan <char>), typeof(JavaScriptEncoder) });

                var writeString = typeof(Utf8JsonWriter)
                                  .GetMethod(nameof(Utf8JsonWriter.WriteStringValue), new[] { typeof(JsonEncodedText) });

                return(JsonSerializerBuilderCaseResult.FromExpression(
                           Expression.Block(
                               new[] { chars },
                               Expression.Assign(
                                   chars,
                                   Expression.NewArrayBounds(typeof(char), Expression.Constant(12))),
                               Write(
                                   Expression.ConvertChecked(Expression.Property(value, totalDays), typeof(uint)),
                                   Expression.Constant(4)),
                               Write(
                                   Expression.ConvertChecked(
                                       Expression.Subtract(
                                           Expression.Convert(Expression.Property(value, totalMs), typeof(ulong)),
                                           Expression.Multiply(
                                               Expression.Convert(Expression.Property(value, totalDays), typeof(ulong)),
                                               Expression.Constant(86400000UL))),
                                       typeof(uint)),
                                   Expression.Constant(8)),
                               Expression.Call(
                                   context.Writer,
                                   writeString,
                                   Expression.Call(
                                       null,
                                       encode,
                                       Expression.Convert(chars, typeof(ReadOnlySpan <char>)),
                                       Expression.Constant(JsonEncoder.Bytes))))));
            }
            else
            {
                return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonDurationSerializerBuilderCase)} can only be applied schemas with a {nameof(DurationLogicalType)}.")));
            }
        }
        /// <summary>
        /// Builds a <see cref="JsonSerializer{T}" /> for an <see cref="EnumSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="type" />
        /// is an enum and <paramref name="schema" /> is an <see cref="EnumSchema" />; an
        /// unsuccessful <see cref="JsonSerializerBuilderCaseResult" /> otherwise.
        /// </returns>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <paramref name="schema" /> does not have a matching symbol for each member
        /// of <paramref name="type" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext context)
        {
            if (schema is EnumSchema enumSchema)
            {
                var writeString = typeof(Utf8JsonWriter)
                                  .GetMethod(nameof(Utf8JsonWriter.WriteStringValue), new[] { typeof(string) });

                // enum fields will always be public static, so no need to expose binding flags:
                var fields  = type.GetFields(BindingFlags.Public | BindingFlags.Static);
                var symbols = enumSchema.Symbols.ToList();

                // find a match for each enum in the type:
                var cases = type.IsEnum
                    ? fields
                            .Select(field =>
                {
                    var match = symbols.Find(symbol => IsMatch(symbol, field));

                    if (match == null)
                    {
                        throw new UnsupportedTypeException(type, $"{type} has a field {field.Name} that cannot be serialized.");
                    }

                    if (symbols.FindLast(symbol => IsMatch(symbol, field)) != match)
                    {
                        throw new UnsupportedTypeException(type, $"{type.Name} has an ambiguous field {field.Name}.");
                    }

                    return(Expression.SwitchCase(
                               Expression.Call(context.Writer, writeString, Expression.Constant(match)),
                               Expression.Constant(Enum.Parse(type, field.Name))));
                })
                    : symbols
                            .Select(symbol =>
                {
                    return(Expression.SwitchCase(
                               Expression.Call(context.Writer, writeString, Expression.Constant(symbol)),
                               Expression.Constant(symbol)));
                });

                var exceptionConstructor = typeof(ArgumentOutOfRangeException)
                                           .GetConstructor(new[] { typeof(string) });

                var exception    = Expression.New(exceptionConstructor, Expression.Constant("Enum value out of range."));
                var intermediate = Expression.Variable(type.IsEnum ? type : typeof(string));

                return(JsonSerializerBuilderCaseResult.FromExpression(
                           Expression.Block(
                               new[] { intermediate },
                               Expression.Assign(intermediate, BuildConversion(value, intermediate.Type)),
                               Expression.Switch(intermediate, Expression.Throw(exception), cases.ToArray()))));
            }
            else
            {
                return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonEnumSerializerBuilderCase)} can only be applied to {nameof(EnumSchema)}s.")));
            }
        }
        /// <summary>
        /// Builds a <see cref="JsonSerializer{T}" /> for an <see cref="MapSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="type" />
        /// is a dictionary type and <paramref name="schema" /> is a <see cref="MapSchema" />; an
        /// unsuccessful <see cref="JsonSerializerBuilderCaseResult" /> otherwise.
        /// </returns>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <paramref name="type" /> does not implement <see cref="T:System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair`2}" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext 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(object);
                    var valueType = dictionaryTypes?.Value ?? typeof(object);

                    var pairType   = typeof(KeyValuePair <,>).MakeGenericType(keyType, valueType);
                    var enumerable = Expression.Variable(typeof(IEnumerable <>).MakeGenericType(pairType));
                    var enumerator = Expression.Variable(typeof(IEnumerator <>).MakeGenericType(pairType));
                    var loop       = Expression.Label();

                    var writeStartObject = typeof(Utf8JsonWriter)
                                           .GetMethod(nameof(Utf8JsonWriter.WriteStartObject), Type.EmptyTypes);

                    var getEnumerator = enumerable.Type
                                        .GetMethod("GetEnumerator", Type.EmptyTypes);

                    var getCurrent = enumerator.Type
                                     .GetProperty(nameof(IEnumerator.Current))
                                     .GetGetMethod();

                    var moveNext = typeof(IEnumerator)
                                   .GetMethod(nameof(IEnumerator.MoveNext), Type.EmptyTypes);

                    var getKey = pairType
                                 .GetProperty("Key")
                                 .GetGetMethod();

                    var getValue = pairType
                                   .GetProperty("Value")
                                   .GetGetMethod();

                    var writeKey = new KeySerializerVisitor().Visit(
                        SerializerBuilder.BuildExpression(
                            Expression.Property(Expression.Property(enumerator, getCurrent), getKey), new StringSchema(), context));

                    var writeValue = SerializerBuilder.BuildExpression(
                        Expression.Property(Expression.Property(enumerator, getCurrent), getValue), mapSchema.Value, context);

                    var writeEndObject = typeof(Utf8JsonWriter)
                                         .GetMethod(nameof(Utf8JsonWriter.WriteEndObject), Type.EmptyTypes);

                    var dispose = typeof(IDisposable)
                                  .GetMethod(nameof(IDisposable.Dispose), Type.EmptyTypes);

                    Expression expression;

                    try
                    {
                        expression = BuildConversion(value, enumerable.Type);
                    }
                    catch (Exception exception)
                    {
                        throw new UnsupportedTypeException(type, $"Failed to map {mapSchema} to {type}.", exception);
                    }

                    return(JsonSerializerBuilderCaseResult.FromExpression(
                               Expression.Block(
                                   new[] { enumerator },
                                   Expression.Call(context.Writer, writeStartObject),
                                   Expression.Assign(
                                       enumerator,
                                       Expression.Call(expression, getEnumerator)),
                                   Expression.TryFinally(
                                       Expression.Loop(
                                           Expression.IfThenElse(
                                               Expression.Call(enumerator, moveNext),
                                               Expression.Block(writeKey, writeValue),
                                               Expression.Break(loop)),
                                           loop),
                                       Expression.Call(enumerator, dispose)),
                                   Expression.Call(context.Writer, writeEndObject))));
                }
                else
                {
                    return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedTypeException(type, $"{nameof(JsonMapSerializerBuilderCase)} can only be applied to dictionary types.")));
                }
            }