Skip to content

Commit

Permalink
Merge pull request #22 from davewalker5/FR-94-External-Flight-Lookup-Api
Browse files Browse the repository at this point in the history
Added AeroDataBox flight lookup
  • Loading branch information
davewalker5 committed Nov 4, 2023
2 parents d212d60 + 8a1abb2 commit c55acd5
Show file tree
Hide file tree
Showing 33 changed files with 2,197 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using FlightRecorder.Entities.Api;
using FlightRecorder.Entities.Interfaces;
using FlightRecorder.Entities.Logging;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace FlightRecorder.BusinessLogic.Api.AeroDataBox
{
public class AeroDataBoxFlightsApi : ExternalApiBase, IFlightsApi
{
private const string DateFormat = "yyyy-MM-dd";

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

public AeroDataBoxFlightsApi(
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 Uri(url);
_host = uri.Host;
}

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

/// <summary>
/// Look up flight details given a flight number and date
/// </summary>
/// <param name="number"></param>
/// <param name="date"></param>
/// <returns></returns>
public async Task<Dictionary<ApiPropertyType, string>> LookupFlightByNumberAndDate(string number, DateTime date)
{
Logger.LogMessage(Severity.Info, $"Looking up flight {number} on {date}");
var parameters = $"{number}/{date.ToString(DateFormat)}";
var properties = await MakeApiRequest(parameters);
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;

// Definte the properties to be
List<ApiPropertyDefinition> definitions = new List<ApiPropertyDefinition>
{
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.DepartureAirportIATA, JsonPath = "departure.airport.iata" },
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.DestinationAirportIATA, JsonPath = "arrival.airport.iata" },
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.AirlineName, JsonPath = "airline.name" }
};

// 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![0], definitions);

// 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;
}
}
}
143 changes: 143 additions & 0 deletions src/FlightRecorder.BusinessLogic/Api/ExternalApiBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
using FlightRecorder.Entities.Api;
using FlightRecorder.Entities.Interfaces;
using FlightRecorder.Entities.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Nodes;
using System.Threading.Tasks;

namespace FlightRecorder.BusinessLogic.Api
{
public abstract class ExternalApiBase
{
private readonly IFlightRecorderHttpClient _client;
protected IFlightRecorderLogger Logger { get; private set; }

protected ExternalApiBase(IFlightRecorderLogger logger, IFlightRecorderHttpClient client)
{
Logger = logger;
_client = client;
}

/// <summary>
/// Set the request headers
/// </summary>
/// <param name="headers"></param>
protected virtual void SetHeaders(Dictionary<string, string> headers)
=> _client.SetHeaders(headers);

/// <summary>
/// Make a request to the specified URL and return the response properties as a JSON DOM
/// </summary>
/// <param name="endpoint"></param>
/// <returns></returns>
protected virtual async Task<JsonNode> SendRequest(string endpoint)
{
JsonNode node = null;

try
{
// Make a request for the data from the API
using (var response = await _client.GetAsync(endpoint))
{
// Check the request was successful
if (response.IsSuccessStatusCode)
{
// Read the response, parse to a JSON DOM
var json = await response.Content.ReadAsStringAsync();
node = JsonNode.Parse(json);
}
}
}
catch (Exception ex)
{
var message = $"Error calling {endpoint}: {ex.Message}";
Logger.LogMessage(Severity.Error, message);
Logger.LogException(ex);
node = null;
}

return node;
}

/// <summary>
/// Given a JSON node and the path to an element, return the value at that element
/// </summary>
/// <param name="node"></param>
/// <param name="path"></param>
/// <returns></returns>
private static string GetPropertyValueByPath(JsonNode node, ApiPropertyDefinition definition)
{
string value = null;
var current = node;

// Walk the JSON document to the requested element
foreach (var element in definition.JsonPath.Split(".", StringSplitOptions.RemoveEmptyEntries))
{
current = current?[element];
}

// Check the element is a type that can yield a value
if (current is JsonValue)
{
// Extract the value as a string and if "cleanup" has been specified perform it
value = current?.GetValue<string>();

Check warning on line 85 in src/FlightRecorder.BusinessLogic/Api/ExternalApiBase.cs

View workflow job for this annotation

GitHub Actions / build

Change this expression which always evaluates to the same result.

Check warning on line 85 in src/FlightRecorder.BusinessLogic/Api/ExternalApiBase.cs

View workflow job for this annotation

GitHub Actions / build

Change this expression which always evaluates to the same result.
}

return value;
}

/// <summary>
///
/// </summary>
/// <param name="node"></param>
/// <param name="propertyDefinitions"></param>
protected virtual Dictionary<ApiPropertyType, string> GetPropertyValuesFromResponse(JsonNode node, IEnumerable<ApiPropertyDefinition> propertyDefinitions)
{
var properties = new Dictionary<ApiPropertyType, string>();

// Iterate over the property definitions
foreach (var definition in propertyDefinitions)
{
// Get the value from
var value = GetPropertyValueByPath(node, definition);
properties.Add(definition.PropertyType, value ?? "");
}

// Log the properties dictionary
LogProperties(properties!);

return properties;
}

/// <summary>
/// Log the content of a properties dictionary resulting from an external API call
/// </summary>
/// <param name="properties"></param>
[ExcludeFromCodeCoverage]
protected void LogProperties(Dictionary<ApiPropertyType, string> properties)
{
// Check the properties dictionary isn't NULL
if (properties != null)
{
// Not a NULL dictionary, so iterate over all the properties it contains
foreach (var property in properties)
{
// Construct a message containing the property name and the value, replacing
// null values with "NULL"
var value = property.Value != null ? property.Value.ToString() : "NULL";
var message = $"API property {property.Key.ToString()} = {value}";

// Log the message for this property
Logger.LogMessage(Severity.Info, message);
}
}
else
{
// Log the fact that the properties dictionary is NULL
Logger.LogMessage(Severity.Warning, "API lookup generated a NULL properties dictionary");
}
}
}
}
61 changes: 61 additions & 0 deletions src/FlightRecorder.BusinessLogic/Api/FlightRecorderHttpClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using FlightRecorder.Entities.Interfaces;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Threading.Tasks;

namespace FlightRecorder.BusinessLogic.Api
{
[ExcludeFromCodeCoverage]
public class FlightRecorderHttpClient : IFlightRecorderHttpClient
{
private readonly static HttpClient _client = new();
private static FlightRecorderHttpClient? _instance = null;

Check warning on line 13 in src/FlightRecorder.BusinessLogic/Api/FlightRecorderHttpClient.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 13 in src/FlightRecorder.BusinessLogic/Api/FlightRecorderHttpClient.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 13 in src/FlightRecorder.BusinessLogic/Api/FlightRecorderHttpClient.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 13 in src/FlightRecorder.BusinessLogic/Api/FlightRecorderHttpClient.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 13 in src/FlightRecorder.BusinessLogic/Api/FlightRecorderHttpClient.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 13 in src/FlightRecorder.BusinessLogic/Api/FlightRecorderHttpClient.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
private readonly static object _lock = new();

private FlightRecorderHttpClient() { }

/// <summary>
/// Return the singleton instance of the client
/// </summary>
public static FlightRecorderHttpClient Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new FlightRecorderHttpClient();
}
}
}

return _instance;
}
}

/// <summary>
/// Set the request headers
/// </summary>
/// <param name="headers"></param>
public void SetHeaders(Dictionary<string, string> headers)
{
_client.DefaultRequestHeaders.Clear();
foreach (var header in headers)
{
_client.DefaultRequestHeaders.Add(header.Key, header.Value);
}
}

/// <summary>
/// Send a GET request to the specified URI and return the response
/// </summary>
/// <param name="uri"></param>
/// <returns></returns>
public async Task<HttpResponseMessage> GetAsync(string uri)
=> await _client.GetAsync(uri);
}
}
Loading

0 comments on commit c55acd5

Please sign in to comment.