зеркало из https://github.com/microsoft/Power-Fx.git
JSON function: serialize UO (#2639)
>> JSON(ParseJSON("{""a"": 1}"), JSONFormat.IgnoreUnsupportedTypes) "{""a"":1}"
This commit is contained in:
Родитель
93daa65514
Коммит
ee0ca2c67e
|
@ -845,5 +845,7 @@ namespace Microsoft.PowerFx.Core.Localization
|
|||
public static ErrorResourceKey ErrInvalidDataSourceForFunction = new ErrorResourceKey("ErrInvalidDataSourceForFunction");
|
||||
public static ErrorResourceKey ErrInvalidArgumentExpectedType = new ErrorResourceKey("ErrInvalidArgumentExpectedType");
|
||||
public static ErrorResourceKey ErrUnsupportedTypeInTypeArgument = new ErrorResourceKey("ErrUnsupportedTypeInTypeArgument");
|
||||
public static ErrorResourceKey ErrReachedMaxJsonDepth = new ErrorResourceKey("ErrReachedMaxJsonDepth");
|
||||
public static ErrorResourceKey ErrReachedMaxJsonLength = new ErrorResourceKey("ErrReachedMaxJsonLength");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.PowerFx
|
||||
{
|
||||
internal class Canceller
|
||||
{
|
||||
private readonly List<Action> _cancellationAction;
|
||||
|
||||
public Canceller()
|
||||
{
|
||||
_cancellationAction = new List<Action>();
|
||||
}
|
||||
|
||||
public Canceller(params Action[] cancellationActions)
|
||||
: this()
|
||||
{
|
||||
if (cancellationActions != null)
|
||||
{
|
||||
foreach (Action cancellationAction in cancellationActions)
|
||||
{
|
||||
AddAction(cancellationAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddAction(Action cancellationAction)
|
||||
{
|
||||
if (cancellationAction != null)
|
||||
{
|
||||
_cancellationAction.Add(cancellationAction);
|
||||
}
|
||||
}
|
||||
|
||||
public void ThrowIfCancellationRequested()
|
||||
{
|
||||
foreach (Action cancellationAction in _cancellationAction)
|
||||
{
|
||||
cancellationAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
|
|||
{
|
||||
// JSON(data:any, [format:s])
|
||||
internal class JsonFunction : BuiltinFunction
|
||||
{
|
||||
{
|
||||
private const char _includeBinaryDataEnumValue = 'B';
|
||||
private const char _ignoreBinaryDataEnumValue = 'G';
|
||||
private const char _ignoreUnsupportedTypesEnumValue = 'I';
|
||||
|
@ -31,26 +31,25 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
|
|||
DKind.DataEntity,
|
||||
DKind.LazyRecord,
|
||||
DKind.LazyTable,
|
||||
DKind.View,
|
||||
DKind.View,
|
||||
DKind.ViewValue
|
||||
};
|
||||
|
||||
private static readonly DKind[] _unsupportedTypes = new[]
|
||||
{
|
||||
DKind.Control,
|
||||
DKind.Control,
|
||||
DKind.LazyRecord,
|
||||
DKind.LazyTable,
|
||||
DKind.Metadata,
|
||||
DKind.OptionSet,
|
||||
DKind.PenImage,
|
||||
DKind.OptionSet,
|
||||
DKind.PenImage,
|
||||
DKind.Polymorphic,
|
||||
DKind.UntypedObject,
|
||||
DKind.Void
|
||||
};
|
||||
|
||||
public override bool IsSelfContained => true;
|
||||
|
||||
public override bool IsAsync => true;
|
||||
public override bool IsAsync => true;
|
||||
|
||||
public override bool SupportsParamCoercion => false;
|
||||
|
||||
|
@ -78,13 +77,13 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
|
|||
// Do not call base.CheckTypes for arg0
|
||||
if (args.Length > 1)
|
||||
{
|
||||
if (context.Features.StronglyTypedBuiltinEnums &&
|
||||
if (context.Features.StronglyTypedBuiltinEnums &&
|
||||
!base.CheckType(context, args[1], argTypes[1], BuiltInEnums.JSONFormatEnum.FormulaType._type, errors, ref nodeToCoercedTypeMap))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TexlNode optionsNode = args[1];
|
||||
TexlNode optionsNode = args[1];
|
||||
if (!IsConstant(context, argTypes, optionsNode, out string nodeValue))
|
||||
{
|
||||
errors.EnsureError(optionsNode, TexlStrings.ErrFunctionArg2ParamMustBeConstant, "JSON", TexlStrings.JSONArg2.Invoke());
|
||||
|
@ -117,11 +116,11 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
|
|||
|
||||
bool includeBinaryData = false;
|
||||
bool ignoreUnsupportedTypes = false;
|
||||
bool ignoreBinaryData = false;
|
||||
bool ignoreBinaryData = false;
|
||||
|
||||
if (args.Length > 1)
|
||||
{
|
||||
TexlNode optionsNode = args[1];
|
||||
TexlNode optionsNode = args[1];
|
||||
if (!IsConstant(binding.CheckTypesContext, argTypes, optionsNode, out string nodeValue))
|
||||
{
|
||||
return;
|
||||
|
@ -180,12 +179,12 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
|
|||
}
|
||||
|
||||
if (!ignoreUnsupportedTypes)
|
||||
{
|
||||
{
|
||||
if (HasUnsupportedType(dataArgType, supportsLazyTypes, out DType unsupportedNestedType, out var unsupportedColumnName))
|
||||
{
|
||||
errors.EnsureError(dataNode, TexlStrings.ErrJSONArg1UnsupportedNestedType, unsupportedColumnName, unsupportedNestedType.GetKindString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsConstant(CheckTypesContext context, DType[] argTypes, TexlNode optionsNode, out string nodeValue)
|
||||
|
|
|
@ -332,7 +332,19 @@ namespace Microsoft.PowerFx
|
|||
}
|
||||
else if (func is IAsyncTexlFunction5 asyncFunc5)
|
||||
{
|
||||
result = await asyncFunc5.InvokeAsync(_services, node.IRContext.ResultType, args, _cancellationToken).ConfigureAwait(false);
|
||||
BasicServiceProvider services2 = new BasicServiceProvider(_services);
|
||||
|
||||
if (services2.GetService(typeof(TimeZoneInfo)) == null)
|
||||
{
|
||||
services2.AddService(TimeZoneInfo);
|
||||
}
|
||||
|
||||
if (services2.GetService(typeof(Canceller)) == null)
|
||||
{
|
||||
services2.AddService(new Canceller(CheckCancel));
|
||||
}
|
||||
|
||||
result = await asyncFunc5.InvokeAsync(services2, node.IRContext.ResultType, args, _cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (func is IAsyncConnectorTexlFunction asyncConnectorTexlFunction)
|
||||
{
|
||||
|
|
|
@ -6,9 +6,7 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerFx.Core.Functions;
|
||||
using Microsoft.PowerFx.Core.IR;
|
||||
using Microsoft.PowerFx.Core.Types;
|
||||
using Microsoft.PowerFx.Core.Utils;
|
||||
using Microsoft.PowerFx.Functions;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.PowerFx.Core.Texl.Builtins
|
||||
|
@ -18,6 +16,7 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
|
|||
public async Task<FormulaValue> InvokeAsync(TimeZoneInfo timezoneInfo, FormulaType ft, FormulaValue[] args, CancellationToken cancellationToken)
|
||||
{
|
||||
Contracts.Assert(args.Length == 2);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var irContext = IRContext.NotInSource(ft);
|
||||
var typeString = (StringValue)args[1];
|
||||
|
|
|
@ -6,9 +6,7 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerFx.Core.Functions;
|
||||
using Microsoft.PowerFx.Core.IR;
|
||||
using Microsoft.PowerFx.Core.Types;
|
||||
using Microsoft.PowerFx.Core.Utils;
|
||||
using Microsoft.PowerFx.Functions;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.PowerFx.Core.Texl.Builtins
|
||||
|
@ -18,6 +16,7 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
|
|||
public async Task<FormulaValue> InvokeAsync(TimeZoneInfo timezoneInfo, FormulaType ft, FormulaValue[] args, CancellationToken cancellationToken)
|
||||
{
|
||||
Contracts.Assert(args.Length == 2);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var irContext = IRContext.NotInSource(FormulaType.UntypedObject);
|
||||
var typeString = (StringValue)args[1];
|
||||
|
|
|
@ -16,24 +16,30 @@ using System.Threading.Tasks;
|
|||
using Microsoft.PowerFx.Core.Entities;
|
||||
using Microsoft.PowerFx.Core.Functions;
|
||||
using Microsoft.PowerFx.Core.IR;
|
||||
using Microsoft.PowerFx.Core.Localization;
|
||||
using Microsoft.PowerFx.Core.Types.Enums;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.PowerFx.Core.Texl.Builtins
|
||||
{
|
||||
internal class JsonFunctionImpl : JsonFunction, IAsyncTexlFunction4
|
||||
internal class JsonFunctionImpl : JsonFunction, IAsyncTexlFunction5
|
||||
{
|
||||
public Task<FormulaValue> InvokeAsync(TimeZoneInfo timezoneInfo, FormulaType type, FormulaValue[] args, CancellationToken cancellationToken)
|
||||
public Task<FormulaValue> InvokeAsync(IServiceProvider runtimeServiceProvider, FormulaType type, FormulaValue[] args, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(new JsonProcessing(timezoneInfo, type, args, supportsLazyTypes).Process());
|
||||
TimeZoneInfo timeZoneInfo = runtimeServiceProvider.GetService(typeof(TimeZoneInfo)) as TimeZoneInfo ?? throw new InvalidOperationException("TimeZoneInfo is required");
|
||||
Canceller canceller = runtimeServiceProvider.GetService(typeof(Canceller)) as Canceller ?? new Canceller(() => cancellationToken.ThrowIfCancellationRequested());
|
||||
|
||||
return Task.FromResult(new JsonProcessing(timeZoneInfo, type, args, supportsLazyTypes).Process(canceller));
|
||||
}
|
||||
|
||||
internal class JsonProcessing
|
||||
{
|
||||
private readonly FormulaValue[] _arguments;
|
||||
|
||||
private readonly FormulaType _type;
|
||||
|
||||
private readonly TimeZoneInfo _timeZoneInfo;
|
||||
|
||||
private readonly bool _supportsLazyTypes;
|
||||
|
||||
internal JsonProcessing(TimeZoneInfo timezoneInfo, FormulaType type, FormulaValue[] args, bool supportsLazyTypes)
|
||||
|
@ -44,8 +50,10 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
|
|||
_supportsLazyTypes = supportsLazyTypes;
|
||||
}
|
||||
|
||||
internal FormulaValue Process()
|
||||
internal FormulaValue Process(Canceller canceller)
|
||||
{
|
||||
canceller.ThrowIfCancellationRequested();
|
||||
|
||||
JsonFlags flags = GetFlags();
|
||||
|
||||
if (flags == null || JsonFunction.HasUnsupportedType(_arguments[0].Type._type, _supportsLazyTypes, out _, out _))
|
||||
|
@ -61,10 +69,21 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
|
|||
|
||||
using MemoryStream memoryStream = new MemoryStream();
|
||||
using Utf8JsonWriter writer = new Utf8JsonWriter(memoryStream, new JsonWriterOptions() { Indented = flags.IndentFour, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping });
|
||||
Utf8JsonWriterVisitor jsonWriterVisitor = new Utf8JsonWriterVisitor(writer, _timeZoneInfo, flattenValueTables: flags.FlattenValueTables);
|
||||
Utf8JsonWriterVisitor jsonWriterVisitor = new Utf8JsonWriterVisitor(writer, _timeZoneInfo, flattenValueTables: flags.FlattenValueTables, canceller);
|
||||
|
||||
_arguments[0].Visit(jsonWriterVisitor);
|
||||
writer.Flush();
|
||||
try
|
||||
{
|
||||
_arguments[0].Visit(jsonWriterVisitor);
|
||||
writer.Flush();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
if (!jsonWriterVisitor.ErrorValues.Any())
|
||||
{
|
||||
// Unexpected error, rethrow
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonWriterVisitor.ErrorValues.Any())
|
||||
{
|
||||
|
@ -104,7 +123,7 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
|
|||
optionString = sv.Value;
|
||||
break;
|
||||
|
||||
// if not one of these, will check optionString != null below
|
||||
// if not one of these, will check optionString != null below
|
||||
}
|
||||
|
||||
if (optionString != null)
|
||||
|
@ -129,17 +148,58 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
|
|||
|
||||
private class Utf8JsonWriterVisitor : IValueVisitor
|
||||
{
|
||||
private const int _maxDepth = 20; // maximum depth of UO
|
||||
|
||||
private const int _maxLength = 1024 * 1024; // 1 MB, maximum number of bytes allowed to be sent to Utf8JsonWriter
|
||||
|
||||
private readonly Utf8JsonWriter _writer;
|
||||
|
||||
private readonly TimeZoneInfo _timeZoneInfo;
|
||||
|
||||
private readonly bool _flattenValueTables;
|
||||
|
||||
private readonly Canceller _canceller;
|
||||
|
||||
internal readonly List<ErrorValue> ErrorValues = new List<ErrorValue>();
|
||||
|
||||
internal Utf8JsonWriterVisitor(Utf8JsonWriter writer, TimeZoneInfo timeZoneInfo, bool flattenValueTables)
|
||||
internal Utf8JsonWriterVisitor(Utf8JsonWriter writer, TimeZoneInfo timeZoneInfo, bool flattenValueTables, Canceller canceller)
|
||||
{
|
||||
_writer = writer;
|
||||
_timeZoneInfo = timeZoneInfo;
|
||||
_flattenValueTables = flattenValueTables;
|
||||
|
||||
_canceller = canceller;
|
||||
}
|
||||
|
||||
private void CheckLimitsAndCancellation(int index)
|
||||
{
|
||||
_canceller.ThrowIfCancellationRequested();
|
||||
|
||||
if (index > _maxDepth)
|
||||
{
|
||||
IRContext irContext = IRContext.NotInSource(FormulaType.UntypedObject);
|
||||
ErrorValues.Add(new ErrorValue(irContext, new ExpressionError()
|
||||
{
|
||||
ResourceKey = TexlStrings.ErrReachedMaxJsonDepth,
|
||||
Span = irContext.SourceContext,
|
||||
Kind = ErrorKind.InvalidArgument
|
||||
}));
|
||||
|
||||
throw new InvalidOperationException($"Maximum depth {_maxDepth} reached while traversing JSON payload.");
|
||||
}
|
||||
|
||||
if (_writer.BytesCommitted + _writer.BytesPending > _maxLength)
|
||||
{
|
||||
IRContext irContext = IRContext.NotInSource(FormulaType.UntypedObject);
|
||||
ErrorValues.Add(new ErrorValue(irContext, new ExpressionError()
|
||||
{
|
||||
ResourceKey = TexlStrings.ErrReachedMaxJsonLength,
|
||||
Span = irContext.SourceContext,
|
||||
Kind = ErrorKind.InvalidArgument
|
||||
}));
|
||||
|
||||
throw new InvalidOperationException($"Maximum length {_maxLength} reached in JSON function.");
|
||||
}
|
||||
}
|
||||
|
||||
public void Visit(BlankValue blankValue)
|
||||
|
@ -173,7 +233,7 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
|
|||
}
|
||||
|
||||
public void Visit(ErrorValue errorValue)
|
||||
{
|
||||
{
|
||||
ErrorValues.Add(errorValue);
|
||||
_writer.WriteStringValue("ErrorValue");
|
||||
}
|
||||
|
@ -256,8 +316,10 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
|
|||
{
|
||||
_writer.WriteStartObject();
|
||||
|
||||
foreach (NamedValue namedValue in recordValue.Fields)
|
||||
foreach (NamedValue namedValue in recordValue.Fields.OrderBy(f => f.Name, StringComparer.Ordinal))
|
||||
{
|
||||
CheckLimitsAndCancellation(0);
|
||||
|
||||
_writer.WritePropertyName(namedValue.Name);
|
||||
namedValue.Value.Visit(this);
|
||||
}
|
||||
|
@ -287,6 +349,8 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
|
|||
|
||||
foreach (DValue<RecordValue> row in tableValue.Rows)
|
||||
{
|
||||
CheckLimitsAndCancellation(0);
|
||||
|
||||
if (row.IsBlank)
|
||||
{
|
||||
row.Blank.Visit(this);
|
||||
|
@ -319,7 +383,77 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
|
|||
|
||||
public void Visit(UntypedObjectValue untypedObjectValue)
|
||||
{
|
||||
throw new ArgumentException($"Unable to serialize type {untypedObjectValue.GetType().FullName} to Json format.");
|
||||
Visit(untypedObjectValue.Impl);
|
||||
}
|
||||
|
||||
private void Visit(IUntypedObject untypedObject, int depth = 0)
|
||||
{
|
||||
FormulaType type = untypedObject.Type;
|
||||
|
||||
CheckLimitsAndCancellation(depth);
|
||||
|
||||
if (type is StringType)
|
||||
{
|
||||
_writer.WriteStringValue(untypedObject.GetString());
|
||||
}
|
||||
else if (type is DecimalType)
|
||||
{
|
||||
_writer.WriteNumberValue(untypedObject.GetDecimal());
|
||||
}
|
||||
else if (type is NumberType)
|
||||
{
|
||||
_writer.WriteNumberValue(untypedObject.GetDouble());
|
||||
}
|
||||
else if (type is BooleanType)
|
||||
{
|
||||
_writer.WriteBooleanValue(untypedObject.GetBoolean());
|
||||
}
|
||||
else if (type is ExternalType externalType)
|
||||
{
|
||||
if (externalType.Kind == ExternalTypeKind.Array || externalType.Kind == ExternalTypeKind.ArrayAndObject)
|
||||
{
|
||||
_writer.WriteStartArray();
|
||||
|
||||
for (var i = 0; i < untypedObject.GetArrayLength(); i++)
|
||||
{
|
||||
CheckLimitsAndCancellation(depth);
|
||||
|
||||
IUntypedObject row = untypedObject[i];
|
||||
Visit(row, depth + 1);
|
||||
}
|
||||
|
||||
_writer.WriteEndArray();
|
||||
}
|
||||
else if ((externalType.Kind == ExternalTypeKind.Object || externalType.Kind == ExternalTypeKind.ArrayAndObject) && untypedObject.TryGetPropertyNames(out IEnumerable<string> propertyNames))
|
||||
{
|
||||
_writer.WriteStartObject();
|
||||
|
||||
foreach (var propertyName in propertyNames.OrderBy(prop => prop, StringComparer.Ordinal))
|
||||
{
|
||||
CheckLimitsAndCancellation(depth);
|
||||
|
||||
if (untypedObject.TryGetProperty(propertyName, out IUntypedObject res))
|
||||
{
|
||||
_writer.WritePropertyName(propertyName);
|
||||
Visit(res, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
_writer.WriteEndObject();
|
||||
}
|
||||
else if (externalType.Kind == ExternalTypeKind.UntypedNumber)
|
||||
{
|
||||
_writer.WriteRawValue(untypedObject.GetUntypedNumber());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Unknown ExternalType");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Unknown IUntypedObject");
|
||||
}
|
||||
}
|
||||
|
||||
public void Visit(BlobValue value)
|
||||
|
@ -332,7 +466,7 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
|
|||
{
|
||||
_writer.WriteBase64StringValue(value.GetAsByteArrayAsync(CancellationToken.None).Result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static string GetColorString(Color color) => $"#{color.R:x2}{color.G:x2}{color.B:x2}{color.A:x2}";
|
||||
|
|
|
@ -6,9 +6,7 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerFx.Core.Functions;
|
||||
using Microsoft.PowerFx.Core.IR;
|
||||
using Microsoft.PowerFx.Core.Types;
|
||||
using Microsoft.PowerFx.Core.Utils;
|
||||
using Microsoft.PowerFx.Functions;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.PowerFx.Core.Texl.Builtins
|
||||
|
@ -18,6 +16,7 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
|
|||
public async Task<FormulaValue> InvokeAsync(TimeZoneInfo timezoneInfo, FormulaType ft, FormulaValue[] args, CancellationToken cancellationToken)
|
||||
{
|
||||
Contracts.Assert(args.Length == 2);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var irContext = IRContext.NotInSource(ft);
|
||||
var typeString = (StringValue)args[1];
|
||||
|
|
|
@ -4760,4 +4760,12 @@
|
|||
<value>Unsupported untyped/JSON conversion type '{0}' in argument.</value>
|
||||
<comment>Error Message shown to user when a unsupported type is passed in type argument of AsType, IsType and ParseJSON functions.</comment>
|
||||
</data>
|
||||
<data name="ErrReachedMaxJsonDepth" xml:space="preserve">
|
||||
<value>Maximum depth reached while traversing JSON payload.</value>
|
||||
<comment>Error message returned by the {Locked=JSON} function when a document that is too deeply nested is passed to it. The term JSON refers to the data format described in www.json.org.</comment>
|
||||
</data>
|
||||
<data name="ErrReachedMaxJsonLength" xml:space="preserve">
|
||||
<value>Maximum length reached in JSON function.</value>
|
||||
<comment>Error message returned by the {Locked=JSON} function when the result generated by this function would be too long. The term JSON refers to the data format described in www.json.org.</comment>
|
||||
</data>
|
||||
</root>
|
|
@ -1,4 +1,4 @@
|
|||
#SETUP: EnableJsonFunctions
|
||||
#SETUP: EnableJsonFunctions, PowerFxV1CompatibilityRules
|
||||
|
||||
>> JSON()
|
||||
Errors: Error 0-6: Invalid number of arguments: received 0, expected 1-2.
|
||||
|
@ -101,6 +101,10 @@ Error({Kind:ErrorKind.Div0})
|
|||
>> JSON({a:1,b:Sqrt(-1),c:true})
|
||||
Error({Kind:ErrorKind.Numeric})
|
||||
|
||||
// Reordering of properties in culture-invariant order
|
||||
>> JSON({b:2,a:1})
|
||||
"{""a"":1,""b"":2}"
|
||||
|
||||
>> JSON([{a:1,b:[2]},{a:3,b:[4,5]},{a:6,b:[7,1/0,9]}])
|
||||
Error({Kind:ErrorKind.Div0})
|
||||
|
||||
|
@ -133,3 +137,95 @@ Error({Kind:ErrorKind.Div0})
|
|||
// Flattening nested tables
|
||||
>> JSON([[1,2,3],[4,5],[6]], JSONFormat.FlattenValueTables)
|
||||
"[[1,2,3],[4,5],[6]]"
|
||||
|
||||
>> JSON(ParseJSON("{}"))
|
||||
"{}"
|
||||
|
||||
>> JSON(ParseJSON("[]"))
|
||||
"[]"
|
||||
|
||||
>> JSON(ParseJSON("1"))
|
||||
"1"
|
||||
|
||||
>> JSON(ParseJSON("1.77"))
|
||||
"1.77"
|
||||
|
||||
>> JSON(ParseJSON("-871"))
|
||||
"-871"
|
||||
|
||||
>> JSON(ParseJSON("""John"""))
|
||||
"""John"""
|
||||
|
||||
>> JSON(ParseJSON("true"))
|
||||
"true"
|
||||
|
||||
>> JSON(ParseJSON("false"))
|
||||
"false"
|
||||
|
||||
>> JSON(ParseJSON("{""a"": 1}"))
|
||||
"{""a"":1}"
|
||||
|
||||
// Reordering of properties in culture-invariant order
|
||||
>> JSON(ParseJSON("{""b"": 2, ""a"": 1}"))
|
||||
"{""a"":1,""b"":2}"
|
||||
|
||||
>> JSON(ParseJSON("{""a"": ""x""}"))
|
||||
"{""a"":""x""}"
|
||||
|
||||
>> JSON(ParseJSON("[1]"))
|
||||
"[1]"
|
||||
|
||||
>> JSON(ParseJSON("{""a"": 1.5}"))
|
||||
"{""a"":1.5}"
|
||||
|
||||
>> JSON(ParseJSON("[1.5]"))
|
||||
"[1.5]"
|
||||
|
||||
>> JSON(ParseJSON("{""a"":[1]}"))
|
||||
"{""a"":[1]}"
|
||||
|
||||
>> JSON(ParseJSON("[{""a"": -17}]"))
|
||||
"[{""a"":-17}]"
|
||||
|
||||
>> JSON(ParseJSON("[true, false]"))
|
||||
"[true,false]"
|
||||
|
||||
>> JSON(ParseJSON("[""True"", ""False""]"))
|
||||
"[""True"",""False""]"
|
||||
|
||||
// Round-trip is not guaranteed
|
||||
>> JSON(ParseJSON(" { ""a"" : 1 } "))
|
||||
"{""a"":1}"
|
||||
|
||||
>> JSON(ParseJSON("{""a"": {""a"": 1}}"))
|
||||
"{""a"":{""a"":1}}"
|
||||
|
||||
// Depth 21
|
||||
>> JSON(ParseJSON("{""a"": {""a"": {""a"": {""a"": {""a"": {""a"": {""a"": {""a"": {""a"": {""a"": {""a"": {""a"": {""a"": {""a"": {""a"": {""a"": {""a"": {""a"": {""a"": {""a"": {""a"": 1}}}}}}}}}}}}}}}}}}}}}"))
|
||||
Error({Kind:ErrorKind.InvalidArgument})
|
||||
|
||||
>> JSON(ParseJSON("[[1]]"))
|
||||
"[[1]]"
|
||||
|
||||
// Depth 21
|
||||
>> JSON(ParseJSON("[[[[[[[[[[[[[[[[[[[[[1]]]]]]]]]]]]]]]]]]]]]"))
|
||||
Error({Kind:ErrorKind.InvalidArgument})
|
||||
|
||||
>> JSON(Decimal(ParseJSON("123456789012345.6789012345678")))
|
||||
"123456789012345.6789012345678"
|
||||
|
||||
>> JSON(ParseJSON("123456789012345.6789012345678"))
|
||||
"123456789012345.6789012345678"
|
||||
|
||||
// Round-trip is not guaranteed - escaped characters that don't need escaping will not be re-escaped when JSON-ified
|
||||
>> JSON(ParseJSON("""\u0048\u0065\u006c\u006c\u006f"""))
|
||||
"""Hello"""
|
||||
|
||||
>> JSON(ParseJSON("1e300"))
|
||||
"1e300"
|
||||
|
||||
>> JSON(ParseJSON("1111111111111111111111111111111.2222222222222222222222222222222222"))
|
||||
"1111111111111111111111111111111.2222222222222222222222222222222222"
|
||||
|
||||
>> JSON(ParseJSON("1e700"))
|
||||
"1e700"
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerFx.Core.Tests;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Xunit;
|
||||
|
||||
#pragma warning disable CA1065
|
||||
|
||||
namespace Microsoft.PowerFx.Tests
|
||||
{
|
||||
public class JsonSerializeUOTests : PowerFxTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task JsonSerializeUOTest()
|
||||
{
|
||||
PowerFxConfig config = new PowerFxConfig();
|
||||
config.EnableJsonFunctions();
|
||||
|
||||
SymbolTable symbolTable = new SymbolTable();
|
||||
ISymbolSlot objSlot = symbolTable.AddVariable("obj", FormulaType.UntypedObject);
|
||||
|
||||
foreach ((int id, TestUO uo, string expectedResult) in GetUOTests())
|
||||
{
|
||||
SymbolValues symbolValues = new SymbolValues(symbolTable);
|
||||
symbolValues.Set(objSlot, FormulaValue.New(uo));
|
||||
|
||||
RuntimeConfig runtimeConfig = new RuntimeConfig(symbolValues);
|
||||
RecalcEngine engine = new RecalcEngine(config);
|
||||
|
||||
FormulaValue fv = await engine.EvalAsync("JSON(obj)", CancellationToken.None, runtimeConfig: runtimeConfig);
|
||||
Assert.IsNotType<ErrorValue>(fv);
|
||||
|
||||
string str = fv.ToExpression().ToString();
|
||||
Assert.True(expectedResult == str, $"[{id}: Expected={expectedResult}, Result={str}]");
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<(int id, TestUO uo, string expectedResult)> GetUOTests()
|
||||
{
|
||||
yield return (1, new TestUO(true), @"""true""");
|
||||
yield return (2, new TestUO(false), @"""false""");
|
||||
yield return (3, new TestUO(string.Empty), @"""""""""""""");
|
||||
yield return (4, new TestUO("abc"), @"""""""abc""""""");
|
||||
yield return (5, new TestUO(null), @"""null""");
|
||||
yield return (6, new TestUO(0), @"""0""");
|
||||
yield return (7, new TestUO(1.3f), @"""1.3""");
|
||||
yield return (8, new TestUO(-1.7m), @"""-1.7""");
|
||||
yield return (9, new TestUO(new[] { true, false }), @"""[true,false]""");
|
||||
yield return (10, new TestUO(new bool[0]), @"""[]""");
|
||||
yield return (11, new TestUO(new[] { "abc", "def" }), @"""[""""abc"""",""""def""""]""");
|
||||
yield return (12, new TestUO(new string[0]), @"""[]""");
|
||||
yield return (13, new TestUO(new[] { 11.5m, -7.5m }), @"""[11.5,-7.5]""");
|
||||
yield return (14, new TestUO(new string[0]), @"""[]""");
|
||||
yield return (15, new TestUO(new[] { new[] { 1, 2 }, new[] { 3, 4 } }), @"""[[1,2],[3,4]]""");
|
||||
yield return (16, new TestUO(new[] { new object[] { 1, 2 }, new object[] { true, "a", 7 } }), @"""[[1,2],[true,""""a"""",7]]""");
|
||||
yield return (17, new TestUO(new { a = 10, b = -20m, c = "abc" }), @"""{""""a"""":10,""""b"""":-20,""""c"""":""""abc""""}""");
|
||||
yield return (18, new TestUO(new { x = new { y = true } }), @"""{""""x"""":{""""y"""":true}}""");
|
||||
yield return (19, new TestUO(new { x = new { y = new[] { 1 }, z = "a", t = new { } }, a = false }), @"""{""""a"""":false,""""x"""":{""""t"""":{},""""y"""":[1],""""z"""":""""a""""}}""");
|
||||
yield return (20, new TestUO(123456789012345.6789012345678m), @"""123456789012345.6789012345678""");
|
||||
}
|
||||
|
||||
public class TestUO : IUntypedObject
|
||||
{
|
||||
private enum UOType
|
||||
{
|
||||
Unknown = -1,
|
||||
Array,
|
||||
Object,
|
||||
Bool,
|
||||
Decimal,
|
||||
String
|
||||
}
|
||||
|
||||
private readonly dynamic _o;
|
||||
|
||||
public TestUO(object o)
|
||||
{
|
||||
_o = o;
|
||||
}
|
||||
|
||||
public IUntypedObject this[int index] => GetUOType(_o) == UOType.Array ? new TestUO(_o[index]) : throw new Exception("Not an array");
|
||||
|
||||
public FormulaType Type => _o == null ? FormulaType.String : _o switch
|
||||
{
|
||||
string => FormulaType.String,
|
||||
int or double or float => new ExternalType(ExternalTypeKind.UntypedNumber),
|
||||
decimal => FormulaType.Decimal,
|
||||
bool => FormulaType.Boolean,
|
||||
Array => new ExternalType(ExternalTypeKind.Array),
|
||||
object o => new ExternalType(ExternalTypeKind.Object),
|
||||
_ => throw new Exception("Not a valid type")
|
||||
};
|
||||
|
||||
private static UOType GetUOType(object o) => o switch
|
||||
{
|
||||
bool => UOType.Bool,
|
||||
int or decimal or double => UOType.Decimal,
|
||||
string => UOType.String,
|
||||
Array => UOType.Array,
|
||||
_ => UOType.Object
|
||||
};
|
||||
|
||||
public int GetArrayLength()
|
||||
{
|
||||
return _o is Array a ? a.Length : throw new Exception("Not an array");
|
||||
}
|
||||
|
||||
public bool GetBoolean()
|
||||
{
|
||||
return _o is bool b ? b
|
||||
: throw new Exception("Not a boolean");
|
||||
}
|
||||
|
||||
public decimal GetDecimal()
|
||||
{
|
||||
return _o is int i ? (decimal)i
|
||||
: _o is float f ? (decimal)f
|
||||
: _o is decimal dec ? dec
|
||||
: throw new Exception("Not a decimal");
|
||||
}
|
||||
|
||||
public double GetDouble()
|
||||
{
|
||||
return _o is int i ? (double)i
|
||||
: _o is float f ? (double)f
|
||||
: _o is double dbl ? dbl
|
||||
: throw new Exception("Not a double");
|
||||
}
|
||||
|
||||
public string GetString()
|
||||
{
|
||||
return _o == null ? null
|
||||
: _o is string str ? str
|
||||
: throw new Exception("Not a string");
|
||||
}
|
||||
|
||||
public string GetUntypedNumber()
|
||||
{
|
||||
return _o is int i ? i.ToString()
|
||||
: _o is float f ? f.ToString()
|
||||
: _o is double dbl ? dbl.ToString()
|
||||
: _o is decimal dec ? dec.ToString()
|
||||
: throw new Exception("Not valid untyped number");
|
||||
}
|
||||
|
||||
public bool TryGetProperty(string value, out IUntypedObject result)
|
||||
{
|
||||
if (_o is object o && o.GetType().GetProperties().Any(pi => pi.Name == value))
|
||||
{
|
||||
PropertyInfo pi = o.GetType().GetProperty(value);
|
||||
object prop = pi.GetValue(_o);
|
||||
|
||||
result = new TestUO(prop);
|
||||
return true;
|
||||
}
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetPropertyNames(out IEnumerable<string> propertyNames)
|
||||
{
|
||||
if (_o is object o)
|
||||
{
|
||||
propertyNames = o.GetType().GetProperties().Select(pi => pi.Name);
|
||||
return true;
|
||||
}
|
||||
|
||||
propertyNames = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче