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

FR-99 AeroDataBox API Integration #23

Merged
merged 2 commits into from
Nov 5, 2023
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
using FlightRecorder.Entities.Api;
using FlightRecorder.Entities.Interfaces;
using FlightRecorder.Entities.Logging;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace FlightRecorder.BusinessLogic.Api.AeroDataBox
{
public partial class AeroDataBoxAircraftApi : ExternalApiBase, IAircraftApi
{
private const string DateFormat = "yyyy-MM-dd";
private const double DaysPerYear = 365.2425;

private readonly string _baseAddress;
private readonly string _host;
private readonly string _key;

public AeroDataBoxAircraftApi(
IFlightRecorderLogger logger,
IFlightRecorderHttpClient client,
string url,
string key)
: base(logger, client)
{
_baseAddress = url;
_key = key;

// The URL contains the protocol, host and base route (if any), but we need to extract the host name only
// to pass in the headers as the RapidAPI host, so capture the host and the full URL
Uri uri = new(url);
_host = uri.Host;
}

/// <summary>
/// Look up aircraft details given a registration number
/// </summary>
/// <param name="registration"></param>
/// <returns></returns>
public async Task<Dictionary<ApiPropertyType, string>> LookupAircraftByRegistration(string registration)
{
Logger.LogMessage(Severity.Info, $"Looking up aircraft with registration number {registration}");
var properties = await MakeApiRequest($"reg/{registration}");
return properties;
}

/// <summary>
/// Look up aircraft details given a 24-bit ICAO address
/// </summary>
/// <param name="address"></param>
/// <returns></returns>
public async Task<Dictionary<ApiPropertyType, string>> LookupAircraftByICAOAddress(string address)
{
Logger.LogMessage(Severity.Info, $"Looking up aircraft with 24-bit ICAO address {address}");
var properties = await MakeApiRequest($"icao24/{address}");
return properties;
}

/// <summary>
/// Make a request for flight details using the specified parameters
/// </summary>
/// <param name="parameters"></param>
/// <returns></returns>
private async Task<Dictionary<ApiPropertyType, string>> MakeApiRequest(string parameters)
{
Dictionary<ApiPropertyType, string> properties = null;

// Define the properties to be extracted from the response
List<ApiPropertyDefinition> definitions = new()
{
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.AircraftRegistration, JsonPath = "reg" },
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.AircraftICAOAddress, JsonPath = "hexIcao" },
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.AircraftSerialNumber, JsonPath = "serial" },
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.AircraftRegistrationDate, JsonPath = "registrationDate" },
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.AircraftModel, JsonPath = "model" },
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.AircraftModelCode, JsonPath = "modelCode" },
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.AircraftType, JsonPath = "typeName" },
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.AircraftProductionLine, JsonPath = "productionLine" },
};

// Set the headers
SetHeaders(new Dictionary<string, string>
{
{ "X-RapidAPI-Host", _host },
{ "X-RapidAPI-Key", _key }
});

// Make a request for the data from the API
var url = $"{_baseAddress}{parameters}";
var node = await SendRequest(url);

if (node != null)
{
try
{
// Extract the required properties from the response
properties = GetPropertyValuesFromResponse(node, definitions);

// Calculate the age of the aircraft and add it to the properties
var age = CalculateAircraftAge(properties[ApiPropertyType.AircraftRegistrationDate]);
properties.Add(ApiPropertyType.AircraftAge, age?.ToString() ?? "");

// Determine the manufacturer from the type name and model code
var manufacturer = DetermineManufacturer(properties);
properties.Add(ApiPropertyType.ManufacturerName, manufacturer);

// Log the properties dictionary
LogProperties(properties!);
}
catch (Exception ex)
{
var message = $"Error processing response: {ex.Message}";
Logger.LogMessage(Severity.Error, message);
Logger.LogException(ex);
properties = null;
}
}

return properties;
}

/// <summary>
/// Calculate the age of an aircraft from its registration date
/// </summary>
/// <param name="yearOfRegistration"></param>
/// <returns></returns>
private static int? CalculateAircraftAge(string yearOfRegistration)
{
int? age = null;

try
{
// Convert the registration date string to a date then calculate the number of years between then and now
var registered = DateTime.ParseExact(yearOfRegistration, DateFormat, null);

Check warning on line 136 in src/FlightRecorder.BusinessLogic/Api/AeroDataBox/AeroDataBoxAircraftApi.cs

View workflow job for this annotation

GitHub Actions / build

Use a format provider when parsing date and time.
age = (int)Math.Round((DateTime.Now - registered).TotalDays / DaysPerYear, 0, MidpointRounding.AwayFromZero);

Check warning on line 137 in src/FlightRecorder.BusinessLogic/Api/AeroDataBox/AeroDataBoxAircraftApi.cs

View workflow job for this annotation

GitHub Actions / build

Avoid using "DateTime.Now" for benchmarking or timespan calculation operations.
}
catch
{
// Malformed year of registration, so we can't return an age
}

return age;
}

/// <summary>
/// Determine the aircraft manufacturer's name given the model type name and the production line name
/// </summary>
/// <param name="properties"></param>
/// <returns></returns>
private static string DetermineManufacturer(Dictionary<ApiPropertyType, string> properties)
{
var builder = new StringBuilder();
var numbers = MyRegex();

// Get the properties of interest from the properties returned by the API
var modelTypeName = properties[ApiPropertyType.AircraftType];
var productionLine = properties[ApiPropertyType.AircraftProductionLine];

// Check the strings have some content
if (!string.IsNullOrEmpty(modelTypeName) && !string.IsNullOrEmpty(productionLine))
{
// The manufacturer can be inferred from the properties returned from the API:
//
// Type Name: Boeing 737-800
// Production Line: Boeing 737 NG
//
// It's the (trimmed) part of the two strings that are identical and (from other
// examples) don't contain numbers, which are unlikely (though not impossible) in
// a manufacturer name

// Split the two into words
var typeWords = modelTypeName.Split(' ');
var lineWords = productionLine.Split(' ');

// Use a string builder to build up a string containing only the parts where the words match
for (int i = 0; i < typeWords.Length; i++)
{
// Compare the word at the current position in the type and production line strings
if (typeWords[i].Equals(lineWords[i], StringComparison.OrdinalIgnoreCase) && !numbers.Match(typeWords[i]).Success)
{
// The same and not containing numbers, so add this word to the builder (with a preceding
// space if it's not the first word)
if (builder.Length > 0) builder.Append(' ');
builder.Append(typeWords[i]);
}
else
{
break;
}
}
}

return builder.ToString();
}

/// <summary>
/// Regular expression to find numbers
/// </summary>
/// <returns></returns>
[GeneratedRegex("[0-9]", RegexOptions.Compiled)]
private static partial Regex MyRegex();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ namespace FlightRecorder.BusinessLogic.Api.AeroDataBox
{
public class AeroDataBoxAirportsApi : ExternalApiBase, IAirportsApi
{
private const string DateFormat = "yyyy-MM-dd";

private readonly string _baseAddress;
private readonly string _host;
private readonly string _key;
Expand All @@ -27,12 +25,12 @@ public AeroDataBoxAirportsApi(

// The URL contains the protocol, host and base route (if any), but we need to extract the host name only
// to pass in the headers as the RapidAPI host, so capture the host and the full URL
Uri uri = new Uri(url);
Uri uri = new(url);
_host = uri.Host;
}

/// <summary>
/// Look up flight details given a flight number
/// Look up airport details given an airport IATA code
/// </summary>
/// <param name="code"></param>
/// <returns></returns>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public AeroDataBoxFlightsApi(

// The URL contains the protocol, host and base route (if any), but we need to extract the host name only
// to pass in the headers as the RapidAPI host, so capture the host and the full URL
Uri uri = new Uri(url);
Uri uri = new(url);
_host = uri.Host;
}

Expand Down
47 changes: 47 additions & 0 deletions src/FlightRecorder.BusinessLogic/Config/ApiKeyResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.IO;

namespace FlightRecorder.BusinessLogic.Config
{
public static class ApiKeyResolver
{
/// <summary>
/// Resolve an API key given the value from the configuration file
/// </summary>
/// <param name="configValue"></param>
/// <returns></returns>
public static string ResolveApiKey(string configValue)
{
string apiKey;

// If the value from the configuration file is a valid file path, the keys are
// stored separately. This separation allows the API keys not to be published
// as part of the API Docker container image but read from a volume mount
if (File.Exists(configValue))
{
apiKey = File.ReadAllText(configValue);
}
else
{
// Not a path to a file, so just return the configuration value as the key
apiKey = configValue;
}

return apiKey;
}

/// <summary>
/// Resolve all the API key definitions in the supplied application settings
/// </summary>
/// <param name="settings"></param>
public static void ResolveAllApiKeys(FlightRecorderApplicationSettings settings)
{

// Iterate over the service API key definitions
foreach (var service in settings.ApiServiceKeys)
{
// Resolve the key for the current service
service.Key = ResolveApiKey(service.Key);
}
}
}
}
28 changes: 28 additions & 0 deletions src/FlightRecorder.BusinessLogic/Config/ConfigReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using FlightRecorder.Entities.Interfaces;
using Microsoft.Extensions.Configuration;

namespace FlightRecorder.BusinessLogic.Config
{
public abstract class ConfigReader<T> : IConfigReader<T> where T : class
{
/// <summary>
/// Load and return the application settings from the named JSON-format application settings file
/// </summary>
/// <param name="jsonFileName"></param>
/// <param name="sectionName"></param>
/// <returns></returns>
public virtual T Read(string jsonFileName, string sectionName)
{
// Set up the configuration reader
IConfiguration configuration = new ConfigurationBuilder()
.AddJsonFile(jsonFileName)
.Build();

// Read the application settings section
IConfigurationSection section = configuration.GetSection(sectionName);
var settings = section.Get<T>();

return settings;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using FlightRecorder.Entities.Config;
using FlightRecorder.Entities.Logging;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

namespace FlightRecorder.BusinessLogic.Config
{
[ExcludeFromCodeCoverage]
public class FlightRecorderApplicationSettings
{
public string Secret { get; set; }
public int TokenLifespanMinutes { get; set; }
public string LogFile { get; set; }
public Severity MinimumLogLevel { get; set; }
public string SightingsExportPath { get; set; }
public string AirportsExportPath { get; set; }
public string ReportsExportPath { get; set; }
public List<ApiEndpoint> ApiEndpoints { get; set; }
public List<ApiServiceKey> ApiServiceKeys { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using FlightRecorder.Entities.Interfaces;

namespace FlightRecorder.BusinessLogic.Config
{
public class FlightRecorderConfigReader : ConfigReader<FlightRecorderApplicationSettings>, IConfigReader<FlightRecorderApplicationSettings>
{
/// <summary>
/// Load and return the application settings from the named JSON-format application settings file
/// </summary>
/// <param name="jsonFileName"></param>
/// <param name="sectionName"></param>
/// <returns></returns>
public override FlightRecorderApplicationSettings Read(string jsonFileName, string sectionName)
{
// Read the settings
var settings = base.Read(jsonFileName, sectionName);
if (settings != null)
{
// Resolve all the API keys for services where the key is held in a separate file
ApiKeyResolver.ResolveAllApiKeys(settings);
}

return settings;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,8 @@
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
</ItemGroup>
</Project>
Loading
Loading