Introduce IsType, AsType and ParseJSON overloads with custom types (#2569)

Support IsType, AsType and ParseJSON functions with custom type as input
parameter

```
>> IsType(ParseJSON("42"), Number)
true

>> AsType(ParseJSON("42"), Number)
42

>> ParseJSON("42", Number)
42

>> AsType(ParseJSON("{ ""a"": 42 }"), Type({a: Number}))
{a : 42}

```

---------

Co-authored-by: Luc Genetier <69138830+LucGenetier@users.noreply.github.com>
This commit is contained in:
Adithya Selvaprithiviraj 2024-08-20 19:31:55 -07:00 коммит произвёл GitHub
Родитель 930ca694c5
Коммит 251e918f32
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
33 изменённых файлов: 1371 добавлений и 24 удалений

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

@ -861,7 +861,7 @@ namespace Microsoft.PowerFx.Connectors
{
ExpressionError er = null;
if (result is ErrorValue ev && (er = ev.Errors.FirstOrDefault(e => e.Kind == ErrorKind.Network || e.Kind == ErrorKind.InvalidJSON)) != null)
if (result is ErrorValue ev && (er = ev.Errors.FirstOrDefault(e => e.Kind == ErrorKind.Network || e.Kind == ErrorKind.InvalidJSON || e.Kind == ErrorKind.InvalidArgument)) != null)
{
runtimeContext.ExecutionLogger?.LogError($"{this.LogFunction(nameof(PostProcessResultAsync))}, ErrorValue is returned with {er.Message}");
ExpressionError newError = er is HttpExpressionError her

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

@ -78,6 +78,11 @@ namespace Microsoft.PowerFx.Core.Binding
/// </summary>
TypeName,
Lim
/// <summary>
/// This BindKind applies to globally defined NamedTypes is not used for any data.
/// </summary>
NamedType,
Lim,
}
}

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

@ -19,7 +19,9 @@ using Microsoft.PowerFx.Core.Functions.Delegation;
using Microsoft.PowerFx.Core.Functions.Delegation.DelegationStrategies;
using Microsoft.PowerFx.Core.Glue;
using Microsoft.PowerFx.Core.Localization;
using Microsoft.PowerFx.Core.Syntax.Visitors;
using Microsoft.PowerFx.Core.Texl;
using Microsoft.PowerFx.Core.Texl.Builtins;
using Microsoft.PowerFx.Core.Types;
using Microsoft.PowerFx.Core.Types.Enums;
using Microsoft.PowerFx.Core.Utils;
@ -2643,6 +2645,30 @@ namespace Microsoft.PowerFx.Core.Binding
_txb.SetType(node, DType.ObjNull);
}
public override void Visit(TypeLiteralNode node)
{
AssertValid();
Contracts.AssertValue(node);
if (_nameResolver == null)
{
_txb.SetType(node, DType.Unknown);
return;
}
var type = DTypeVisitor.Run(node.TypeRoot, _nameResolver);
if (type.IsValid)
{
_txb.SetType(node, type);
}
else
{
_txb.SetType(node, DType.Error);
_txb.ErrorContainer.Error(node, TexlStrings.ErrTypeLiteral_InvalidTypeDefinition, node.ToString());
}
}
public override void Visit(BoolLitNode node)
{
AssertValid();
@ -4296,6 +4322,34 @@ namespace Microsoft.PowerFx.Core.Binding
_txb.SetInfo(node, new CallInfo(maybeFunc, node, null, default, false, _currentScope.Nest));
_txb.SetType(node, maybeFunc.ReturnType);
}
// checks if the call node best matches function overloads with UntypedObject/JSON
private bool MatchOverloadWithUntypedOrJSONConversionFunctions(CallNode node, TexlFunction maybeFunc)
{
Contracts.AssertValue(node);
Contracts.AssertValue(maybeFunc);
Contracts.Assert(maybeFunc.HasTypeArgs);
if (maybeFunc.Name == AsTypeFunction.AsTypeInvariantFunctionName &&
_txb.GetType(node.Args.Children[0]) == DType.UntypedObject)
{
return true;
}
if (maybeFunc.Name == IsTypeFunction_UO.IsTypeInvariantFunctionName &&
_txb.GetType(node.Args.Children[0]) == DType.UntypedObject)
{
return true;
}
if (maybeFunc.Name == ParseJSONFunction.ParseJSONInvariantFunctionName &&
node.Args.Count > 1)
{
return true;
}
return false;
}
public override bool PreVisit(CallNode node)
@ -4346,7 +4400,10 @@ namespace Microsoft.PowerFx.Core.Binding
// If there are no overloads with lambdas or identifiers, we can continue the visitation and
// yield to the normal overload resolution.
var overloadsWithLambdasOrIdentifiers = overloads.Where(func => func.HasLambdas || func.HasColumnIdentifiers);
var overloadsWithLambdasOrIdentifiers = overloads.Where(func => func.HasLambdas || func.HasColumnIdentifiers);
var overloadsWithTypeArgs = overloads.Where(func => func.HasTypeArgs);
if (!overloadsWithLambdasOrIdentifiers.Any())
{
// We may still need a scope to determine inline-record types
@ -4377,6 +4434,25 @@ namespace Microsoft.PowerFx.Core.Binding
startArg++;
}
if (overloadsWithTypeArgs.Any() && node.Args.Count > 1)
{
var nodeInp = node.Args.Children[0];
nodeInp.Accept(this);
Contracts.Assert(overloadsWithTypeArgs.Count() == 1);
var functionWithTypeArg = overloadsWithTypeArgs.First();
if (MatchOverloadWithUntypedOrJSONConversionFunctions(node, functionWithTypeArg))
{
PreVisitTypeArgAndProccesCallNode(node, functionWithTypeArg);
FinalizeCall(node);
return false;
}
startArg++;
}
PreVisitHeadNode(node);
PreVisitBottomUp(node, startArg, maybeScope);
FinalizeCall(node);
@ -5034,6 +5110,96 @@ namespace Microsoft.PowerFx.Core.Binding
}
_txb.SetType(node, returnType);
}
// Method to previsit type arg of callnode if it is determined as untyped/json conversion function
private void PreVisitTypeArgAndProccesCallNode(CallNode node, TexlFunction func)
{
AssertValid();
Contracts.AssertValue(node);
Contracts.AssertValue(func);
Contracts.Assert(func.HasTypeArgs);
var args = node.Args.Children.ToArray();
var argCount = args.Count();
Contracts.AssertValue(_txb.GetType(args[0]));
if (argCount < func.MinArity || argCount > func.MaxArity)
{
ArityError(func.MinArity, func.MaxArity, node, argCount, _txb.ErrorContainer);
_txb.SetInfo(node, new CallInfo(func, node));
_txb.SetType(node, DType.Error);
return;
}
Contracts.Assert(argCount > 1);
Contracts.AssertValue(args[1]);
if (args[1] is FirstNameNode typeName)
{
if (_nameResolver.LookupType(typeName.Ident.Name, out var typeArgType))
{
_txb.SetType(typeName, typeArgType._type);
_txb.SetInfo(typeName, FirstNameInfo.Create(typeName, new NameLookupInfo(BindKind.NamedType, typeArgType._type, DPath.Root, 0)));
}
else
{
_txb.ErrorContainer.Error(args[1], TexlStrings.ErrInvalidName, typeName.Ident.Name.Value);
_txb.SetType(args[1], DType.Error);
_txb.SetInfo(typeName, FirstNameInfo.Create(typeName, new NameLookupInfo(BindKind.NamedType, DType.Error, DPath.Root, 0)));
_txb.ErrorContainer.Error(node, TexlStrings.ErrInvalidArgumentExpectedType, typeName.Ident.Name.Value);
_txb.SetInfo(node, new CallInfo(func, node));
_txb.SetType(node, DType.Error);
}
}
else if (args[1] is TypeLiteralNode typeLiteral)
{
typeLiteral.Accept(this);
}
else
{
_txb.ErrorContainer.Error(args[1], TexlStrings.ErrInvalidArgumentExpectedType, args[1]);
_txb.SetType(args[1], DType.Error);
}
PostVisit(node.Args);
var info = _txb.GetInfo(node);
// If PreVisit resulted in errors for the node (and a non-null CallInfo),
// we're done -- we have a match and appropriate errors logged already.
if (_txb.ErrorContainer.HasErrors(node) || _txb.ErrorContainer.HasErrors(node.Head.Token))
{
Contracts.Assert(info != null);
return;
}
Contracts.AssertNull(info);
_txb.SetInfo(node, new CallInfo(func, node));
var returnType = func.ReturnType;
var argTypes = args.Select(_txb.GetType).ToArray();
bool fArgsValid;
var checkErrorContainer = new ErrorContainer();
// Typecheck the invocation and infer the return type.
fArgsValid = func.HandleCheckInvocation(_txb, args, argTypes, checkErrorContainer, out returnType, out var _);
if (checkErrorContainer?.HasErrors() == true)
{
_txb.ErrorContainer.MergeErrors(checkErrorContainer.GetErrors());
}
if (!fArgsValid)
{
_txb.ErrorContainer.Error(DocumentErrorSeverity.Severe, node.Head.Token, TexlStrings.ErrInvalidArgs_Func, func.Name);
}
_txb.SetType(node, returnType);
}
private void PreVisitBottomUp(CallNode node, int argCountVisited, Scope scopeNew = null)

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

@ -388,6 +388,10 @@ namespace Microsoft.PowerFx.Core.Functions
public bool IsDeprecatedOrInternalFunction => this is IHasUnsupportedFunctions sdf && (sdf.IsDeprecated || sdf.IsInternal);
// This property is true for a function if and only if there is an argIndex such that func.ArgIsType(argIndex) == true
// Eg: for example TypedParseJSON.ArgIsType(1) == true and hence TypedParseJSON.HasTypeArg is true
public virtual bool HasTypeArgs => false;
public TexlFunction(
DPath theNamespace,
string name,
@ -514,6 +518,11 @@ namespace Microsoft.PowerFx.Core.Functions
return SupportsParamCoercion && (argIndex <= MinArity || argIndex <= MaxArity);
}
public virtual bool ArgIsType(int argIndex)
{
return false;
}
private bool CheckTypesCore(CheckTypesContext context, TexlNode[] args, DType[] argTypes, IErrorContainer errors, out DType returnType, out Dictionary<TexlNode, DType> nodeToCoercedTypeMap)
{
Contracts.AssertValue(args);
@ -531,8 +540,8 @@ namespace Microsoft.PowerFx.Core.Functions
// Type check the args
for (var i = 0; i < count; i++)
{
// Identifiers don't have a type
if (ParameterCanBeIdentifier(args[i], i, context.Features))
// Identifiers don't have a type and type arguments need not be type checked
if (ParameterCanBeIdentifier(args[i], i, context.Features) || ArgIsType(i))
{
continue;
}
@ -567,7 +576,7 @@ namespace Microsoft.PowerFx.Core.Functions
for (var i = count; i < args.Length; i++)
{
// Identifiers don't have a type
if (ParameterCanBeIdentifier(args[i], i, context.Features))
if (ParameterCanBeIdentifier(args[i], i, context.Features) || ArgIsType(i))
{
continue;
}

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

@ -80,6 +80,14 @@ namespace Microsoft.PowerFx.Core.IR
Contracts.AssertValue(context);
return MaybeInjectCoercion(node, new TextLiteralNode(context.GetIRContext(node), node.Value), context);
}
public override IntermediateNode Visit(TypeLiteralNode node, IRTranslatorContext context)
{
Contracts.AssertValue(node);
Contracts.AssertValue(context);
return new TextLiteralNode(IRContext.NotInSource(FormulaType.String), context.Binding.GetType(node).ToString());
}
public override IntermediateNode Visit(NumLitNode node, IRTranslatorContext context)
@ -672,6 +680,11 @@ namespace Microsoft.PowerFx.Core.IR
result = new ResolvedObjectNode(context.GetIRContext(node), info.Data);
break;
}
case BindKind.NamedType:
{
return new TextLiteralNode(IRContext.NotInSource(FormulaType.String), context.Binding.GetType(node).ToString());
}
default:

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

@ -466,10 +466,18 @@ namespace Microsoft.PowerFx.Core.Localization
public static StringGetter IsTypeArg1 = (b) => StringResources.Get("IsTypeArg1", b);
public static StringGetter IsTypeArg2 = (b) => StringResources.Get("IsTypeArg2", b);
public static StringGetter AboutIsTypeUO = (b) => StringResources.Get("AboutIsTypeUO", b);
public static StringGetter IsTypeUOArg1 = (b) => StringResources.Get("IsTypeUOArg1", b);
public static StringGetter IsTypeUOArg2 = (b) => StringResources.Get("IsTypeUOArg2", b);
public static StringGetter AboutAsType = (b) => StringResources.Get("AboutAsType", b);
public static StringGetter AsTypeArg1 = (b) => StringResources.Get("AsTypeArg1", b);
public static StringGetter AsTypeArg2 = (b) => StringResources.Get("AsTypeArg2", b);
public static StringGetter AboutAsTypeUO = (b) => StringResources.Get("AboutAsTypeUO", b);
public static StringGetter AsTypeUOArg1 = (b) => StringResources.Get("AsTypeUOArg1", b);
public static StringGetter AsTypeUOArg2 = (b) => StringResources.Get("AsTypeUOArg2", b);
public static StringGetter AboutWith = (b) => StringResources.Get("AboutWith", b);
public static StringGetter WithArg1 = (b) => StringResources.Get("WithArg1", b);
public static StringGetter WithArg2 = (b) => StringResources.Get("WithArg2", b);
@ -482,6 +490,10 @@ namespace Microsoft.PowerFx.Core.Localization
public static StringGetter AboutParseJSON = (b) => StringResources.Get("AboutParseJSON", b);
public static StringGetter ParseJSONArg1 = (b) => StringResources.Get("ParseJSONArg1", b);
public static StringGetter AboutTypedParseJSON = (b) => StringResources.Get("AboutTypedParseJSON", b);
public static StringGetter TypedParseJSONArg1 = (b) => StringResources.Get("TypedParseJSONArg1", b);
public static StringGetter TypedParseJSONArg2 = (b) => StringResources.Get("TypedParseJSONArg2", b);
public static StringGetter AboutFileInfo = (b) => StringResources.Get("AboutFileInfo", b);
public static StringGetter FileInfoArg1 = (b) => StringResources.Get("FileInfoArg1", b);
@ -829,5 +841,7 @@ namespace Microsoft.PowerFx.Core.Localization
public static ErrorResourceKey ErrSummarizeDataSourceScopeNotSupported = new ErrorResourceKey("ErrSummarizeDataSourceScopeNotSupported");
public static ErrorResourceKey ErrInvalidDataSourceForFunction = new ErrorResourceKey("ErrInvalidDataSourceForFunction");
public static ErrorResourceKey ErrInvalidArgumentExpectedType = new ErrorResourceKey("ErrInvalidArgumentExpectedType");
public static ErrorResourceKey ErrUnsupportedTypeInTypeArgument = new ErrorResourceKey("ErrUnsupportedTypeInTypeArgument");
}
}

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

@ -88,6 +88,7 @@ namespace Microsoft.PowerFx
(NumberIsFloat ? TexlParser.Flags.NumberIsFloat : 0) |
(DisableReservedKeywords ? TexlParser.Flags.DisableReservedKeywords : 0) |
(TextFirst ? TexlParser.Flags.TextFirst : 0) |
(AllowParseAsTypeLiteral ? TexlParser.Flags.AllowTypeLiteral : 0) |
(features.PowerFxV1CompatibilityRules ? TexlParser.Flags.PFxV1 : 0);
var result = TexlParser.ParseScript(script, features, Culture, flags);

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

@ -320,14 +320,13 @@ namespace Microsoft.PowerFx.Core.Parser
var result = ParseExpr(Precedence.None);
if (result is TypeLiteralNode typeLiteralNode)
{
if (typeLiteralNode.IsValid(out var errors))
if (typeLiteralNode.IsValid(out _))
{
definedTypes.Add(new DefinedType(thisIdentifier.As<IdentToken>(), typeLiteralNode, true));
}
else
{
definedTypes.Add(new DefinedType(thisIdentifier.As<IdentToken>(), typeLiteralNode, false));
CollectionUtils.Add(ref _errors, errors);
}
continue;
@ -1189,7 +1188,14 @@ namespace Microsoft.PowerFx.Core.Parser
{
if (ident.Token.As<IdentToken>().Name.Value == "Type" && _flagsMode.Peek().HasFlag(Flags.AllowTypeLiteral))
{
return ParseTypeLiteral();
var typeLiteralNode = ParseTypeLiteral();
if (!typeLiteralNode.IsValid(out var err))
{
CollectionUtils.Add(ref _errors, err);
}
return typeLiteralNode;
}
trivia = ParseTrivia();

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

@ -325,6 +325,11 @@ namespace Microsoft.PowerFx.Syntax
public override void Visit(SelfNode node)
{
SetCurrentNodeAsResult(node);
}
public override void Visit(TypeLiteralNode node)
{
SetCurrentNodeAsResult(node);
}
}
}

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

@ -259,7 +259,10 @@ namespace Microsoft.PowerFx.Core.Texl
public static readonly TexlFunction UTCToday = _featureGateFunctions.Add(new UTCTodayFunction());
public static readonly TexlFunction BooleanL = _featureGateFunctions.Add(new BooleanLFunction());
public static readonly TexlFunction BooleanL_T = _featureGateFunctions.Add(new BooleanLFunction_T());
public static readonly TexlFunction Summarize = _featureGateFunctions.Add(new SummarizeFunction());
public static readonly TexlFunction Summarize = _featureGateFunctions.Add(new SummarizeFunction());
public static readonly TexlFunction AsType_UO = _featureGateFunctions.Add(new AsTypeFunction_UO());
public static readonly TexlFunction IsType_UO = _featureGateFunctions.Add(new IsTypeFunction_UO());
public static readonly TexlFunction TypedParseJSON = _featureGateFunctions.Add(new TypedParseJSONFunction());
// Slow API, only use for backward compatibility
#pragma warning disable CS0618 // Type or member is obsolete

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

@ -141,6 +141,20 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
// AsType should always be marked as valid regardless of it being async and impure.
return true;
}
}
// AsType(UntypedObject:O, Type:U): ?
internal class AsTypeFunction_UO : UntypedOrJSONConversionFunction
{
public AsTypeFunction_UO()
: base(AsTypeFunction.AsTypeInvariantFunctionName, TexlStrings.AboutAsTypeUO, DType.Error, 2, DType.UntypedObject, DType.Error)
{
}
public override IEnumerable<TexlStrings.StringGetter[]> GetSignatures()
{
yield return new[] { TexlStrings.AsTypeUOArg1, TexlStrings.AsTypeUOArg2 };
}
}
}

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

@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
using System.Collections.Generic;
using Microsoft.PowerFx.Core.App.ErrorContainers;
using Microsoft.PowerFx.Core.Functions;
using Microsoft.PowerFx.Core.Localization;
using Microsoft.PowerFx.Core.Types;
using Microsoft.PowerFx.Core.Utils;
using Microsoft.PowerFx.Syntax;
#pragma warning disable SA1402 // File may only contain a single type
#pragma warning disable SA1649 // File name should match first type name
namespace Microsoft.PowerFx.Core.Texl.Builtins
{
// IsType(UntypedObject:O, Type:U): Boolean
internal class IsTypeFunction_UO : UntypedOrJSONConversionFunction
{
public const string IsTypeInvariantFunctionName = "IsType";
public IsTypeFunction_UO()
: base(IsTypeInvariantFunctionName, TexlStrings.AboutIsTypeUO, DType.Boolean, 2, DType.UntypedObject, DType.Error)
{
}
public override IEnumerable<TexlStrings.StringGetter[]> GetSignatures()
{
yield return new[] { TexlStrings.IsTypeUOArg1, TexlStrings.IsTypeUOArg2 };
}
public override bool CheckTypes(CheckTypesContext context, TexlNode[] args, DType[] argTypes, IErrorContainer errors, out DType returnType, out Dictionary<TexlNode, DType> nodeToCoercedTypeMap)
{
Contracts.AssertValue(args);
Contracts.AssertAllValues(args);
Contracts.AssertValue(argTypes);
Contracts.Assert(args.Length == 2);
Contracts.Assert(argTypes.Length == 2);
Contracts.AssertValue(errors);
if (!base.CheckTypes(context, args, argTypes, errors, out returnType, out nodeToCoercedTypeMap))
{
return false;
}
returnType = DType.Boolean;
return true;
}
}
}
#pragma warning restore SA1402 // File may only contain a single type
#pragma warning restore SA1649 // File name should match first type name

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

@ -2,9 +2,16 @@
// Licensed under the MIT license.
using System.Collections.Generic;
using System.Linq;
using Microsoft.PowerFx.Core.App.ErrorContainers;
using Microsoft.PowerFx.Core.Binding;
using Microsoft.PowerFx.Core.Errors;
using Microsoft.PowerFx.Core.Functions;
using Microsoft.PowerFx.Core.Localization;
using Microsoft.PowerFx.Core.Types;
using Microsoft.PowerFx.Core.Utils;
using Microsoft.PowerFx.Syntax;
using Microsoft.PowerFx.Types;
namespace Microsoft.PowerFx.Core.Texl.Builtins
{
@ -45,4 +52,18 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
yield return new[] { TexlStrings.IndexArg1, TexlStrings.IndexArg2 };
}
}
// ParseJSON(JsonString:s, Type:U): ?
internal class TypedParseJSONFunction : UntypedOrJSONConversionFunction
{
public TypedParseJSONFunction()
: base(ParseJSONFunction.ParseJSONInvariantFunctionName, TexlStrings.AboutTypedParseJSON, DType.Error, 2, DType.String, DType.Error)
{
}
public override IEnumerable<TexlStrings.StringGetter[]> GetSignatures()
{
yield return new[] { TexlStrings.TypedParseJSONArg1, TexlStrings.TypedParseJSONArg2 };
}
}
}

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

@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.PowerFx.Core.App.ErrorContainers;
using Microsoft.PowerFx.Core.Binding;
using Microsoft.PowerFx.Core.Errors;
using Microsoft.PowerFx.Core.Functions;
using Microsoft.PowerFx.Core.IR;
using Microsoft.PowerFx.Core.Localization;
using Microsoft.PowerFx.Core.Types;
using Microsoft.PowerFx.Core.Utils;
using Microsoft.PowerFx.Syntax;
using Microsoft.PowerFx.Types;
namespace Microsoft.PowerFx.Core.Texl.Builtins
{
internal abstract class UntypedOrJSONConversionFunction : BuiltinFunction
{
public override bool IsSelfContained => true;
public override bool SupportsParamCoercion => false;
public override bool HasTypeArgs => true;
public override bool ArgIsType(int argIndex)
{
Contracts.Assert(HasTypeArgs);
return argIndex == 1;
}
private static readonly ISet<DType> SupportedJSONTypes = new HashSet<DType> { DType.Boolean, DType.Number, DType.Decimal, DType.Date, DType.DateTime, DType.DateTimeNoTimeZone, DType.Time, DType.String, DType.Guid, DType.Hyperlink, DType.UntypedObject };
public UntypedOrJSONConversionFunction(string name, TexlStrings.StringGetter description, DType returnType, int arityMax, params DType[] paramTypes)
: base(name, description, FunctionCategories.Text, returnType, 0, 2, arityMax, paramTypes)
{
}
public override bool CheckTypes(CheckTypesContext context, TexlNode[] args, DType[] argTypes, IErrorContainer errors, out DType returnType, out Dictionary<TexlNode, DType> nodeToCoercedTypeMap)
{
Contracts.AssertValue(args);
Contracts.AssertAllValues(args);
Contracts.AssertValue(argTypes);
Contracts.Assert(args.Length >= 2 && args.Length <= MaxArity);
Contracts.Assert(argTypes.Length >= 2 && argTypes.Length <= MaxArity);
Contracts.AssertValue(errors);
if (!base.CheckTypes(context, args, argTypes, errors, out returnType, out nodeToCoercedTypeMap))
{
return false;
}
returnType = argTypes[1];
return true;
}
public override void CheckSemantics(TexlBinding binding, TexlNode[] args, DType[] argTypes, IErrorContainer errors)
{
Contracts.AssertValue(args);
Contracts.AssertAllValues(args);
Contracts.AssertValue(argTypes);
Contracts.Assert(args.Length >= 2 && args.Length <= MaxArity);
Contracts.Assert(argTypes.Length >= 2 && argTypes.Length <= MaxArity);
Contracts.AssertValue(errors);
base.CheckSemantics(binding, args, argTypes, errors);
CheckTypeArgHasSupportedTypes(args[1], argTypes[1], errors);
}
private void CheckTypeArgHasSupportedTypes(TexlNode typeArg, DType type, IErrorContainer errors)
{
// Dataverse types may contain fields with ExpandInfo that may have self / mutually recursive reference
// we allow these in type check phase by ignoring validation of types in such fields.
if (type.HasExpandInfo)
{
return;
}
if ((type.IsRecordNonObjNull || type.IsTableNonObjNull) && type.TypeTree != null)
{
type.TypeTree.ToList().ForEach(t => CheckTypeArgHasSupportedTypes(typeArg, t.Value, errors));
return;
}
if (!SupportedJSONTypes.Contains(type))
{
errors.EnsureError(DocumentErrorSeverity.Severe, typeArg, TexlStrings.ErrUnsupportedTypeInTypeArgument, type.Kind);
return;
}
}
}
}

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

@ -17,6 +17,9 @@ namespace Microsoft.PowerFx
{
config.AddFunction(new ParseJSONFunctionImpl());
config.AddFunction(new JsonFunctionImpl());
config.AddFunction(new AsTypeFunction_UOImpl());
config.AddFunction(new IsTypeFunction_UOImpl());
config.AddFunction(new TypedParseJSONFunctionImpl());
}
}
}

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

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using Microsoft.PowerFx.Core.IR;
@ -17,6 +18,13 @@ namespace Microsoft.PowerFx.Types
public bool NumberIsFloat { get; init; } = false;
public bool ReturnUnknownRecordFieldsAsUntypedObjects { get; init; } = false;
// JSON value input of type object may contain fields with values that not present in the target schema.
// This attribute controls if the result such conversion should be valid or an error.
// This is false (i.e we dont allow additional field in input) when used for functions like AsType_UO, IsType_UO and TypedParseJSON
public bool AllowUnknownRecordFields { get; init; } = true;
public TimeZoneInfo ResultTimeZone { get; init; } = TimeZoneInfo.Utc;
}
internal class FormulaValueJsonSerializerWorkingData
@ -58,7 +66,7 @@ namespace Microsoft.PowerFx.Types
{
Message = $"{pfxje.GetType().Name} {pfxje.Message}",
Span = new Syntax.Span(0, 0),
Kind = ErrorKind.InvalidJSON
Kind = ErrorKind.InvalidArgument
});
}
}
@ -76,7 +84,7 @@ namespace Microsoft.PowerFx.Types
public static FormulaValue FromJson(JsonElement element, FormulaValueJsonSerializerSettings settings, FormulaType formulaType = null)
{
return FromJson(element, settings, new FormulaValueJsonSerializerWorkingData(), formulaType);
return FromJson(element, settings, new FormulaValueJsonSerializerWorkingData(), formulaType);
}
internal static FormulaValue FromJson(JsonElement element, FormulaValueJsonSerializerSettings settings, FormulaValueJsonSerializerWorkingData data, FormulaType formulaType = null)
@ -126,18 +134,29 @@ namespace Microsoft.PowerFx.Types
dt2 = dt2.ToUniversalTime();
}
if (dt2.Kind == DateTimeKind.Unspecified)
if (settings.ResultTimeZone == TimeZoneInfo.Utc && dt2.Kind == DateTimeKind.Unspecified)
{
dt2 = new DateTime(dt2.Ticks, DateTimeKind.Utc);
}
return DateTimeValue.New(dt2);
return DateTimeValue.New(DateTimeValue.GetConvertedDateTimeValue(dt2, settings.ResultTimeZone));
}
else if (formulaType is DateTimeNoTimeZoneType)
{
DateTime dt3 = element.GetDateTime();
return DateTimeValue.New(TimeZoneInfo.ConvertTimeToUtc(dt3));
}
else if (formulaType is TimeType)
{
var timeString = element.GetString();
if (TimeSpan.TryParseExact(timeString, @"hh\:mm\:ss\.FFFFFFF", CultureInfo.InvariantCulture, TimeSpanStyles.None, out var res) ||
TimeSpan.TryParseExact(timeString, @"hh\:mm\:ss", CultureInfo.InvariantCulture, TimeSpanStyles.None, out res))
{
return TimeValue.New(res);
}
throw new PowerFxJsonException($"Time '{timeString}' could not be parsed", data.Path);
}
else if (formulaType is BlobType)
{
return FormulaValue.NewBlob(element.GetBytesFromBase64());
@ -226,6 +245,11 @@ namespace Microsoft.PowerFx.Types
if (recordType?.TryGetFieldType(name, out fieldType) == false)
{
if (!settings.AllowUnknownRecordFields)
{
throw new PowerFxJsonException($"Unexpected field '{name}' found in JSONObject", $"{data.Path}/{name}");
}
// if we expect a record type and the field is unknown, let's ignore it like in Power Apps
if (!settings.ReturnUnknownRecordFieldsAsUntypedObjects)
{

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

@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
using System;
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
{
internal class AsTypeFunction_UOImpl : AsTypeFunction_UO, IAsyncTexlFunction4
{
public async Task<FormulaValue> InvokeAsync(TimeZoneInfo timezoneInfo, FormulaType ft, FormulaValue[] args, CancellationToken cancellationToken)
{
Contracts.Assert(args.Length == 2);
var irContext = IRContext.NotInSource(ft);
var typeString = (StringValue)args[1];
try
{
return JSONFunctionUtils.ConvertUntypedObjectToFormulaValue(irContext, args[0], typeString, timezoneInfo);
}
catch (Exception e)
{
return new ErrorValue(irContext, new ExpressionError()
{
Message = $"{e.GetType().Name} {e.Message}",
Span = irContext.SourceContext,
Kind = ErrorKind.InvalidArgument
});
}
}
}
}

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

@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
using System;
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
{
internal class IsTypeFunction_UOImpl : IsTypeFunction_UO, IAsyncTexlFunction4
{
public async Task<FormulaValue> InvokeAsync(TimeZoneInfo timezoneInfo, FormulaType ft, FormulaValue[] args, CancellationToken cancellationToken)
{
Contracts.Assert(args.Length == 2);
var irContext = IRContext.NotInSource(FormulaType.UntypedObject);
var typeString = (StringValue)args[1];
try
{
var fv = JSONFunctionUtils.ConvertUntypedObjectToFormulaValue(irContext, args[0], typeString, timezoneInfo);
if (fv is BlankValue || fv is ErrorValue)
{
return fv;
}
else
{
return BooleanValue.New(true);
}
}
catch
{
return BooleanValue.New(false);
}
}
}
}

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

@ -0,0 +1,77 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.PowerFx.Core.IR;
using Microsoft.PowerFx.Core.Types;
using Microsoft.PowerFx.Functions;
using Microsoft.PowerFx.Types;
namespace Microsoft.PowerFx.Core.Utils
{
internal class JSONFunctionUtils
{
public static FormulaValue ConvertUntypedObjectToFormulaValue(IRContext irContext, FormulaValue input, StringValue typeString, TimeZoneInfo timeZoneInfo)
{
if (input is BlankValue || input is ErrorValue)
{
return input;
}
if (!DType.TryParse(typeString.Value, out DType dtype))
{
// This should never happen, as the typeString will be created by the IR
return new ErrorValue(irContext, new ExpressionError()
{
Message = $"Internal error: Unable to parse type argument",
Span = irContext.SourceContext,
Kind = ErrorKind.Internal
});
}
var untypedObjectValue = (UntypedObjectValue)input;
var uo = untypedObjectValue.Impl;
var jsElement = ((JsonUntypedObject)uo)._element;
var settings = new FormulaValueJsonSerializerSettings { AllowUnknownRecordFields = false, ResultTimeZone = timeZoneInfo };
return FormulaValueJSON.FromJson(jsElement, settings, FormulaType.Build(dtype));
}
public static FormulaValue ConvertJSONStringToFormulaValue(IRContext irContext, FormulaValue input, StringValue typeString, TimeZoneInfo timeZoneInfo)
{
if (input is BlankValue || input is ErrorValue)
{
return input;
}
if (input is not StringValue)
{
return new ErrorValue(irContext, new ExpressionError()
{
Message = "Runtime type mismatch",
Span = irContext.SourceContext,
Kind = ErrorKind.InvalidArgument
});
}
if (!DType.TryParse(typeString.Value, out DType dtype))
{
// This should never happen, as the typeString will be created by the IR
return new ErrorValue(irContext, new ExpressionError()
{
Message = $"Internal error: Unable to parse type argument",
Span = irContext.SourceContext,
Kind = ErrorKind.Internal
});
}
var json = ((StringValue)input).Value;
var settings = new FormulaValueJsonSerializerSettings { AllowUnknownRecordFields = false, ResultTimeZone = timeZoneInfo };
return FormulaValueJSON.FromJson(json, settings, FormulaType.Build(dtype));
}
}
}

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

@ -61,7 +61,7 @@ namespace Microsoft.PowerFx.Core.Texl.Builtins
{
Message = $"The Json could not be parsed: {ex.Message}",
Span = irContext.SourceContext,
Kind = ErrorKind.InvalidArgument
Kind = ErrorKind.InvalidJSON
});
}
}

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

@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
using System;
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
{
internal class TypedParseJSONFunctionImpl : TypedParseJSONFunction, IAsyncTexlFunction4
{
public async Task<FormulaValue> InvokeAsync(TimeZoneInfo timezoneInfo, FormulaType ft, FormulaValue[] args, CancellationToken cancellationToken)
{
Contracts.Assert(args.Length == 2);
var irContext = IRContext.NotInSource(ft);
var typeString = (StringValue)args[1];
return JSONFunctionUtils.ConvertJSONStringToFormulaValue(irContext, args[0], typeString, timezoneInfo);
}
}
}

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

@ -1807,13 +1807,31 @@
<value>typeTable</value>
<comment>function_parameter - Second argument of the IsType function - The Entity table representing the type that we wish to compare the value to. For example, if the author has a CDS data source named Account, that table could be passed here (eg IsType(myVal, Account)).</comment>
</data>
<data name="AboutIsTypeUO" xml:space="preserve">
<value>Returns true if the provided untyped value is of the given type.</value>
<comment>Description of the 'IsType' function.</comment>
</data>
<data name="IsTypeUOArg1" xml:space="preserve">
<value>untypedValue</value>
<comment>function_parameter - First argument of the IsTypeUO function - The untyped value to be inspected.</comment>
</data>
<data name="IsTypeUOArg2" xml:space="preserve">
<value>type</value>
<comment>function_parameter - Second argument of the IsTypeUO function - Type value - either a typeliteral or global named type.</comment>
</data>
<data name="AboutIsType_untypedValue" xml:space="preserve">
<value>The untyped value to check if it can be casted as specified type.</value>
</data>
<data name="AboutIsType_type" xml:space="preserve">
<value>The type that we wish to check the untypedValue.</value>
</data>
<data name="AboutAsType" xml:space="preserve">
<value>Uses the provided value as the given type.</value>
<comment>Description of the 'AsType' function.</comment>
</data>
<data name="AsTypeArg1" xml:space="preserve">
<value>value</value>
<comment>function_parameter - First argument of the IsType function - The polymorphic value to be used as the new type.</comment>
<comment>function_parameter - First argument of the AsType function - The polymorphic value to be used as the new type.</comment>
</data>
<data name="AsTypeArg2" xml:space="preserve">
<value>typeTable</value>
@ -1825,6 +1843,24 @@
<data name="AboutAsType_typeTable" xml:space="preserve">
<value>The entity table representing the type we wish the value to be used as.</value>
</data>
<data name="AboutAsTypeUO" xml:space="preserve">
<value>Checks and uses the provided Untyped value as the given type.</value>
<comment>Description of the 'AsType' function.</comment>
</data>
<data name="AsTypeUOArg1" xml:space="preserve">
<value>untypedValue</value>
<comment>function_parameter - First argument of the AsType function - untyped value to be used as the new type.</comment>
</data>
<data name="AsTypeUOArg2" xml:space="preserve">
<value>type</value>
<comment>function_parameter - Second argument of the AsType function - The type that we wish the value to be used as.</comment>
</data>
<data name="AboutAsType_untypedValue" xml:space="preserve">
<value>The untyped value to cast as a new type.</value>
</data>
<data name="AboutAsType_type" xml:space="preserve">
<value>The type that we wish the value to be used as.</value>
</data>
<data name="AboutWith" xml:space="preserve">
<value>Executes the formula provided as second parameter using the scope provided by the first.</value>
<comment>Description of the 'With' function.</comment>
@ -2673,6 +2709,24 @@
<data name="AboutParseJSON_input" xml:space="preserve">
<value>A JSON string to process.</value>
</data>
<data name="AboutTypedParseJSON" xml:space="preserve">
<value>Converts a JSON string into a typed object.</value>
<comment>Description of 'ParseJSON' function overload that converts a string input to typed object.</comment>
</data>
<data name="TypedParseJSONArg1" xml:space="preserve">
<value>string</value>
<comment>function_parameter - First argument of the ParseJSON function - String type.</comment>
</data>
<data name="AboutParseJSON_string" xml:space="preserve">
<value>A JSON string to convert into typed object.</value>
</data>
<data name="TypedParseJSONArg2" xml:space="preserve">
<value>type</value>
<comment>function_parameter - Second argument of the typed ParseJSON function overload - Inlined type definition or NamedType.</comment>
</data>
<data name="AboutParseJSON_type" xml:space="preserve">
<value>An inlined type definiton or a globally defined named type.</value>
</data>
<data name="AboutIndex" xml:space="preserve">
<value>Returns the record in a table at a given index.</value>
<comment>Description of 'Index' function.</comment>
@ -4694,4 +4748,12 @@
<value>The specified data source cannot be used with this function.</value>
<comment>Error Message.</comment>
</data>
<data name="ErrInvalidArgumentExpectedType" xml:space="preserve">
<value>Invalid argument '{0}'. Expected valid type name or type literal.</value>
<comment>Error Message shown to user when a value other name or type literal is passed into AsType, IsType and ParseJSON functions.</comment>
</data>
<data name="ErrUnsupportedTypeInTypeArgument" xml:space="preserve">
<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>
</root>

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

@ -610,7 +610,7 @@ namespace Microsoft.PowerFx.Connectors.Tests
FormulaValue httpResult = await function.InvokeAsync(new FormulaValue[] { kind, analysisInputParam, parametersParam }, context, CancellationToken.None);
ErrorValue ev = Assert.IsType<ErrorValue>(httpResult);
Assert.Equal(ErrorKind.InvalidJSON, ev.Errors.First().Kind);
Assert.Equal(ErrorKind.InvalidArgument, ev.Errors.First().Kind);
Assert.Equal("ACSL.ConversationAnalysisAnalyzeConversationConversation failed: PowerFxJsonException Expecting Table but received a Number, in result/prediction/intents", ev.Errors.First().Message);
}
@ -675,7 +675,7 @@ namespace Microsoft.PowerFx.Connectors.Tests
FormulaValue httpResult = await function.InvokeAsync(new FormulaValue[] { kind, analysisInputParam, parametersParam }, context, CancellationToken.None);
ErrorValue ev = Assert.IsType<ErrorValue>(httpResult);
Assert.Equal(ErrorKind.InvalidJSON, ev.Errors.First().Kind);
Assert.Equal(ErrorKind.InvalidArgument, ev.Errors.First().Kind);
Assert.Equal("ACSL.ConversationAnalysisAnalyzeConversationConversation failed: PowerFxJsonException Expecting String but received a Table with 2 elements, in result/prediction/entities/category", ev.Errors.First().Message);
}

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

@ -0,0 +1,160 @@
#SETUP: AllowTypeLiteral, TimeZoneInfo("Pacific Standard Time")
// Primitives
>> AsType(ParseJSON("987654321"), Number)
987654321
>> AsType(ParseJSON("98765.4321"), Decimal)
98765.4321
>> AsType(ParseJSON("12345678901234.567890123456"), Decimal)
12345678901234.567890123456
>> AsType(ParseJSON("-1.3"), Decimal)
-1.3
>> AsType(ParseJSON("2e3"), Decimal)
2000
>> AsType(ParseJSON("98765.4321"), Number) > AsType(ParseJSON("98765.4320"), Number)
true
>> AsType(ParseJSON("""AstypeFunction"""), Type(Text))
"AstypeFunction"
>> AsType(ParseJSON("""1984-01-01"""), Date)
Date(1984,1,1)
>> AsType(ParseJSON("""1900-12-31T23:59:59.999Z"""), DateTime)
DateTime(1900,12,31,15,59,59,999)
>> AsType(ParseJSON("""1900-12-31T23:59:59.999"""), DateTime)
DateTime(1900,12,31,23,59,59,999)
>> AsType(ParseJSON("""1900-12-31T23:59:59.999+00:00"""), DateTime)
DateTime(1900,12,31,15,59,59,999)
>> AsType(ParseJSON("""1900-12-31T23:59:59.999-08:00"""), DateTime)
DateTime(1900,12,31,23,59,59,999)
>> AsType(ParseJSON("""1900-12-31"""), DateTime)
DateTime(1900,12,31,0,0,0,0)
>> AsType(ParseJSON("""1900-12-31T23:59:59.999"""), Date)
Date(1900,12,31)
>> AsType(ParseJSON("""1900-12-31T00:00:00.000Z"""), Date)
Date(1900,12,31)
>> AsType(ParseJSON("""11:59:59.999"""), Time)
Time(11,59,59,999)
>> AsType(ParseJSON("""00:00:00"""), Time)
Time(0,0,0,0)
>> AsType(ParseJSON("""12:34:56.789"""), Time) = TimeValue(ParseJSON("""12:34:56.789"""))
true
>> AsType(ParseJSON("""12:34:56.789"""), Time) = TimeValue(ParseJSON("""12:34:56.7891"""))
false
>> AsType(ParseJSON("""1900-12-31T00:00:00.000Z"""), DateTimeTZInd)
DateTime(1900,12,31,0,0,0,0)
>> AsType(ParseJSON("""1900-12-31T00:00:00.000-08:00"""), DateTimeTZInd)
DateTime(1900,12,31,8,0,0,0)
>> DateTimeValue(AsType(ParseJSON("""1900-12-31T00:00:00.000Z"""), UntypedObject))
DateTime(1900,12,30,16,0,0,0)
>> DateValue(AsType(ParseJSON("""1900-12-31T00:00:00.000Z"""), UntypedObject))
Date(1900,12,30)
>> Value(AsType(ParseJSON("42"), UntypedObject))
42
>> Value(AsType(ParseJSON("true"), UntypedObject))
1
>> If(AsType(ParseJSON("false"), Boolean), "MyFalse", "MyTrue")
"MyTrue"
>> AsType(ParseJSON("42"), Number) = 42
true
>> UniChar(AsType(ParseJSON("66"), Number))
"B"
// record
>> AsType(ParseJSON("{""foo"": true, ""bar"": 1.1}"), Type({foo: Boolean, bar: Number}))
{bar:1.1,foo:true}
// record missing field
>> AsType(ParseJSON("{""Name"": ""SpongeBob"", ""Age"": 1}"), Type({Name: Text, Age: Number, Aquatic: Boolean})).Name
"SpongeBob"
// Deeply nested record with table
>> AsType(ParseJSON("{""a"": {""b"" : { ""c"" : [1, 2, 3, 4]}}}"), Type({a: {b: {c: [Number]}}}))
{a:{b:{c:Table({Value:1},{Value:2},{Value:3},{Value:4})}}}
// Table
>> AsType(ParseJSON("[{""a"": ""Hello"", ""b"": ""2012-01-02""}, {""a"": ""Hi"", ""b"": ""2012-01-03""}]"), Type([{a: Text, b: Date}]))
Table({a:"Hello",b:Date(2012,1,2)},{a:"Hi",b:Date(2012,1,3)})
>> AsType(ParseJSON("[{""a"": [{""z"": true}, {""z"": false}]}, {""a"": [{""z"": false}, {""z"": true}]}]"), Type([{a: [{z: Boolean}]}]))
Table({a:Table({z:true},{z:false})},{a:Table({z:false},{z:true})})
// Negative tests
>> AsType(ParseJSON("5"), Text)
Error({Kind:ErrorKind.InvalidArgument})
>> AsType(ParseJSON("""42"""), Number)
Error({Kind:ErrorKind.InvalidArgument})
>> AsType(ParseJSON("""1900-12-31T24:59:59.1002Z"""), DateTime)
Error({Kind:ErrorKind.InvalidArgument})
>> AsType(ParseJSON("""24:59:59.12345678"""), Time)
Error({Kind:ErrorKind.InvalidArgument})
>> AsType(ParseJSON("1"), 1)
Errors: Error 23-24: Invalid argument '1'. Expected valid type name or type literal.
>> AsType(ParseJSON("5"), Type(5))
Errors: Error 28-29: Type literal declaration is invalid. The expression '5' cannot be used in a type definition.|Error 27-28: Type literal declaration is invalid. The expression 'Type(5)' cannot be used in a type definition.
>> AsType(ParseJSON("true"), UnKnown)
Errors: Error 26-33: Name isn't valid. 'UnKnown' isn't recognized.|Error 0-34: Invalid argument 'UnKnown'. Expected valid type name or type literal.
>> AsType(ParseJSON("fasle"), Boolean)
Error({Kind:ErrorKind.InvalidJSON})
>> AsType(ParseJSON("true"), Void)
Errors: Error 26-30: Unsupported untyped/JSON conversion type 'Void' in argument.
>> AsType(ParseJSON("{""a"": 5, ""b"":true}"), Type({a: Number}))
Error({Kind:ErrorKind.InvalidArgument})
>> AsType(ParseJSON("{""a"": 5}"), Type({a: Number}), "Hello")
Errors: Error 0-59: Invalid number of arguments: received 3, expected 2.
>> AsType(ParseJSON("true"), None)
Errors: Error 26-30: Unsupported untyped/JSON conversion type 'ObjNull' in argument.
>> AsType(ParseJSON("null"), None)
Errors: Error 26-30: Unsupported untyped/JSON conversion type 'ObjNull' in argument.
>> AsType(ParseJSON("{}"), Type({a: Text, b: [Color]}))
Errors: Error 28-29: Unsupported untyped/JSON conversion type 'Color' in argument.
>> AsType(If(1/0 > 1, ParseJSON("42")), Number)
Error({Kind:ErrorKind.Div0})
>> AsType(ParseJSON(Blank()), Number)
Blank()
>> AsType(ParseJSON("42"), Blank())
Errors: Error 24-31: Invalid argument 'Blank()'. Expected valid type name or type literal.
>> AsType(ParseJSON("42"), 1/0)
Errors: Error 25-26: Invalid argument '1 / 0'. Expected valid type name or type literal.

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

@ -258,10 +258,10 @@ Errors: Error 0-4: The function 'Text' has some invalid arguments.|Warning 45-85
// badly formed numbers
>> Text( ParseJSON("123456789d12") )
Error({Kind:ErrorKind.InvalidArgument})
Error({Kind:ErrorKind.InvalidJSON})
>> Text( ParseJSON("--123456789") )
Error({Kind:ErrorKind.InvalidArgument})
Error({Kind:ErrorKind.InvalidJSON})

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

@ -283,7 +283,7 @@ Errors: Error 0-4: The function 'Text' has some invalid arguments.|Warning 45-85
// badly formed numbers
>> Text( ParseJSON("123456789d12") )
Error({Kind:ErrorKind.InvalidArgument})
Error({Kind:ErrorKind.InvalidJSON})
>> Text( ParseJSON("--123456789") )
Error({Kind:ErrorKind.InvalidArgument})
Error({Kind:ErrorKind.InvalidJSON})

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

@ -413,7 +413,7 @@ Error({Kind:ErrorKind.Div0})
Error({Kind:ErrorKind.Numeric})
>> ParseJSON("not a JSON string")
Error({Kind:ErrorKind.InvalidArgument})
Error({Kind:ErrorKind.InvalidJSON})
>> ParseJSON(If(1/0<2,"not a JSON string"))
Error({Kind:ErrorKind.Div0})

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

@ -0,0 +1,131 @@
#SETUP: AllowTypeLiteral, TimeZoneInfo("Pacific Standard Time")
// Primitives
>> IsType(ParseJSON("987654321"), Number)
true
>> AsType(ParseJSON("12345678901234.567890123456"), Decimal)
12345678901234.567890123456
>> IsType(ParseJSON("98765.4321"), Decimal)
true
>> IsType(ParseJSON("-1.3"), Decimal)
true
>> IsType(ParseJSON("2e3"), Decimal)
true
>> IsType(ParseJSON("987654321"), Text)
false
>> IsType(ParseJSON("987654321"), Date)
false
>> If(IsType(ParseJSON("""1900-12-31T23:59:59.999Z"""), DateTime), "ValidDate")
"ValidDate"
>> If(IsType(ParseJSON("""1900-12-31T23:59:59.999Z"""), Text), "ValidText")
"ValidText"
>> IsType(ParseJSON("""IstypeFunction"""), Type(Text))
true
>> IsType(ParseJSON("""1984-01-01"""), Date)
true
>> IsType(ParseJSON("""1900-12-31"""), DateTime)
true
>> IsType(ParseJSON("""1900-12-31T23:59:59.999"""), Date)
true
>> IsType(ParseJSON("""11:59:59.999"""), Time)
true
>> IsType(ParseJSON("""00:00:00"""), Time)
true
>> IsType(ParseJSON("""1900-12-31T00:00:00.000Z"""), DateTimeTZInd)
true
>> IsType(ParseJSON("""true"""), Boolean)
false
>> IsType(ParseJSON("true"), Boolean)
true
// record
>> IsType(ParseJSON("{""foo"": true, ""bar"": 1.1}"), Type({foo: Boolean, bar: Number}))
true
// record missing field
>> IsType(ParseJSON("{""Name"": ""SpongeBob"", ""Age"": 1}"), Type({Name: Text, Age: Number, Aquatic: Boolean}))
true
// record with additional field
>> IsType(ParseJSON("{""a"": 5, ""b"":true}"), Type({a: Number}))
false
// record with incorrect field type
>> IsType(ParseJSON("{""a"": 5}"), Type({a: Text}))
false
// Deeply nested record with table
>> IsType(ParseJSON("{""a"": {""b"" : { ""c"" : [1, 2, 3, 4]}}}"), Type({a: {b: {c: [Number]}}}))
true
// Table
>> IsType(ParseJSON("[{""a"": ""Hello"", ""b"": ""2012-01-02""}, {""a"": ""Hi"", ""b"": ""2012-01-03""}]"), Type([{a: Text, b: Date}]))
true
>> IsType(ParseJSON("[{""a"": ""Hello"", ""b"": ""2012-01-02""}]"), Type([{a: Text, b: Text}]))
true
>> IsType(ParseJSON("[{""a"": ""Hello"", ""b"": ""2012-01-02""}]"), Type([{a: Text, b: Number}]))
false
>> IsType(ParseJSON("[{""a"": [{""z"": true}, {""z"": false}]}, {""a"": [{""z"": false}, {""z"": true}]}]"), Type([{a: [{z: Boolean}]}]))
true
>> IsType(ParseJSON("""1900-12-31T24:59:59.1002Z"""), DateTime)
false
>> IsType(ParseJSON("""24:59:59.12345678"""), Time)
false
>> IsType(ParseJSON("1"), 1)
Errors: Error 23-24: Invalid argument '1'. Expected valid type name or type literal.
>> IsType(ParseJSON("5"), Type(5))
Errors: Error 28-29: Type literal declaration is invalid. The expression '5' cannot be used in a type definition.|Error 27-28: Type literal declaration is invalid. The expression 'Type(5)' cannot be used in a type definition.
>> IsType(ParseJSON("true"), UnKnown)
Errors: Error 26-33: Name isn't valid. 'UnKnown' isn't recognized.|Error 0-34: Invalid argument 'UnKnown'. Expected valid type name or type literal.
>> IsType(ParseJSON("fasle"), Boolean)
Error({Kind:ErrorKind.InvalidJSON})
>> IsType(ParseJSON("true"), Void)
Errors: Error 26-30: Unsupported untyped/JSON conversion type 'Void' in argument.
>> IsType(ParseJSON("{""a"": 5}"), Type({a: Number}), "Hello")
Errors: Error 0-59: Invalid number of arguments: received 3, expected 2.
>> IsType(ParseJSON("true"), None)
Errors: Error 26-30: Unsupported untyped/JSON conversion type 'ObjNull' in argument.
>> IsType(ParseJSON("{}"), Type({a: Text, b: [Color]}))
Errors: Error 28-29: Unsupported untyped/JSON conversion type 'Color' in argument.
>> IsType(If(1/0 > 1, ParseJSON("42")), Number)
Error({Kind:ErrorKind.Div0})
>> IsType(ParseJSON(Blank()), Number)
Blank()
>> IsType(ParseJSON("42"), Blank())
Errors: Error 24-31: Invalid argument 'Blank()'. Expected valid type name or type literal.
>> IsType(ParseJSON("42"), 1/0)
Errors: Error 25-26: Invalid argument '1 / 0'. Expected valid type name or type literal.

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

@ -54,7 +54,7 @@ Blank()
Error({Kind:ErrorKind.InvalidArgument})
>> Value(ParseJSON("This "" Is , "" Invalid ").a)
Error({Kind:ErrorKind.InvalidArgument})
Error({Kind:ErrorKind.InvalidJSON})
// Text tests
>> Text(Index(ParseJSON("[""s""]"), 1))

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

@ -0,0 +1,163 @@
#SETUP: AllowTypeLiteral, TimeZoneInfo("Pacific Standard Time")
// Primitives
>> ParseJSON("5", Number)
5
>> ParseJSON("98765.4321", Number)
98765.4321
>> ParseJSON("98765.4321", Decimal)
98765.4321
>> ParseJSON("12345678901234.567890123456", Decimal)
12345678901234.567890123456
>> ParseJSON("-1.3", Decimal)
-1.3
>> ParseJSON("2e3", Decimal)
2000
>> ParseJSON("""HelloWorld""", Text)
"HelloWorld"
>> ParseJSON("""HelloWorld""", Type(Text))
"HelloWorld"
>> ParseJSON("""1984-01-01""", Date)
Date(1984,1,1)
>> ParseJSON("""2008-01-01T12:12:12.100Z""", DateTime)
DateTime(2008,1,1,4,12,12,100)
>> ParseJSON("""2008-01-01T12:12:12.100""", DateTime)
DateTime(2008,1,1,12,12,12,100)
>> ParseJSON("""2008-01-01T12:12:12.100-08:00""", DateTime)
DateTime(2008,1,1,12,12,12,100)
>> ParseJSON("""1900-12-31""", DateTime)
DateTime(1900,12,31,0,0,0,0)
>> ParseJSON("""1900-12-31T23:59:59.999""", Date)
Date(1900,12,31)
>> ParseJSON("""11:59:59.999""", Time)
Time(11,59,59,999)
>> ParseJSON("""00:00:00""", Time)
Time(0,0,0,0)
>> ParseJSON("""12:34:56.789""", Time) = TimeValue(ParseJSON("""12:34:56.789"""))
true
>> ParseJSON("""12:34:56.789""", Time) = TimeValue(ParseJSON("""12:34:56.7891"""))
false
>> ParseJSON("""1900-12-31T00:00:00.000Z""", DateTimeTZInd)
DateTime(1900,12,31,0,0,0,0)
>> ParseJSON("""1900-12-31T00:00:00.000-08:00""", DateTimeTZInd)
DateTime(1900,12,31,8,0,0,0)
>> DateTimeValue(ParseJSON("""1900-12-31T00:00:00.000Z""", UntypedObject))
DateTime(1900,12,30,16,0,0,0)
>> DateValue(ParseJSON("""1900-12-31T00:00:00.000Z""", UntypedObject))
Date(1900,12,30)
>> Value(ParseJSON("42", UntypedObject))
42
>> Value(ParseJSON("true", UntypedObject))
1
>> ParseJSON("true", Boolean)
true
>> If(ParseJSON("false", Boolean), "No", "Yes")
"Yes"
>> ParseJSON("555", Number) = 555
true
>> 2 < ParseJSON("1", Number)
false
>> UniChar(ParseJSON("65", Number))
"A"
// record
>> ParseJSON("{""foo"": true}", Type({foo: Boolean}))
{foo:true}
// record missing field
>> ParseJSON("{""Name"": ""SpongeBob"", ""Age"": 1}", Type({Name: Text, Age: Number, Aquatic: Boolean})).Name
"SpongeBob"
// Deeply nested record with table
>> ParseJSON("{""a"": {""b"" : { ""c"" : [1, 2, 3, 4]}}}", Type({a: {b: {c: [Number]}}}))
{a:{b:{c:Table({Value:1},{Value:2},{Value:3},{Value:4})}}}
// Table
>> ParseJSON("[{""a"": ""Hello"", ""b"": ""2012-01-02""}, {""a"": ""Hi"", ""b"": ""2012-01-03""}]", Type([{a: Text, b: Date}]))
Table({a:"Hello",b:Date(2012,1,2)},{a:"Hi",b:Date(2012,1,3)})
>> ParseJSON("[{""a"": [{""z"": true}, {""z"": false}]}, {""a"": [{""z"": false}, {""z"": true}]}]", Type([{a: [{z: Boolean}]}]))
Table({a:Table({z:true},{z:false})},{a:Table({z:false},{z:true})})
// Negative tests
>> ParseJSON("5", Text)
Error({Kind:ErrorKind.InvalidArgument})
>> ParseJSON("""42""", Number)
Error({Kind:ErrorKind.InvalidArgument})
>> ParseJSON("""24:59:59.12345678""", Time)
Error({Kind:ErrorKind.InvalidArgument})
>> ParseJSON("1", 1)
Errors: Error 15-16: Invalid argument '1'. Expected valid type name or type literal.
>> ParseJSON("""RED""", Color)
Errors: Error 21-26: Unsupported untyped/JSON conversion type 'Color' in argument.
>> ParseJSON("5", Type(5))
Errors: Error 20-21: Type literal declaration is invalid. The expression '5' cannot be used in a type definition.|Error 19-20: Type literal declaration is invalid. The expression 'Type(5)' cannot be used in a type definition.
>> ParseJSON("true", UnKnown)
Errors: Error 18-25: Name isn't valid. 'UnKnown' isn't recognized.|Error 0-26: Invalid argument 'UnKnown'. Expected valid type name or type literal.
>> ParseJSON("fasle", Boolean)
Error({Kind:ErrorKind.InvalidJSON})
>> ParseJSON("true", Void)
Errors: Error 18-22: Unsupported untyped/JSON conversion type 'Void' in argument.
>> ParseJSON("{""a"": 5, ""b"":true}", Type({a: Number}))
Error({Kind:ErrorKind.InvalidArgument})
>> ParseJSON("{""a"": 5}", Type({a: Number}), "Hello")
Errors: Error 0-51: Invalid number of arguments: received 3, expected 2.
>> ParseJSON("true", None)
Errors: Error 18-22: Unsupported untyped/JSON conversion type 'ObjNull' in argument.
>> ParseJSON("null", None)
Errors: Error 18-22: Unsupported untyped/JSON conversion type 'ObjNull' in argument.
>> ParseJSON("{}", Type({a: Text, b: [Color]}))
Errors: Error 20-21: Unsupported untyped/JSON conversion type 'Color' in argument.
>> ParseJSON(If(1/0 > 1, "42"), Number)
Error({Kind:ErrorKind.Div0})
>> ParseJSON(Blank(), Number)
Blank()
>> ParseJSON("42", Blank())
Errors: Error 16-23: Invalid argument 'Blank()'. Expected valid type name or type literal.
>> ParseJSON("42", 1/0)
Errors: Error 17-18: Invalid argument '1 / 0'. Expected valid type name or type literal.

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

@ -72,6 +72,7 @@
"IsMatch",
"IsNumeric",
"IsToday",
"IsType",
"JSON",
"Language",
"Last",

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

@ -32,6 +32,11 @@ namespace Microsoft.PowerFx.Interpreter.Tests.Helpers
parserOptions.TextFirst = true;
}
if (flags.HasFlag(TexlParser.Flags.AllowTypeLiteral))
{
parserOptions.AllowParseAsTypeLiteral = true;
}
return parserOptions;
}
}

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

@ -0,0 +1,203 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.PowerFx.Core.Functions;
using Microsoft.PowerFx.Core.IR;
using Microsoft.PowerFx.Core.Localization;
using Microsoft.PowerFx.Core.Tests;
using Microsoft.PowerFx.Core.Texl;
using Microsoft.PowerFx.Core.Texl.Builtins;
using Microsoft.PowerFx.Functions;
using Microsoft.PowerFx.Types;
using Xunit;
namespace Microsoft.PowerFx.Json.Tests
{
public class AsTypeIsTypeParseJSONTests
{
private static readonly ParserOptions ParseType = new ParserOptions
{
AllowParseAsTypeLiteral = true,
};
private RecalcEngine SetupEngine()
{
var config = new PowerFxConfig();
config.EnableJsonFunctions();
return new RecalcEngine(config);
}
[Fact]
public void PrimitivesTest()
{
var engine = SetupEngine();
// custom-type type alias
engine.AddUserDefinitions("T = Type(Number);");
// Positive tests
CheckIsTypeAsTypeParseJSON(engine, "\"42\"", "Number", 42D);
CheckIsTypeAsTypeParseJSON(engine, "\"17.29\"", "Number", 17.29D);
CheckIsTypeAsTypeParseJSON(engine, "\"\"\"HelloWorld\"\"\"", "Text", "HelloWorld");
CheckIsTypeAsTypeParseJSON(engine, "\"\"\"2000-01-01\"\"\"", "Date", new DateTime(2000, 1, 1));
CheckIsTypeAsTypeParseJSON(engine, "\"\"\"2000-01-01T00:00:01.100\"\"\"", "DateTime", new DateTime(2000, 1, 1, 0, 0, 1, 100));
CheckIsTypeAsTypeParseJSON(engine, "\"true\"", "Boolean", true);
CheckIsTypeAsTypeParseJSON(engine, "\"false\"", "Boolean", false);
CheckIsTypeAsTypeParseJSON(engine, "\"1234.56789\"", "Decimal", 1234.56789m);
CheckIsTypeAsTypeParseJSON(engine, "\"42\"", "T", 42D);
CheckIsTypeAsTypeParseJSON(engine, "\"\"\"Power Fx\"\"\"", "Type(Text)", "Power Fx", options: ParseType);
CheckIsTypeAsTypeParseJSON(engine, "\"\"\"2000-01-01T00:00:01.100Z\"\"\"", "DateTimeTZInd", new DateTime(2000, 1, 1, 0, 0, 1, 100));
CheckIsTypeAsTypeParseJSON(engine, "\"\"\"11:59:59\"\"\"", "Time", new TimeSpan(11, 59, 59));
// Negative tests - Coercions not allowed
CheckIsTypeAsTypeParseJSON(engine, "\"42\"", "Text", string.Empty, false);
CheckIsTypeAsTypeParseJSON(engine, "\"\"\"42\"\"\"", "Number", string.Empty, false);
CheckIsTypeAsTypeParseJSON(engine, "\"\"\"42\"\"\"", "Decimal", string.Empty, false);
CheckIsTypeAsTypeParseJSON(engine, "\"0\"", "Boolean", false, false);
CheckIsTypeAsTypeParseJSON(engine, "\"true\"", "Number", false, false);
// Negative tests - types not supported in FromJSON converter
CheckIsTypeAsTypeParseJSONCompileErrors(engine, "\"42\"", "None", TexlStrings.ErrUnsupportedTypeInTypeArgument.Key);
CheckIsTypeAsTypeParseJSONCompileErrors(engine, "\"\"\"RED\"\"\"", "Color", TexlStrings.ErrUnsupportedTypeInTypeArgument.Key);
CheckIsTypeAsTypeParseJSON(engine, "\"\"\"abcd-efgh-1234-ijkl\"\"\"", "GUID", string.Empty, false);
CheckIsTypeAsTypeParseJSON(engine, "\"\"\"foo/bar/uri\"\"\"", "Hyperlink", string.Empty, false);
}
[Fact]
public void RecordsTest()
{
var engine = SetupEngine();
engine.AddUserDefinitions("T = Type({a: Number});");
dynamic obj1 = new ExpandoObject();
obj1.a = 5D;
dynamic obj2 = new ExpandoObject();
obj2.a = new ExpandoObject();
obj2.a.b = new ExpandoObject();
obj2.a.b.c = false;
dynamic obj3 = new ExpandoObject();
obj3.a = new object[] { 1m, 2m, 3m, 4m };
CheckIsTypeAsTypeParseJSON(engine, "\"{\"\"a\"\": 5}\"", "T", obj1);
CheckIsTypeAsTypeParseJSON(engine, "\"{\"\"a\"\": 5}\"", "Type({a: Number})", obj1, options: ParseType);
CheckIsTypeAsTypeParseJSON(engine, "\"{\"\"a\"\": { \"\"b\"\": {\"\"c\"\": false}}}\"", "Type({a: {b: {c: Boolean }}})", obj2, options: ParseType);
CheckIsTypeAsTypeParseJSON(engine, "\"{\"\"a\"\": [1, 2, 3, 4]}\"", "Type({a: [Decimal]})", obj3, options: ParseType);
// Negative Tests
CheckIsTypeAsTypeParseJSON(engine, "\"{\"\"a\"\": 5}\"", "Type({a: Text})", obj1, isValid: false, options: ParseType);
CheckIsTypeAsTypeParseJSON(engine, "\"{\"\"a\"\": 5, \"\"b\"\": 6}\"", "Type({a: Number})", obj1, false, options: ParseType);
CheckIsTypeAsTypeParseJSONCompileErrors(engine, "\"{\"\"a\"\": \"\"foo/bar/uri\"\"}\"", "Type({a: Void})", TexlStrings.ErrUnsupportedTypeInTypeArgument.Key, options: ParseType);
}
[Fact]
public void TablesTest()
{
var engine = SetupEngine();
engine.AddUserDefinitions("T = Type([{a: Number}]);");
var t1 = new object[] { 5D };
var t2 = new object[] { 1m, 2m, 3m, 4m };
var t3a = new object[] { true, true, false, true };
var t3 = new object[] { t3a };
CheckIsTypeAsTypeParseJSON(engine, "\"[{\"\"a\"\": 5}]\"", "T", t1);
CheckIsTypeAsTypeParseJSON(engine, "\"[{\"\"a\"\": 5}]\"", "Type([{a: Number}])", t1, options: ParseType);
CheckIsTypeAsTypeParseJSON(engine, "\"[{\"\"a\"\": [true, true, false, true]}]\"", "Type([{a: [Boolean]}])", t3, options: ParseType);
CheckIsTypeAsTypeParseJSON(engine, "\"[1, 2, 3, 4]\"", "Type([Decimal])", t2, options: ParseType);
// Negative tests
CheckIsTypeAsTypeParseJSON(engine, "\"[{\"\"a\"\": 5, \"\"b\"\": 6}]\"", "Type([{a: Number}])", t1, false, options: ParseType);
CheckIsTypeAsTypeParseJSON(engine, "\"[1, 2, 3, 4]\"", "Type([Text])", t2, false, options: ParseType);
CheckIsTypeAsTypeParseJSONCompileErrors(engine, "\"[\"\"foo/bar/uri\"\"]\"", "Type([Color])", TexlStrings.ErrUnsupportedTypeInTypeArgument.Key, options: ParseType);
}
[Theory]
[InlineData("\"42\"", "SomeType", true, "ErrInvalidName")]
[InlineData("\"42\"", "Type(5)", true, "ErrTypeLiteral_InvalidTypeDefinition")]
[InlineData("\"42\"", "Text(42)", true, "ErrInvalidArgumentExpectedType")]
[InlineData("\"\"\"Hello\"\"\"", "\"Hello\"", true, "ErrInvalidArgumentExpectedType")]
[InlineData("\"{}\"", "Type([{a: 42}])", true, "ErrTypeLiteral_InvalidTypeDefinition")]
[InlineData("AsType(ParseJSON(\"42\"))", "", false, "ErrBadArity")]
[InlineData("IsType(ParseJSON(\"42\"))", "", false, "ErrBadArity")]
[InlineData("AsType(ParseJSON(\"42\"), Number , Text(5))", "", false, "ErrBadArity")]
[InlineData("IsType(ParseJSON(\"42\"), Number, 5)", "", false, "ErrBadArity")]
[InlineData("AsType(ParseJSON(\"123\"), 1)", "", false, "ErrInvalidArgumentExpectedType")]
public void TestCompileErrors(string expression, string type, bool testAllFunctions, string expectedError)
{
var engine = SetupEngine();
if (testAllFunctions)
{
CheckIsTypeAsTypeParseJSONCompileErrors(engine, expression, type, expectedError, ParseType);
}
else
{
var result = engine.Check(expression, options: ParseType);
Assert.False(result.IsSuccess);
Assert.Contains(result.Errors, e => e.MessageKey == expectedError);
}
}
[Fact]
public void TestFunctionsWithTypeArgs()
{
var expectedFunctions = new HashSet<string> { "AsType", "IsType", "ParseJSON" };
var functionWithTypeArgs = BuiltinFunctionsCore.TestOnly_AllBuiltinFunctions.Where(f => f.HasTypeArgs);
Assert.All(functionWithTypeArgs, f => Assert.Contains(f.Name, expectedFunctions));
Assert.All(functionWithTypeArgs, f => Assert.Contains(Enumerable.Range(0, f.MaxArity), i => f.ArgIsType(i)));
var functionsWithoutTypeArgs = BuiltinFunctionsCore.TestOnly_AllBuiltinFunctions.Where(f => !f.HasTypeArgs);
Assert.All(functionsWithoutTypeArgs, f => Assert.DoesNotContain(Enumerable.Range(0, Math.Min(f.MaxArity, 5)), i => f.ArgIsType(i)));
}
private void CheckIsTypeAsTypeParseJSON(RecalcEngine engine, string json, string type, object expectedValue, bool isValid = true, ParserOptions options = null)
{
var result = engine.Eval($"AsType(ParseJSON({json}), {type})", options: options);
CheckResult(expectedValue, result, isValid);
result = engine.Eval($"ParseJSON({json}, {type})", options: options);
CheckResult(expectedValue, result, isValid);
result = engine.Eval($"IsType(ParseJSON({json}), {type})", options: options);
Assert.Equal(isValid, result.ToObject());
}
private void CheckResult(object expectedValue, FormulaValue resultValue, bool isValid)
{
if (isValid)
{
Assert.Equal(expectedValue, resultValue.ToObject());
}
else
{
Assert.True(resultValue is ErrorValue);
}
}
private void CheckIsTypeAsTypeParseJSONCompileErrors(RecalcEngine engine, string json, string type, string expectedError, ParserOptions options = null)
{
var result = engine.Check($"AsType(ParseJSON({json}), {type})", options: options);
Assert.False(result.IsSuccess);
Assert.Contains(result.Errors, e => e.MessageKey == expectedError);
result = engine.Check($"ParseJSON({json}, {type})", options: options);
Assert.False(result.IsSuccess);
Assert.Contains(result.Errors, e => e.MessageKey == expectedError);
result = engine.Check($"IsType(ParseJSON({json}), {type})", options: options);
Assert.False(result.IsSuccess);
Assert.Contains(result.Errors, e => e.MessageKey == expectedError);
}
}
}