Skip to content

Commit

Permalink
Merge pull request #23 from davewalker5/FR-99-External-Aircraft-Looku…
Browse files Browse the repository at this point in the history
…p-API

FR-99 AeroDataBox API Integration
  • Loading branch information
davewalker5 committed Nov 5, 2023
2 parents 6b2d787 + e123e54 commit 7a2ff21
Show file tree
Hide file tree
Showing 32 changed files with 1,065 additions and 157 deletions.
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.

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.

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

0 comments on commit 7a2ff21

Please sign in to comment.