Skip to content

Commit

Permalink
Implemented support of new JsonLogic functions:
Browse files Browse the repository at this point in the history
-date_add
-now
-toUpperCase
-toLowerCase
  • Loading branch information
ivan committed May 2, 2023
1 parent d66ab27 commit 1363235
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 33 deletions.
129 changes: 108 additions & 21 deletions shesha-core/src/Shesha.Framework/JsonLogic/JsonLogic2LinqConverter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Abp.Dependency;
using Abp.Domain.Entities;
using Abp.Timing;
using Castle.Core;
using Newtonsoft.Json.Linq;
using Shesha.Extensions;
Expand Down Expand Up @@ -554,13 +555,116 @@ out expr
var lambda = specExpression.Body.ReplaceParameter(specExpression.Parameters.Single(), param);

return lambda;
}
case JsOperators.Now:
{
if (@operator.Arguments.Any())
throw new Exception($"{JsOperators.Now} operator doesn't support any arguments");

return Expression.Constant(Clock.Now);
}
case JsOperators.DateAdd:
{
if (@operator.Arguments.Count() != 3)
throw new Exception($"{JsOperators.DateAdd} operator require 3 arguments: date, number and datepart");

var date = ParseTree<T>(@operator.Arguments[0], param);
var number = GetAsInt64(@operator.Arguments[1]);
if (number == null)
throw new ArgumentException($"{JsOperators.DateAdd}: the `number` argument must not be null");

var datepart = GetAsString(@operator.Arguments[2]);

switch (datepart)
{
case "day":
{
var addMethod = typeof(DateTime).GetMethod(nameof(DateTime.Add), new Type[] { typeof(TimeSpan) });
var timeSpan = TimeSpan.FromDays(number.Value);
return Expression.Call(
date,
addMethod,
Expression.Constant(timeSpan)
);
}
case "week":
{
var addMethod = typeof(DateTime).GetMethod(nameof(DateTime.Add), new Type[] { typeof(TimeSpan) });
var timeSpan = TimeSpan.FromDays(number.Value * 7);
return Expression.Call(
date,
addMethod,
Expression.Constant(timeSpan)
);
}
case "month":
{
var addMonthsMethod = typeof(DateTime).GetMethod(nameof(DateTime.AddMonths), new Type[] { typeof(int) });
return Expression.Call(
date,
addMonthsMethod,
Expression.Constant(number.Value)
);
}
case "year":
{
var addYearsMethod = typeof(DateTime).GetMethod(nameof(DateTime.AddYears), new Type[] { typeof(int) });
return Expression.Call(
date,
addYearsMethod,
Expression.Constant(number.Value)
);
}
default:
throw new ArgumentException($"{JsOperators.DateAdd}: the `datepart` = `{datepart}` is not supported");
}
}
case JsOperators.Upper:
{
if (@operator.Arguments.Count() != 1)
throw new Exception($"{JsOperators.Upper} operator require 1 argument");

var arg = ParseTree<T>(@operator.Arguments[0], param);
var toUpperMethod = typeof(string).GetMethod(nameof(string.ToUpper), new Type[] { });
return Expression.Call(arg, toUpperMethod);
}
case JsOperators.Lower:
{
if (@operator.Arguments.Count() != 1)
throw new Exception($"{JsOperators.Lower} operator require 1 argument");

var arg = ParseTree<T>(@operator.Arguments[0], param);
var toLowerMethod = typeof(string).GetMethod(nameof(string.ToLower), new Type[] { });
return Expression.Call(arg, toLowerMethod);
}
default:
throw new NotSupportedException($"Operator `{@operator.Name}` is not supported");
}
}

return null;
}

private string GetAsString(JToken token)
{
if (token is JValue value)
{
return value.Value.ToString();
}
else
throw new NotSupportedException();
}

private Int64? GetAsInt64(JToken token)
{
if (token is JValue value)
{
return value.Value != null
? (Int64)value.Value
: null;
}
else
throw new NotSupportedException();
}

private bool TryCompareMemberAndDateTime(Expression left, Expression right, Func<DateTime, DateTime> dateConverter, Binder binder, out Expression expression)
Expand Down Expand Up @@ -915,27 +1019,6 @@ public bool TryGetOperator(JToken rule, out OperationProps @operator)
return true;
}

private List<string> KnownOperators = new List<string> {

JsOperators.Equal,
JsOperators.StrictEqual,
JsOperators.NotEqual,
JsOperators.StrictNotEqual,
JsOperators.Less,
JsOperators.LessOrEqual,
JsOperators.Greater,
JsOperators.GreaterOrEqual,
JsOperators.Var,
JsOperators.And,
JsOperators.Or,
JsOperators.DoubleNegotiation,
JsOperators.Negotiation,
JsOperators.Not,
JsOperators.In,
JsOperators.StartsWith,
JsOperators.EndsWith
};

/// inheritedDoc
public Expression<Func<T, bool>> ParseExpressionOf<T>(string rule)
{
Expand Down Expand Up @@ -995,6 +1078,10 @@ public static class JsOperators
public const string StartsWith = "startsWith";
public const string EndsWith = "endsWith";
public const string IsSatisfied = "is_satisfied";
public const string DateAdd = "date_add";
public const string Now = "now";
public const string Upper = "toUpperCase";
public const string Lower = "toLowerCase";
}

public class ExpressionPair
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Abp.Domain.Entities;
using Abp;
using Abp.Domain.Entities;
using Abp.Domain.Repositories;
using Abp.Linq;
using Abp.Timing;
using FluentAssertions;
using Newtonsoft.Json.Linq;
using Shesha.Authorization.Users;
Expand All @@ -9,6 +11,8 @@
using Shesha.Extensions;
using Shesha.JsonLogic;
using Shesha.Services;
using Shesha.Tests.TestingUtils;
using Shesha.Utilities;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
Expand Down Expand Up @@ -1248,6 +1252,99 @@ public async Task entityReference_In_Fetch()

#endregion

#region Custom date functions

private readonly string _custom_date_funcs_Convert_expression = @"{""and"":[{""<="":[{""date_add"":[{""now"":[]},-5,""day""]},{""var"":""user.lastLoginDate""},{""now"":[]}]}]} ";

[Fact]
public void Custom_Date_Funcs_Convert()
{
using (FreezeTime())
{
var expression = ConvertToExpression<Person>(_custom_date_funcs_Convert_expression);

// todo: find a way to use start of the minute here
var now = Expression.Constant(Clock.Now).ToString();
// note: use end of minute because we skip seconds for datetime values
var nowEom = Expression.Constant(Clock.Now.EndOfTheMinute()).ToString();

var expected = $@"ent => ((Convert({now}.Add(-5.00:00:00), Nullable`1) <= ent.User.LastLoginDate) AndAlso (ent.User.LastLoginDate <= Convert({nowEom}, Nullable`1)))";
Assert.Equal(expected, expression.ToString());
}
}

[Fact]
public async Task Custom_Date_Funcs_Fetch()
{
var data = await TryFetchData<Person, Guid>(_custom_date_funcs_Convert_expression);
Assert.NotNull(data);
}

#endregion

#region Custom string functions

private readonly string _custom_string_funcs_Convert_expression = @"{
""and"": [
{
""=="": [
{
""var"": ""firstName""
},
{
""toLowerCase"": [
""TeSt""
]
}
]
},
{
""=="": [
{
""var"": ""lastName""
},
{
""toUpperCase"": [
""VaLuE""
]
}
]
},
{
""=="": [
{
""var"": ""emailAddress1""
},
{
""toLowerCase"": [
{
""var"": ""emailAddress2""
}
]
}
]
}
]
}";

[Fact]
public void Custom_String_Funcs_Convert()
{
var expression = ConvertToExpression<Person>(_custom_string_funcs_Convert_expression);

var expected = $@"ent => (((ent.FirstName == ""TeSt"".ToLower()) AndAlso (ent.LastName == ""VaLuE"".ToUpper())) AndAlso (ent.EmailAddress1 == ent.EmailAddress2.ToLower()))";
Assert.Equal(expected, expression.ToString());
}

[Fact]
public async Task Custom_String_Funcs_Fetch()
{
var data = await TryFetchData<Person, Guid>(_custom_string_funcs_Convert_expression);
Assert.NotNull(data);
}

#endregion

public class EntityWithRefListrops : Entity<Guid>
{
public virtual RefListPersonTitle Title { get; set; }
Expand All @@ -1272,5 +1369,19 @@ public class EntityWithDateProps: Entity<Guid>
public virtual int IntProp { get; set; }
public virtual int? NullableIntProp { get; set; }
}

private IDisposable FreezeTime()
{
// save current provider
var prevProvider = Clock.Provider;

// change current provider to static
Clock.Provider = new StaticClockProvider();

// return compensation logic
return new DisposeAction(() => {
Clock.Provider = prevProvider;
});
}
}
}
29 changes: 29 additions & 0 deletions shesha-core/test/Shesha.Tests/TestingUtils/StaticClockProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Abp.Timing;
using System;

namespace Shesha.Tests.TestingUtils
{
/// <summary>
/// Static clock provider, is used for unit tests only
/// It's a cope of the <see cref="UnspecifiedClockProvider"/> with just one change - after first call of <see cref="Now"/> the time stops
/// </summary>
public class StaticClockProvider : IClockProvider
{
private DateTime? _fixedTime = null;

public DateTime Now => _fixedTime ?? (DateTime)(_fixedTime = DateTime.Now);

public DateTimeKind Kind => DateTimeKind.Unspecified;

public bool SupportsMultipleTimezone => false;

public DateTime Normalize(DateTime dateTime)
{
return dateTime;
}

internal StaticClockProvider()
{
}
}
}
2 changes: 1 addition & 1 deletion shesha-core/test/Shesha.Tests/appsettings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"ConnectionStrings": {
"TestDB": "Data Source=.\\sql2019;Initial Catalog=houghton-his-test2;Integrated Security=True"
"TestDB": "Data Source=.; Initial Catalog=SheshaFunctionalTests;Integrated Security=SSPI"
}
}
2 changes: 1 addition & 1 deletion shesha-reactjs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@shesha/reactjs",
"version": "4.0.145-canary",
"version": "4.0.146-canary",
"description": "The reactjs frontend application and ui for the shesha framework",
"license": "GPL-3.0",
"homepage": "https://shesha-io.github.io/shesha-framework",
Expand Down
3 changes: 3 additions & 0 deletions shesha-reactjs/src/components/formDesigner/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export const BedFilter = addStory(DesignerTemplate, {
formId: '7065cf3a-a8ec-494e-b2c8-273274b86d1f',
});

export const CustomFunctions = addStory(DesignerTemplate, {
formId: '30c5cd95-e96d-4023-b213-94b1531ec6d9',
});

export const FormsIndex = addStory(DesignerTemplate, {
formId: {
Expand Down
13 changes: 9 additions & 4 deletions shesha-reactjs/src/components/queryBuilder/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,21 @@ const widgets = {
const evaluateTypes = ['boolean', 'date', 'datetime', 'time', 'number', 'text', 'entityReference', 'refList'];
const evaluateFunctions = {};
evaluateTypes.forEach(type => {
evaluateFunctions[`evaluate_${type}`] = getEvaluateFunc(type);
evaluateFunctions[`evaluate_${type}`.toUpperCase()] = getEvaluateFunc(type);
});

const knownFuncNames = ['NOW', 'LOWER', 'NOW', 'UPPER', 'RELATIVE_DATETIME'];
const knownFuncs: Funcs = {};
knownFuncNames.forEach(funcName => {
if (BasicFuncs.hasOwnProperty(funcName))
knownFuncs[funcName] = BasicFuncs[funcName];
});

const funcs: Funcs = {
...BasicFuncs,
...knownFuncs,
...evaluateFunctions,
//'evaluate': evaluateFunc,
};


export const config: Config = {
...AntdConfig,
types,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const args2JsonLogic: JsonLogicFormatFunc = (funcArgs: TypedMap<JsonLogicValue>)
const jsonLogic2Args: JsonLogicImportFunc = (val): RuleValue[] => {
const typedNode = val as IEvaluateNode;
if (!typedNode?.evaluate)
return [];
throw `Can't parse 'evaluate' function`; // throw exception to skip current function and try to parse others

const args: IEvaluateNodeArgs = Array.isArray(typedNode.evaluate) && typedNode.evaluate.length === 1
? typedNode.evaluate[0] as IEvaluateNodeArgs
Expand Down
Loading

0 comments on commit 1363235

Please sign in to comment.