Skip to content
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
485 changes: 485 additions & 0 deletions jobs/Backend/Task/.gitignore

Large diffs are not rendered by default.

19 changes: 0 additions & 19 deletions jobs/Backend/Task/ExchangeRateProvider.cs

This file was deleted.

15 changes: 14 additions & 1 deletion jobs/Backend/Task/ExchangeRateUpdater.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,20 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
</ItemGroup>

<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
11 changes: 11 additions & 0 deletions jobs/Backend/Task/Interfaces/IExchangeRateProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using ExchangeRateUpdater.Models;

namespace ExchangeRateUpdater.Interfaces
{
public interface IExchangeRateProvider
{
Task<IEnumerable<ExchangeRate>> GetExchangeRates(IEnumerable<Currency> currencies);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace ExchangeRateUpdater
namespace ExchangeRateUpdater.Models
{
public class Currency
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace ExchangeRateUpdater
namespace ExchangeRateUpdater.Models
{
public class ExchangeRate
{
Expand Down
31 changes: 25 additions & 6 deletions jobs/Backend/Task/Program.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ExchangeRateUpdater.Interfaces;
using ExchangeRateUpdater.Models;
using ExchangeRateUpdater.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace ExchangeRateUpdater
{
public static class Program
{
private static IEnumerable<Currency> currencies = new[]
{
private static IEnumerable<Currency> currencies =
[
new Currency("USD"),
new Currency("EUR"),
new Currency("CZK"),
Expand All @@ -17,14 +24,26 @@ public static class Program
new Currency("THB"),
new Currency("TRY"),
new Currency("XYZ")
};
];

public static void Main(string[] args)
public static async Task Main(string[] args)
{

IConfiguration configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();

var serviceProvider = new ServiceCollection()
.AddSingleton(configuration)
.AddScoped<IExchangeRateProvider, ExchangeRateProvider>()
.BuildServiceProvider();

IExchangeRateProvider exchangeRateProvider = serviceProvider.GetRequiredService<IExchangeRateProvider>();

try
{
var provider = new ExchangeRateProvider();
var rates = provider.GetExchangeRates(currencies);
var rates = await exchangeRateProvider.GetExchangeRates(currencies);

Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:");
foreach (var rate in rates)
Expand Down
38 changes: 38 additions & 0 deletions jobs/Backend/Task/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Overview

The Mews take home challenge was a unique challenge based on querying an https endpoint via an console application. A note to mention here is the API responses return data in txt format as opposed to JSON which is generally used across HTTP APIs. Hence the file was meant to be read and parsed on that basis.

The solution implements the following:

- `HttpClientAdapter.cs` : queries the URLs for `DailyRate` and `fx_rates` and reading the result as a Stream.
- `ExchangeRateProvider.cs` : Calls the `httpClient` safely using Disposable methods to avoid memory leaks. Reads the file line by line and formats the result using string manipulation and finally stores the valid results from the table into a `List` object.

# Set up instructions

```bash
git clone https://github.com/sraxler/developers.git
cd jobs/Backend/Task
dotnet restore
dotnet build
dotnet run
```

# Principles used

- KISS: Keep it super simple - the task required one function that could be broken into smaller objectives but its key to keep your code readable. As a result, I implemented a simple solution that mostly relies on 2 files.
- Inversion of Control - To ensure object lifetimes through the length of the program and to safely initiate them from the Main function saves overhead of having to define a new object a constructor.

# Architecture overview

![ExchangeRateProvider system design](attachment:c5bd5aa2-a5fd-4858-a6aa-7684ecbeee3e:image.png)

ExchangeRateProvider system design

# Result

![image.png](attachment:7bb2c428-4337-417b-9262-dbcfc4089109:image.png)

# Future work

- Swap `HttpClient` for `HttpClientFactory` if console app requires making multiple API calls for a recurring application that stays live until it is stopped.
- Create a text file to store data on Local machine using the response to avoid querying the API multiple times as the website states daily rates are updated on business days at 2:30 pm and fx_rates are updated once a month. Since the data is not too large and caching would work only if the application has a longer runtime - text files would suffice.
78 changes: 78 additions & 0 deletions jobs/Backend/Task/Services/ExchangeRateProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ExchangeRateUpdater.Interfaces;
using ExchangeRateUpdater.Models;
using Microsoft.Extensions.Configuration;

namespace ExchangeRateUpdater.Services
{
public class ExchangeRateProvider : IExchangeRateProvider
{
private readonly string CNB_URL_DAILY;
private readonly string CNB_URL_OTHER_RATES;
private const char DELIMITER = '|';
private const byte AMOUNT_COLUMN = 2;
private const byte CODE_COLUMN = 3;
private const byte RATE_COLUMN = 4;
private readonly Currency _targetCurrency;

public ExchangeRateProvider(IConfiguration configuration)
{
_targetCurrency = new Currency("CZK");
CNB_URL_DAILY = configuration["ExchangeRateProvider:CnbUrlDaily"];
CNB_URL_OTHER_RATES = configuration["ExchangeRateProvider:CnbUrlOtherRates"];
}

/// <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>> GetExchangeRates(IEnumerable<Currency> currencies)
{
var daily = GetExchangeRatesFromUrlAsync(currencies, CNB_URL_DAILY);
var otherRates = GetExchangeRatesFromUrlAsync(currencies, CNB_URL_OTHER_RATES);

await Task.WhenAll(daily, otherRates);

return daily.Result.Concat(otherRates.Result);
}

private async Task<IEnumerable<ExchangeRate>> GetExchangeRatesFromUrlAsync(IEnumerable<Currency> currencies, string url)
{
List<ExchangeRate> result = new List<ExchangeRate>();

using (var client = new HttpClientAdapter())
{
using var reader = new StreamReader(await client.GetStreamAsync(url));
await SkipFileHeaderAsync(reader);

string fileLine;
while ((fileLine = await reader.ReadLineAsync()) != null)
{
string[] commaSeparatedLine = fileLine.Split(DELIMITER);

if (currencies.Select(c => c.Code).Contains(commaSeparatedLine[CODE_COLUMN]))
{
if (decimal.TryParse(commaSeparatedLine[AMOUNT_COLUMN], out decimal amount)
&& decimal.TryParse(commaSeparatedLine[RATE_COLUMN], out decimal rate))
{
result.Add(new ExchangeRate(new Currency(commaSeparatedLine[CODE_COLUMN]), _targetCurrency, rate / amount));
}
}
}
}
return result;
}

private async Task SkipFileHeaderAsync(StreamReader reader)
{
await reader.ReadLineAsync();
await reader.ReadLineAsync();
}
}
}
30 changes: 30 additions & 0 deletions jobs/Backend/Task/Services/HttpClientAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;

namespace ExchangeRateUpdater.Services
{
public class HttpClientAdapter: IDisposable
{
private HttpClient _client;

public HttpClientAdapter()
{
_client = new HttpClient();
}

public async Task<Stream> GetStreamAsync(string url)
{
var response = await _client.GetAsync(url);
response.EnsureSuccessStatusCode();

return await response.Content.ReadAsStreamAsync();
}

public void Dispose()
{
_client.Dispose();
}
}
}
6 changes: 6 additions & 0 deletions jobs/Backend/Task/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"ExchangeRateProvider": {
"CnbUrlDaily": "https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/daily.txt",
"CnbUrlOtherRates": "https://www.cnb.cz/en/financial-markets/foreign-exchange-market/fx-rates-of-other-currencies/fx-rates-of-other-currencies/fx_rates.txt"
}
}