Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Giacomo Cavallo Home assignment #647

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 22 additions & 17 deletions jobs/Backend/Task/ExchangeRate.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
namespace ExchangeRateUpdater
using System;

namespace ExchangeRateUpdater
{
public class ExchangeRate
{
public ExchangeRate(Currency sourceCurrency, Currency targetCurrency, decimal value)
{
SourceCurrency = sourceCurrency;
TargetCurrency = targetCurrency;
Value = value;
}
public class ExchangeRate
{
public ExchangeRate(Currency sourceCurrency, Currency targetCurrency, decimal value, DateTime lastUpdate)
{
SourceCurrency = sourceCurrency;
TargetCurrency = targetCurrency;
Value = value;
LastUpdate = lastUpdate;
}

public Currency SourceCurrency { get; }
public Currency SourceCurrency { get; }

public Currency TargetCurrency { get; }
public Currency TargetCurrency { get; }

public decimal Value { get; }
public decimal Value { get; }

public DateTime LastUpdate { get; }

public override string ToString()
{
return $"{SourceCurrency}/{TargetCurrency}={Value}";
}
}
public override string ToString()
{
return $"{SourceCurrency}/{TargetCurrency}={Value}";
}
}
}
84 changes: 84 additions & 0 deletions jobs/Backend/Task/ExchangeRateApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;

namespace ExchangeRateUpdater
{
public interface IExchangeRateApi
{
Task<ExchangeRateDitributor> GetLastExchangeRateAsync();
}

public class CzechExchangeRateApi : IExchangeRateApi
{

private readonly HttpClient _httpClient;

public CzechExchangeRateApi()
{
_httpClient = new HttpClient()
{
BaseAddress = new Uri("https://api.cnb.cz"),
};
}

public async Task<ExchangeRateDitributor> GetLastExchangeRateAsync()
{
string path = "/cnbapi/exrates/daily?lang=EN";

HttpResponseMessage response = await _httpClient.GetAsync(path);

if(response.IsSuccessStatusCode)
{
ExrateResponseDto responseDto = await response.Content.ReadFromJsonAsync<ExrateResponseDto>();

List<ExchangeRate> rates = new();
DateTime lastUpdate = DateTime.MaxValue;

responseDto.Rates.ForEach(rateDto =>
{
rates.Add(ToExchangeRate(rateDto));
lastUpdate = new DateTime(Math.Min(lastUpdate.Ticks, rateDto.ValidFor.Ticks));
});

return new ExchangeRateDitributor(rates, lastUpdate);
}

throw await ThrowException(response);
}

private static async Task<Exception> ThrowException(HttpResponseMessage errorResponse)
{
try
{
ErrorResponse errorDto = await errorResponse.Content.ReadFromJsonAsync<ErrorResponse>();
return new ExchangeRateApiException($"Unexpected {errorResponse.StatusCode} response from {errorDto.EndPoint} with error code {errorDto.ErrorCode} cause {errorDto.Description}");
}
catch(JsonException e)
{
return new ExchangeRateApiException($"Unexpected {errorResponse.StatusCode} response.", e);
}
}

private static ExchangeRate ToExchangeRate(ExRateDto dto)
{
decimal value = dto.Rate/dto.Amount;
return new ExchangeRate(new Currency(dto.CurrencyCode), new("CZK"), value, dto.ValidFor);
}

private record ExRateDto(int Amount, string Country, string Currency, string CurrencyCode, int Order, decimal Rate, DateTime ValidFor);
private record ExrateResponseDto(List<ExRateDto> Rates);
private record ErrorResponse(string Description, string EndPoint, string ErrorCode, string MessageId);

}

public class ExchangeRateApiException: Exception
{
public ExchangeRateApiException(string message): base(message){}
public ExchangeRateApiException(string message, Exception innerException): base(message, innerException){}
}

}
29 changes: 29 additions & 0 deletions jobs/Backend/Task/ExchangeRateDitributor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace ExchangeRateUpdater
{
public class ExchangeRateDitributor
{
private readonly Dictionary<string, ExchangeRate> _exchangeRates;
public DateTime LastUpdateDate { get; }

public ExchangeRateDitributor(IEnumerable<ExchangeRate> rates, DateTime lastUpdate)
{
_exchangeRates = rates.ToDictionary(r => r.SourceCurrency.Code);
LastUpdateDate = lastUpdate;
}

public bool TryGet(Currency currency, out ExchangeRate exRate)
{
if(currency == null || string.IsNullOrEmpty(currency.Code))
{
exRate = null;
return false;
}

return _exchangeRates.TryGetValue(currency.Code, out exRate);
}
}
}
80 changes: 66 additions & 14 deletions jobs/Backend/Task/ExchangeRateProvider.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,71 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ExchangeRateUpdater
{
public class ExchangeRateProvider
{
/// <summary>
/// Should return exchange rates among the specified currencies that are defined by the source. But only those defined
/// by the source, do not return calculated exchange rates. E.g. if the source contains "CZK/USD" but not "USD/CZK",
/// do not return exchange rate "USD/CZK" with value calculated as 1 / "CZK/USD". If the source does not provide
/// some of the currencies, ignore them.
/// </summary>
public IEnumerable<ExchangeRate> GetExchangeRates(IEnumerable<Currency> currencies)
{
return Enumerable.Empty<ExchangeRate>();
}
}
public class ExchangeRateProvider
{
private readonly IExchangeRateApi _exchangeRateApi;
private readonly ICache<ExchangeRateDitributor> _exchangeRateCache = null;
private readonly TimeSpan? _cacheDuration = null;
private readonly bool _useCache = false;
private const string CZECH_EXRATE_CACHE_KEY = "czech-exchange-rate";


public ExchangeRateProvider(IExchangeRateApi exchageRateApi)
{
ArgumentNullException.ThrowIfNull(exchageRateApi);

_exchangeRateApi = exchageRateApi;
}

public ExchangeRateProvider(IExchangeRateApi exchageRateApi, ICache<ExchangeRateDitributor> exchangeRateCache, TimeSpan cacheDuration): this(exchageRateApi)
{
ArgumentNullException.ThrowIfNull(exchangeRateCache);

_exchangeRateCache = exchangeRateCache;
_cacheDuration = cacheDuration;
_useCache = true;
}

/// <summary>
/// Should return exchange rates among the specified currencies that are defined by the source. But only those defined
/// by the source, do not return calculated exchange rates. E.g. if the source contains "CZK/USD" but not "USD/CZK",
/// do not return exchange rate "USD/CZK" with value calculated as 1 / "CZK/USD". If the source does not provide
/// some of the currencies, ignore them.
/// </summary>
public async Task<IEnumerable<ExchangeRate>> GetExchangeRatesAsync(IEnumerable<Currency> currencies)
{
ArgumentNullException.ThrowIfNull(currencies);

ExchangeRateDitributor exchangeRateDistributor = await GetExchangeRates();
List<ExchangeRate> rates = new();

foreach(Currency c in currencies)
{
if(exchangeRateDistributor.TryGet(c, out ExchangeRate rate))
{
rates.Add(rate);
}
}

return rates;
}

private async Task<ExchangeRateDitributor> GetExchangeRates()
{
if(_useCache && _exchangeRateCache.TryGet(CZECH_EXRATE_CACHE_KEY, out ExchangeRateDitributor exchangeRateDistributor))
return exchangeRateDistributor;

exchangeRateDistributor = await _exchangeRateApi.GetLastExchangeRateAsync();

if(_useCache)
_exchangeRateCache.Add(CZECH_EXRATE_CACHE_KEY, exchangeRateDistributor, DateTime.UtcNow.Add(_cacheDuration.Value));

return exchangeRateDistributor;
}

}
}
82 changes: 82 additions & 0 deletions jobs/Backend/Task/IExchangeRateCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;

namespace ExchangeRateUpdater
{
public interface ICache<T>
{
public void Add(string key, T exchangeRate, DateTime? expiration=null);

public T Get(string key);

public bool Exists(string key);

public bool TryGet(string key, out T exchangeRate);
}

public class InMemoryExchangeRateCache : ICache<ExchangeRateDitributor>
{

private readonly Dictionary<string, CacheEntry> _cache;

public InMemoryExchangeRateCache()
{
_cache = new();
}

public void Add(string key, ExchangeRateDitributor exchangeRate, DateTime? utcExpiration = null)
{
CacheEntry entry = new(exchangeRate, utcExpiration);
if(!_cache.TryAdd(key, entry))
{
_cache[key] = entry;
}
}

public bool Exists(string key)
{
return _cache.TryGetValue(key, out CacheEntry cacheEntry) && !cacheEntry.IsExpired();
}

public ExchangeRateDitributor Get(string key)
{
if(_cache.TryGetValue(key, out CacheEntry cacheEntry) && !cacheEntry.IsExpired())
return cacheEntry.Value;

throw new KeyNotFoundException($"Key {key} not found in cache");
}

public bool TryGet(string key, out ExchangeRateDitributor exchangeRate)
{
if(_cache.TryGetValue(key, out var cacheEntry) && !cacheEntry.IsExpired())
{
exchangeRate = cacheEntry.Value;
return true;
}

exchangeRate = default;
return false;
}

private class CacheEntry
{
public CacheEntry(ExchangeRateDitributor value, DateTime? utcExpiration)
{
Value = value;
_utcExpiration = utcExpiration;
}

public ExchangeRateDitributor Value { get; }

private readonly DateTime? _utcExpiration = null;

public bool IsExpired()
{
if(_utcExpiration.HasValue)
return _utcExpiration.Value < DateTime.UtcNow;

return false;
}
}
}
}
Loading