>> JSON(ParseJSON("{""a"": 1}"), JSONFormat.IgnoreUnsupportedTypes)
"{""a"":1}"
This commit is contained in:
Luc Genetier 2024-09-25 20:09:23 +02:00 коммит произвёл GitHub
Родитель 93daa65514
Коммит ee0ca2c67e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
11 изменённых файлов: 510 добавлений и 35 удалений

Просмотреть файл

@ -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;
}
}
}
}