Skip to content

Commit

Permalink
feat(RestValueExecutionService): can get values from REST API
Browse files Browse the repository at this point in the history
  • Loading branch information
pkuehnel committed Mar 3, 2024
1 parent a61cf65 commit 0ab25e7
Show file tree
Hide file tree
Showing 11 changed files with 253 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ public class RestValueResultConfiguration
{
public int Id { get; set; }
public string? NodePattern { get; set; }
public float CorrectionFactor { get; set; }
public string? XmlAttributeHeaderName { get; set; }
public string? XmlAttributeHeaderValue { get; set; }
public string? XmlAttributeValueName { get; set; }
public decimal CorrectionFactor { get; set; }
public ValueUsage UsedFor { get; set; }
public ValueOperator Operator { get; set; }

Expand Down
1 change: 1 addition & 0 deletions TeslaSolarCharger.Services/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddServicesDependencies(this IServiceCollection services) =>
services
.AddTransient<IRestValueConfigurationService, RestValueConfigurationService>()
.AddTransient<IRestValueExecutionService, RestValueExecutionService>()
;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using TeslaSolarCharger.Shared.Dtos.RestValueConfiguration;

namespace TeslaSolarCharger.Services.Services.Contracts;

public interface IRestValueExecutionService
{
/// <summary>
/// Get result for each configuration ID
/// </summary>
/// <param name="config">Rest Value configuration</param>
/// <param name="headers">Headers for REST request</param>
/// <param name="resultConfigurations">Configurations to extract the values</param>
/// <returns>Dictionary with with resultConfiguration as key and resulting value as Value</returns>
/// <exception cref="InvalidOperationException">Throw if request results in not success status code</exception>
Task<Dictionary<int, decimal>> GetResult(DtoRestValueConfiguration config,
List<DtoRestValueConfigurationHeader> headers,
List<DtoRestValueResultConfiguration> resultConfigurations);
}
110 changes: 110 additions & 0 deletions TeslaSolarCharger.Services/Services/RestValueExecutionService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Xml;
using TeslaSolarCharger.Services.Services.Contracts;
using TeslaSolarCharger.Shared.Dtos.RestValueConfiguration;
using TeslaSolarCharger.SharedModel.Enums;


[assembly: InternalsVisibleTo("TeslaSolarCharger.Tests")]
namespace TeslaSolarCharger.Services.Services;

public class RestValueExecutionService(
ILogger<RestValueConfigurationService> logger) : IRestValueExecutionService
{
/// <summary>
/// Get result for each configuration ID
/// </summary>
/// <param name="config">Rest Value configuration</param>
/// <param name="headers">Headers for REST request</param>
/// <param name="resultConfigurations">Configurations to extract the values</param>
/// <returns>Dictionary with with resultConfiguration as key and resulting value as Value</returns>
/// <exception cref="InvalidOperationException">Throw if request results in not success status code</exception>
public async Task<Dictionary<int, decimal>> GetResult(DtoRestValueConfiguration config,
List<DtoRestValueConfigurationHeader> headers,
List<DtoRestValueResultConfiguration> resultConfigurations)
{
logger.LogTrace("{method}({@config}, {@headers}, {resultConfigurations})", nameof(GetResult), config, headers, resultConfigurations);
var client = new HttpClient();
var request = new HttpRequestMessage(new HttpMethod(config.HttpMethod.ToString()), config.Url);
foreach (var header in headers)
{
request.Headers.Add(header.Key, header.Value);
}
var response = await client.SendAsync(request).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var contentString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
logger.LogError("Requesting JSON Result with url {requestUrl} did result in non success status code: {statusCode} {content}", config.Url, response.StatusCode, contentString);
throw new InvalidOperationException($"Requesting JSON Result with url {config.Url} did result in non success status code: {response.StatusCode} {contentString}");
}
var results = new Dictionary<int, decimal>();
foreach (var resultConfig in resultConfigurations)
{
var contentString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
results.Add(resultConfig.Id, GetValue(contentString, config.NodePatternType, resultConfig));
}
return results;
}

internal decimal GetValue(string responseString, NodePatternType configNodePatternType, DtoRestValueResultConfiguration resultConfig)
{
logger.LogTrace("{method}({responseString}, {configNodePatternType}, {@resultConfig})", nameof(GetValue), responseString, configNodePatternType, resultConfig);
decimal rawValue;
switch (configNodePatternType)
{
case NodePatternType.Direct:
rawValue = decimal.Parse(responseString, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture);
break;
case NodePatternType.Json:
var jsonTokenString = (JObject.Parse(responseString).SelectToken(resultConfig.NodePattern ?? throw new ArgumentNullException(nameof(resultConfig.NodePattern))) ??
throw new InvalidOperationException("Could not find token by pattern")).Value<string>() ?? "0";
rawValue = decimal.Parse(jsonTokenString, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture);
break;
case NodePatternType.Xml:
var xmlDocument = new XmlDocument();
xmlDocument.LoadXml(responseString);
var nodes = xmlDocument.SelectNodes(resultConfig.NodePattern ?? throw new ArgumentNullException(nameof(resultConfig.NodePattern))) ?? throw new InvalidOperationException("Could not find any nodes by pattern");
var xmlTokenString = string.Empty;
switch (nodes.Count)
{
case < 1:
throw new InvalidOperationException($"Could not find any nodes with pattern {resultConfig.NodePattern}");
case 1:
xmlTokenString = nodes[0]?.LastChild?.Value ?? "0";
break;
case > 2:
for (var i = 0; i < nodes.Count; i++)
{
if (nodes[i]?.Attributes?[resultConfig.XmlAttributeHeaderName ?? throw new ArgumentNullException(nameof(resultConfig.XmlAttributeHeaderName))]?.Value == resultConfig.XmlAttributeHeaderValue)
{
xmlTokenString = nodes[i]?.Attributes?[resultConfig.XmlAttributeValueName ?? throw new ArgumentNullException(nameof(resultConfig.XmlAttributeValueName))]?.Value ?? "0";
break;
}
}
break;
}
rawValue = decimal.Parse(xmlTokenString, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture);
break;
default:
throw new InvalidOperationException($"NodePatternType {configNodePatternType} not supported");
}
return MakeCalculationsOnRawValue(resultConfig.CorrectionFactor, resultConfig.Operator, rawValue);
}

internal decimal MakeCalculationsOnRawValue(decimal correctionFactor, ValueOperator valueOperator, decimal rawValue)
{
rawValue = correctionFactor * rawValue;
switch (valueOperator)
{
case ValueOperator.Plus:
return rawValue;
case ValueOperator.Minus:
return -rawValue;
default:
throw new ArgumentOutOfRangeException();
}
}
}
58 changes: 55 additions & 3 deletions TeslaSolarCharger.Tests/Data/DataGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,63 @@
using TeslaSolarCharger.Model.EntityFramework;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using TeslaSolarCharger.Model.Entities.TeslaSolarCharger;
using TeslaSolarCharger.Model.EntityFramework;
using TeslaSolarCharger.SharedModel.Enums;

namespace TeslaSolarCharger.Tests.Data;

public static class DataGenerator
{
public static void InitContextData(this TeslaSolarChargerContext ctx)
public static string _httpLocalhostApiValues = "http://localhost:5000/api/values";
public static NodePatternType _nodePatternType = NodePatternType.Json;
public static HttpVerb _httpMethod = HttpVerb.Get;
public static string _headerKey = "Authorization";
public static string _headerValue = "Bearer asdf";
public static string? _nodePattern = "$.data";
public static decimal _correctionFactor = 1;
public static ValueUsage _valueUsage = ValueUsage.GridPower;
public static ValueOperator _valueOperator = ValueOperator.Plus;


public static TeslaSolarChargerContext InitSpotPrices(this TeslaSolarChargerContext context)
{
context.SpotPrices.Add(new SpotPrice()
{
StartDate = new DateTime(2023, 1, 22, 17, 0, 0),
EndDate = new DateTime(2023, 1, 22, 18, 0, 0), Price = new decimal(0.11)
});
return context;
}

public static TeslaSolarChargerContext InitRestValueConfigurations(this TeslaSolarChargerContext context)
{
ctx.InitSpotPrices();
context.RestValueConfigurations.Add(new RestValueConfiguration()
{
Url = _httpLocalhostApiValues,
NodePatternType = _nodePatternType,
HttpMethod = _httpMethod,
Headers = new List<RestValueConfigurationHeader>()
{
new RestValueConfigurationHeader()
{
Key = _headerKey,
Value = _headerValue,
},
},
RestValueResultConfigurations = new List<RestValueResultConfiguration>()
{
new RestValueResultConfiguration()
{
NodePattern = _nodePattern,
CorrectionFactor = _correctionFactor,
UsedFor = _valueUsage,
Operator = _valueOperator,
},
},
});
return context;
}
}
18 changes: 0 additions & 18 deletions TeslaSolarCharger.Tests/Data/SpotPriceDataGenerator.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading.Tasks;
using TeslaSolarCharger.Model.Entities.TeslaSolarCharger;
using TeslaSolarCharger.SharedModel.Enums;
using TeslaSolarCharger.Tests.Data;
using Xunit;
using Xunit.Abstractions;
#pragma warning disable xUnit2013
Expand All @@ -14,34 +15,24 @@ namespace TeslaSolarCharger.Tests.Services.Services;
[SuppressMessage("ReSharper", "UseConfigureAwaitFalse")]
public class RestValueConfigurationService(ITestOutputHelper outputHelper) : TestBase(outputHelper)
{
private string _httpLocalhostApiValues = "http://localhost:5000/api/values";
private NodePatternType _nodePatternType = NodePatternType.Json;
private HttpVerb _httpMethod = HttpVerb.Get;
private string _headerKey = "Authorization";
private string _headerValue = "Bearer asdf";
private string? _nodePattern = "$.data";
private float _correctionFactor = 1;
private ValueUsage _valueUsage = ValueUsage.GridPower;
private ValueOperator _valueOperator = ValueOperator.Plus;


[Fact]
public async Task Can_Get_Rest_Configurations()
{
await GenerateDemoData();
var service = Mock.Create<TeslaSolarCharger.Services.Services.RestValueConfigurationService>();
var restValueConfigurations = await service.GetAllRestValueConfigurations();
Assert.NotEmpty(restValueConfigurations);
Assert.Equal(1, restValueConfigurations.Count);
var firstValue = restValueConfigurations.First();
Assert.Equal(_httpLocalhostApiValues, firstValue.Url);
Assert.Equal(_nodePatternType, firstValue.NodePatternType);
Assert.Equal(_httpMethod, firstValue.HttpMethod);
Assert.Equal(DataGenerator._httpLocalhostApiValues, firstValue.Url);
Assert.Equal(DataGenerator._nodePatternType, firstValue.NodePatternType);
Assert.Equal(DataGenerator._httpMethod, firstValue.HttpMethod);
}

[Fact]
public async Task Can_Update_Rest_Configurations()
{
await GenerateDemoData();
var service = Mock.Create<TeslaSolarCharger.Services.Services.RestValueConfigurationService>();
var restValueConfigurations = await service.GetAllRestValueConfigurations();
var firstValue = restValueConfigurations.First();
Expand All @@ -61,22 +52,20 @@ public async Task Can_Update_Rest_Configurations()
[Fact]
public async Task Can_Get_Rest_Configuration_Headers()
{
await GenerateDemoData();
var service = Mock.Create<TeslaSolarCharger.Services.Services.RestValueConfigurationService>();
var restValueConfigurations = await service.GetAllRestValueConfigurations();
var firstValue = restValueConfigurations.First();
var headers = await service.GetHeadersByConfigurationId(firstValue.Id);
Assert.NotEmpty(headers);
Assert.Equal(1, headers.Count);
var firstHeader = headers.First();
Assert.Equal(_headerKey, firstHeader.Key);
Assert.Equal(_headerValue, firstHeader.Value);
Assert.Equal(DataGenerator._headerKey, firstHeader.Key);
Assert.Equal(DataGenerator._headerValue, firstHeader.Value);
}

[Fact]
public async Task Can_Update_Rest_Configuration_Headers()
{
await GenerateDemoData();
var service = Mock.Create<TeslaSolarCharger.Services.Services.RestValueConfigurationService>();
var restValueConfigurations = await service.GetAllRestValueConfigurations();
var firstValue = restValueConfigurations.First();
Expand All @@ -95,24 +84,22 @@ public async Task Can_Update_Rest_Configuration_Headers()
[Fact]
public async Task Can_Get_Rest_Result_Configurations()
{
await GenerateDemoData();
var service = Mock.Create<TeslaSolarCharger.Services.Services.RestValueConfigurationService>();
var restValueConfigurations = await service.GetAllRestValueConfigurations();
var firstValue = restValueConfigurations.First();
var values = await service.GetResultConfigurationByConfigurationId(firstValue.Id);
Assert.NotEmpty(values);
Assert.Equal(1, values.Count);
var firstHeader = values.First();
Assert.Equal(_nodePattern, firstHeader.NodePattern);
Assert.Equal(_correctionFactor, firstHeader.CorrectionFactor);
Assert.Equal(_valueUsage, firstHeader.UsedFor);
Assert.Equal(_valueOperator, firstHeader.Operator);
Assert.Equal(DataGenerator._nodePattern, firstHeader.NodePattern);
Assert.Equal(DataGenerator._correctionFactor, firstHeader.CorrectionFactor);
Assert.Equal(DataGenerator._valueUsage, firstHeader.UsedFor);
Assert.Equal(DataGenerator._valueOperator, firstHeader.Operator);
}

[Fact]
public async Task Can_Update_Rest_Result_Configurations()
{
await GenerateDemoData();
var service = Mock.Create<TeslaSolarCharger.Services.Services.RestValueConfigurationService>();
var restValueConfigurations = await service.GetAllRestValueConfigurations();
var firstValue = restValueConfigurations.First();
Expand All @@ -133,35 +120,4 @@ public async Task Can_Update_Rest_Result_Configurations()
var id = await service.SaveResultConfiguration(firstValue.Id, firstHeader);
Assert.Equal(firstHeader.Id, id);
}

private async Task GenerateDemoData()
{
Context.RestValueConfigurations.Add(new RestValueConfiguration()
{
Url = _httpLocalhostApiValues,
NodePatternType = _nodePatternType,
HttpMethod = _httpMethod,
Headers = new List<RestValueConfigurationHeader>()
{
new RestValueConfigurationHeader()
{
Key = _headerKey,
Value = _headerValue,
},
},
RestValueResultConfigurations = new List<RestValueResultConfiguration>()
{
new RestValueResultConfiguration()
{
NodePattern = _nodePattern,
CorrectionFactor = _correctionFactor,
UsedFor = _valueUsage,
Operator = _valueOperator,
},
},
});
await Context.SaveChangesAsync();
Context.ChangeTracker.Entries().Where(e => e.State != EntityState.Detached).ToList()
.ForEach(entry => entry.State = EntityState.Detached);
}
}
Loading

0 comments on commit 0ab25e7

Please sign in to comment.