From 8c3564fa97de641209008abb5dd66386239bb9bf Mon Sep 17 00:00:00 2001 From: Jake Moresca Date: Thu, 16 Nov 2023 15:48:53 +0100 Subject: [PATCH] Add Api Call Rule --- .../Engine/RuleEngineWrapperTests.cs | 11 +- ActionFlow.Tests/Rules/ApiCallRuleTests.cs | 154 ++++++++++++++++++ ActionFlow.Tests/Rules/VariableRuleTests.cs | 8 +- ActionFlow/Domain/ApiCallResult.cs | 18 ++ ActionFlow/Engine/IReSettingsProvider.cs | 9 + ActionFlow/Engine/ReSettingsProvider.cs | 19 +++ ActionFlow/Engine/RuleEngineWrapper.cs | 4 +- ActionFlow/Helpers/ApiClient.cs | 52 ++++++ ActionFlow/Rules/ApiCallRule.cs | 44 +++++ ActionFlow/Rules/IRuleWrapper.cs | 1 - ActionFlow/Rules/RuleWrapper.cs | 38 +++++ ActionFlow/Rules/VariableRule.cs | 35 +--- 12 files changed, 356 insertions(+), 37 deletions(-) create mode 100644 ActionFlow.Tests/Rules/ApiCallRuleTests.cs create mode 100644 ActionFlow/Domain/ApiCallResult.cs create mode 100644 ActionFlow/Engine/IReSettingsProvider.cs create mode 100644 ActionFlow/Engine/ReSettingsProvider.cs create mode 100644 ActionFlow/Helpers/ApiClient.cs create mode 100644 ActionFlow/Rules/ApiCallRule.cs create mode 100644 ActionFlow/Rules/RuleWrapper.cs diff --git a/ActionFlow.Tests/Engine/RuleEngineWrapperTests.cs b/ActionFlow.Tests/Engine/RuleEngineWrapperTests.cs index 94d65f5..0ffd3ce 100644 --- a/ActionFlow.Tests/Engine/RuleEngineWrapperTests.cs +++ b/ActionFlow.Tests/Engine/RuleEngineWrapperTests.cs @@ -15,7 +15,10 @@ public void When_running_rule_engine_with_basic_workflow_it_should_execute() var workflowProvider = Substitute.For(); workflowProvider.GetAllWorkflows().Returns(CreateFakeWorkflows()); - var rulesEngine = new RuleEngineWrapper(workflowProvider); + var resettingsProvider = Substitute.For(); + resettingsProvider.GetReSettings().Returns(CreateFakeResettings()); + + var rulesEngine = new RuleEngineWrapper(workflowProvider, resettingsProvider); //Act dynamic datas = new ExpandoObject(); @@ -52,5 +55,11 @@ private List CreateFakeWorkflows() return workflows; } + + private ReSettings CreateFakeResettings() + { + var reSettingsWithCustomTypes = new ReSettings(); + return reSettingsWithCustomTypes; + } } } diff --git a/ActionFlow.Tests/Rules/ApiCallRuleTests.cs b/ActionFlow.Tests/Rules/ApiCallRuleTests.cs new file mode 100644 index 0000000..a47ec8a --- /dev/null +++ b/ActionFlow.Tests/Rules/ApiCallRuleTests.cs @@ -0,0 +1,154 @@ +using ActionFlow.Engine; +using ActionFlow.Rules; +using NSubstitute; +using RulesEngine.Models; + +namespace ActionFlow.Tests.Rules +{ + [TestClass] + public class ApiCallRuleTests + { + [TestMethod] + public void When_running_api_call_rule_with_get_method_it_should_execute() + { + //Arrange + var workflowProvider = Substitute.For(); + workflowProvider.GetAllWorkflows().Returns(CreateGetApiCallRuleWorkflow()); + + var rulesEngine = new RuleEngineWrapper(workflowProvider, new ReSettingsProvider()); + + //Act + var resultList = rulesEngine.ExecuteAllRulesAsync("Test Workflow Rule 1", new object[] { }).Result; + + //Assert + Assert.IsTrue(resultList.TrueForAll(x => x.IsSuccess)); + } + + [TestMethod] + public void When_running_api_call_rule_with_post_method_it_should_execute() + { + //Arrange + var workflowProvider = Substitute.For(); + workflowProvider.GetAllWorkflows().Returns(CreatePostApiCallRuleWorkflow()); + + var rulesEngine = new RuleEngineWrapper(workflowProvider, new ReSettingsProvider()); + + //Act + var resultList = rulesEngine.ExecuteAllRulesAsync("Test Workflow Rule 1", new object[] { }).Result; + + //Assert + Assert.IsTrue(resultList.TrueForAll(x => x.IsSuccess)); + } + + [TestMethod] + public void When_running_api_call_rule_with_headers_it_should_execute_and_return_200_status_code() + { + //Arrange + var workflowProvider = Substitute.For(); + workflowProvider.GetAllWorkflows().Returns(CreateApiCallRuleWithHeadersWorkflow()); + + var rulesEngine = new RuleEngineWrapper(workflowProvider, new ReSettingsProvider()); + + //Act + var resultList = rulesEngine.ExecuteAllRulesAsync("Test Workflow Rule 1", new object[] { }).Result; + + //Assert + Assert.IsTrue(resultList.TrueForAll(x => x.IsSuccess)); + } + + private List CreateGetApiCallRuleWorkflow() + { + List workflows = new List(); + Workflow workflow = new Workflow(); + workflow.WorkflowName = "Test Workflow Rule 1"; + + List rules = new List(); + + var apiCallRule = new ApiCallRule(); + apiCallRule.Name = "Call test api"; + apiCallRule.Url = "http://httpbin.org/get"; + apiCallRule.ReturnVariableName = "apiResult"; + + var variableRule2 = new VariableRule(); + variableRule2.Name = "Test api call result"; + variableRule2.ConditionExpression = "apiResult.Body != null"; + + apiCallRule.Rules.Add(variableRule2); + + rules.Add(apiCallRule.AsRuleEngineRule()); + workflow.Rules = rules; + workflows.Add(workflow); + + return workflows; + } + + private List CreateApiCallRuleWithHeadersWorkflow() + { + List workflows = new List(); + Workflow workflow = new Workflow(); + workflow.WorkflowName = "Test Workflow Rule 1"; + + List rules = new List(); + + var apiCallRule = new ApiCallRule(); + apiCallRule.Name = "Call test api"; + apiCallRule.Url = "http://httpbin.org/get"; + apiCallRule.Headers = new Dictionary + { + { "test", "value" } + }; + apiCallRule.ReturnVariableName = "apiResult"; + + var variableRule2 = new VariableRule(); + variableRule2.Name = "Test api call result"; + variableRule2.ConditionExpression = "apiResult.StatusCode == 200"; + + apiCallRule.Rules.Add(variableRule2); + + rules.Add(apiCallRule.AsRuleEngineRule()); + workflow.Rules = rules; + workflows.Add(workflow); + + return workflows; + } + + private List CreatePostApiCallRuleWorkflow() + { + List workflows = new List(); + Workflow workflow = new Workflow(); + workflow.WorkflowName = "Test Workflow Rule 1"; + + List rules = new List(); + + var apiCallRule = new ApiCallRule(); + apiCallRule.Name = "Call test api"; + apiCallRule.Method = "POST"; + apiCallRule.Url = "http://httpbin.org/post"; + apiCallRule.ReturnVariableName = "apiResult"; + + string jsonString = "{ \"test\": true }"; + apiCallRule.Content = jsonString; + + var variableRule2 = new VariableRule(); + variableRule2.Name = "Test api call result"; + variableRule2.Variables = new Dictionary + { + { "apiBodyJson", "bool.Parse(apiResult.Body.json.test.ToString())" } + }; + variableRule2.ConditionExpression = "apiResult.Body.json != null"; + + var variableRule3 = new VariableRule(); + variableRule3.Name = "Test apiBodyJson"; + variableRule3.ConditionExpression = "apiBodyJson == \"true\""; + + variableRule2.Rules.Add(variableRule3); + apiCallRule.Rules.Add(variableRule2); + + rules.Add(apiCallRule.AsRuleEngineRule()); + workflow.Rules = rules; + workflows.Add(workflow); + + return workflows; + } + } +} diff --git a/ActionFlow.Tests/Rules/VariableRuleTests.cs b/ActionFlow.Tests/Rules/VariableRuleTests.cs index 86f596e..b298b7c 100644 --- a/ActionFlow.Tests/Rules/VariableRuleTests.cs +++ b/ActionFlow.Tests/Rules/VariableRuleTests.cs @@ -16,7 +16,9 @@ public void When_running_rule_engine_with_basic_workflow_it_should_execute() var workflowProvider = Substitute.For(); workflowProvider.GetAllWorkflows().Returns(CreateBasicWorkflow()); - var rulesEngine = new RuleEngineWrapper(workflowProvider); + var resettingsProvider = Substitute.For(); + + var rulesEngine = new RuleEngineWrapper(workflowProvider, resettingsProvider); //Act var resultList = rulesEngine.ExecuteAllRulesAsync("Test Workflow Rule 1", new object[] { }).Result; @@ -33,7 +35,9 @@ public void When_running_rule_engine_with_false_condition_on_variable_rule_child var workflowProvider = Substitute.For(); workflowProvider.GetAllWorkflows().Returns(CreateConditionExpressionWorkflow()); - var rulesEngine = new RuleEngineWrapper(workflowProvider); + var resettingsProvider = Substitute.For(); + + var rulesEngine = new RuleEngineWrapper(workflowProvider, resettingsProvider); //Act var resultList = rulesEngine.ExecuteAllRulesAsync("Test Workflow Rule 1", new object[] { }).Result; diff --git a/ActionFlow/Domain/ApiCallResult.cs b/ActionFlow/Domain/ApiCallResult.cs new file mode 100644 index 0000000..d3ba94f --- /dev/null +++ b/ActionFlow/Domain/ApiCallResult.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Nodes; + +namespace ActionFlow.Domain +{ + public class ApiCallResult + { + public ApiCallResult(JsonNode body, Dictionary> headers, int statusCode) + { + Body = body; + Headers = headers; + StatusCode = statusCode; + } + + public JsonNode Body { get; } + public int StatusCode { get; } + public Dictionary> Headers { get; } = new Dictionary>(); + } +} diff --git a/ActionFlow/Engine/IReSettingsProvider.cs b/ActionFlow/Engine/IReSettingsProvider.cs new file mode 100644 index 0000000..d0f28e8 --- /dev/null +++ b/ActionFlow/Engine/IReSettingsProvider.cs @@ -0,0 +1,9 @@ +using RulesEngine.Models; + +namespace ActionFlow.Engine +{ + public interface IReSettingsProvider + { + ReSettings GetReSettings(); + } +} \ No newline at end of file diff --git a/ActionFlow/Engine/ReSettingsProvider.cs b/ActionFlow/Engine/ReSettingsProvider.cs new file mode 100644 index 0000000..4541357 --- /dev/null +++ b/ActionFlow/Engine/ReSettingsProvider.cs @@ -0,0 +1,19 @@ +using ActionFlow.Domain; +using ActionFlow.Helpers; +using RulesEngine.Models; + +namespace ActionFlow.Engine +{ + public class ReSettingsProvider : IReSettingsProvider + { + public ReSettings GetReSettings() + { + var reSettingsWithCustomTypes = new ReSettings + { + CustomTypes = new Type[] { typeof(ApiClient), typeof(ApiCallResult) } + }; + + return reSettingsWithCustomTypes; + } + } +} diff --git a/ActionFlow/Engine/RuleEngineWrapper.cs b/ActionFlow/Engine/RuleEngineWrapper.cs index fe8eced..b1d565c 100644 --- a/ActionFlow/Engine/RuleEngineWrapper.cs +++ b/ActionFlow/Engine/RuleEngineWrapper.cs @@ -7,9 +7,9 @@ public class RuleEngineWrapper : IRuleEngineWrapper { private readonly IRulesEngine _rulesEngine; - public RuleEngineWrapper(IWorkflowProvider workflowProvider) + public RuleEngineWrapper(IWorkflowProvider workflowProvider, IReSettingsProvider reSettingsProvider) { - _rulesEngine = new RulesEngine.RulesEngine(workflowProvider.GetAllWorkflows().ToArray(), null); + _rulesEngine = new RulesEngine.RulesEngine(workflowProvider.GetAllWorkflows().ToArray(), reSettingsProvider.GetReSettings()); } public ValueTask> ExecuteAllRulesAsync(string workflowName, params object[] inputs) diff --git a/ActionFlow/Helpers/ApiClient.cs b/ActionFlow/Helpers/ApiClient.cs new file mode 100644 index 0000000..5b567e4 --- /dev/null +++ b/ActionFlow/Helpers/ApiClient.cs @@ -0,0 +1,52 @@ +using ActionFlow.Domain; +using Newtonsoft.Json; +using System.Text.Json.Nodes; + +namespace ActionFlow.Helpers +{ + public static class ApiClient + { + public static async Task CallGet(string url, string? requestHeaders = null) + { + var httpClient = new HttpClient(); + AddRequestHeaders(requestHeaders, httpClient); + + var result = await httpClient.GetAsync(url); + return await CreateApiCallResult(result); + } + + public static async Task CallPost(string url, string data, string? requestHeaders = null) + { + var httpClient = new HttpClient(); + AddRequestHeaders(requestHeaders, httpClient); + + var json = JsonNode.Parse(data); + var content = new StringContent(json!.ToJsonString()); + var result = await httpClient.PostAsync(url, content); + + return await CreateApiCallResult(result); + } + + private static void AddRequestHeaders(string? requestHeaders, HttpClient httpClient) + { + if (requestHeaders != null) + { + var headerDictionary = JsonConvert.DeserializeObject>(requestHeaders); + foreach (var header in headerDictionary!) + { + httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); + } + } + } + + private static async Task CreateApiCallResult(HttpResponseMessage result) + { + var jsonBody = JsonNode.Parse(await result.Content.ReadAsStringAsync()); + var headers = result.Headers.ToDictionary(x => x.Key, x => x.Value); + var statusCode = (int)result.StatusCode; + + var apiCallResult = new ApiCallResult(jsonBody!, headers, statusCode); + return apiCallResult; + } + } +} diff --git a/ActionFlow/Rules/ApiCallRule.cs b/ActionFlow/Rules/ApiCallRule.cs new file mode 100644 index 0000000..52076db --- /dev/null +++ b/ActionFlow/Rules/ApiCallRule.cs @@ -0,0 +1,44 @@ +using Newtonsoft.Json; +using RulesEngine.Models; + +namespace ActionFlow.Rules +{ + public class ApiCallRule : RuleWrapper + { + public override string RuleType => "ApiCall"; + public string? Url { get; set; } + public string Method { get; set; } = "GET"; + public Dictionary Headers { get; set; } = new Dictionary(); + public string? ReturnVariableName { get; set; } + public string? Content { get; set; } + + public override Rule AsRuleEngineRule() + { + var rule = base.AsRuleEngineRule(); + var serializedHeaders = JsonConvert.SerializeObject(JsonConvert.SerializeObject(Headers)); + //JsonConvert.SerializeObject(Headers); + + switch (Method) + { + case "GET": + rule.LocalParams = new List + { + { new ScopedParam { Name = ReturnVariableName!, Expression = $"ApiClient.CallGet(\"{Url}\", {serializedHeaders}).Result" } } + }; + break; + + case "POST": + var content = JsonConvert.SerializeObject(Content); + + rule.LocalParams = new List + { + { new ScopedParam { Name = ReturnVariableName!, Expression = $"ApiClient.CallPost(\"{Url}\", {content}, {serializedHeaders}).Result" } } + }; + break; + } + + + return rule; + } + } +} diff --git a/ActionFlow/Rules/IRuleWrapper.cs b/ActionFlow/Rules/IRuleWrapper.cs index 2a16149..bf762c8 100644 --- a/ActionFlow/Rules/IRuleWrapper.cs +++ b/ActionFlow/Rules/IRuleWrapper.cs @@ -9,7 +9,6 @@ public interface IRuleWrapper string Name { get; set; } string RuleType { get; } string SuccessEvent { get; set; } - Dictionary Variables { get; set; } Rule AsRuleEngineRule(); } diff --git a/ActionFlow/Rules/RuleWrapper.cs b/ActionFlow/Rules/RuleWrapper.cs new file mode 100644 index 0000000..a6858ce --- /dev/null +++ b/ActionFlow/Rules/RuleWrapper.cs @@ -0,0 +1,38 @@ +using RulesEngine.Models; + +namespace ActionFlow.Rules +{ + public abstract class RuleWrapper : IRuleWrapper + { + public virtual string RuleType { get; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string SuccessEvent { get; set; } = string.Empty; + public string ErrorMessage { get; set; } = string.Empty; + public string ConditionExpression { get; set; } = string.Empty; + public List Rules { get; set; } = new List { }; + + public virtual Rule AsRuleEngineRule() + { + var rule = new Rule(); + + rule.RuleName = $"{RuleType} - {Name}"; + rule.SuccessEvent = SuccessEvent; + rule.ErrorMessage = ErrorMessage; + + if (ConditionExpression != string.Empty) + { + rule.Expression = ConditionExpression; + rule.RuleExpressionType = RuleExpressionType.LambdaExpression; + } + + rule.Rules = Rules.Select(x => x.AsRuleEngineRule()).ToList(); + + if (rule.Rules.Any()) + { + rule.Operator = "And"; + } + + return rule; + } + } +} diff --git a/ActionFlow/Rules/VariableRule.cs b/ActionFlow/Rules/VariableRule.cs index 3756314..cef8584 100644 --- a/ActionFlow/Rules/VariableRule.cs +++ b/ActionFlow/Rules/VariableRule.cs @@ -5,47 +5,20 @@ namespace ActionFlow.Rules /// /// Variable Rule is a kind of rule for storing variables which stored values can be used by child rules /// - public class VariableRule : IRuleWrapper + public class VariableRule : RuleWrapper { - public string RuleType => "Variable"; - public string Name { get; set; } = string.Empty; - public string SuccessEvent { get; set; } = string.Empty; - public string ErrorMessage { get; set; } = string.Empty; - public string ConditionExpression { get; set; } = string.Empty; + public override string RuleType => "Variable"; public Dictionary Variables { get; set; } = new Dictionary(); - public List Rules { get; set; } = new List { }; - public Rule AsRuleEngineRule() + public override Rule AsRuleEngineRule() { - Rule rule = new Rule(); - - rule.RuleName = $"{RuleType} - {Name}"; - rule.SuccessEvent = SuccessEvent; - rule.ErrorMessage = ErrorMessage; + var rule = base.AsRuleEngineRule(); rule.LocalParams = Variables.Select(x => { return new ScopedParam { Name = x.Key, Expression = x.Value }; }); - if (ConditionExpression != string.Empty) - { - rule.Expression = ConditionExpression; - rule.RuleExpressionType = RuleExpressionType.LambdaExpression; - } - - //if (!rule.LocalParams.Any()) - //{ - // rule.Expression = ConditionExpression; - //} - - rule.Rules = Rules.Select(x => x.AsRuleEngineRule()).ToList(); - - if (rule.Rules.Any()) - { - rule.Operator = "And"; - } - return rule; } }