Skip to content

Commit

Permalink
feat(fdc3):Load AppDirectory from web or FDC3 AppD API
Browse files Browse the repository at this point in the history
  • Loading branch information
BalassaMarton committed Sep 21, 2023
1 parent 7087a6d commit 87fb045
Show file tree
Hide file tree
Showing 8 changed files with 303 additions and 116 deletions.
4 changes: 3 additions & 1 deletion src/fdc3/dotnet/AppDirectory/AppDirectory.sln
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{62C29BA4-0D3
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MorganStanley.ComposeUI.Testing", "..\..\..\shared\dotnet\MorganStanley.ComposeUI.Testing\MorganStanley.ComposeUI.Testing.csproj", "{08AEA9CD-1DA8-4A73-BB10-90B0EF0EE8DC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{976748F5-4871-48F4-8AE0-99ABFB733D29}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -37,7 +39,7 @@ Global
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{4B8CA047-2023-4E1A-BD73-85F7EB1D56B3} = {9E52D00C-AABB-4579-8724-6CC7049F021E}
{EAA7B5A8-FD08-4E2F-809B-6CBD489714C1} = {9E52D00C-AABB-4579-8724-6CC7049F021E}
{EAA7B5A8-FD08-4E2F-809B-6CBD489714C1} = {976748F5-4871-48F4-8AE0-99ABFB733D29}
{08AEA9CD-1DA8-4A73-BB10-90B0EF0EE8DC} = {62C29BA4-0D3B-4764-A898-38F29FF5C36C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
Expand Down
92 changes: 71 additions & 21 deletions src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ public class AppDirectory : IAppDirectory
{
public AppDirectory(
IOptions<AppDirectoryOptions> options,
IHttpClientFactory? httpClientFactory = null,
IMemoryCache? cache = null,
IFileSystem? fileSystem = null,
ILogger<AppDirectory>? logger = null)
{
_httpClientFactory = httpClientFactory;
_options = options.Value;
_cache = cache ?? new MemoryCache(new MemoryCacheOptions());
_fileSystem = fileSystem ?? new FileSystem();
Expand All @@ -51,6 +53,8 @@ public async Task<IEnumerable<Fdc3App>> GetApps()
return app;
}

private readonly IHttpClientFactory? _httpClientFactory;
private HttpClient? _httpClient;
private readonly IFileSystem _fileSystem;
private readonly ILogger<AppDirectory> _logger;
private readonly AppDirectoryOptions _options;
Expand All @@ -65,7 +69,14 @@ private Task<Dictionary<string, Fdc3App>> GetAppsCore()
async entry =>
{
var result = await LoadApps();
entry.ExpirationTokens.Add(result.ChangeToken);
if (result.ChangeToken != null)
{
entry.ExpirationTokens.Add(result.ChangeToken);
}
else
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(_options.CacheExpirationInSeconds);
}

// Assuming that appIds are case-insensitive (not specified by the standard)
return result.Apps.ToDictionary(
Expand All @@ -74,39 +85,82 @@ private Task<Dictionary<string, Fdc3App>> GetAppsCore()
});
}

private Task<(IEnumerable<Fdc3App> Apps, IChangeToken ChangeToken)> LoadApps()
private Task<(IEnumerable<Fdc3App> Apps, IChangeToken? ChangeToken)> LoadApps()
{
if (_fileSystem.File.Exists(_options.Source?.LocalPath))
if (_options.Source != null)
{
return LoadAppsFromFile(_options.Source.LocalPath);
if (_options.Source.IsFile && _fileSystem.File.Exists(_options.Source.LocalPath))
{
return LoadAppsFromFile(_options.Source.LocalPath);
}

if (_options.Source.Scheme == Uri.UriSchemeHttp || _options.Source.Scheme == Uri.UriSchemeHttps)
{
return LoadAppsFromHttp(_options.Source.ToString());
}
}

if (!string.IsNullOrEmpty(_options.HttpClientName))
{
return LoadAppsFromHttp("apps/");
}
// TODO: add provider for online static files and FDC3 AppD API

_logger.LogWarning("The configured source is empty or not supported");
_logger.LogError("The configured source is empty or not supported");

Check warning on line 108 in src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs

View check run for this annotation

Codecov / codecov/patch

src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs#L108

Added line #L108 was not covered by tests

return Task.FromResult<(IEnumerable<Fdc3App>, IChangeToken)>(
return Task.FromResult<(IEnumerable<Fdc3App>, IChangeToken?)>(

Check warning on line 110 in src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs

View check run for this annotation

Codecov / codecov/patch

src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs#L110

Added line #L110 was not covered by tests
(
Enumerable.Empty<Fdc3App>(),
NullChangeToken.Singleton));
null));

Check warning on line 113 in src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs

View check run for this annotation

Codecov / codecov/patch

src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs#L113

Added line #L113 was not covered by tests
}

private Task<(IEnumerable<Fdc3App>, IChangeToken)> LoadAppsFromFile(string fileName)
private Task<(IEnumerable<Fdc3App>, IChangeToken?)> LoadAppsFromFile(string fileName)
{
using var stream = _fileSystem.File.OpenRead(fileName);

return Task.FromResult<(IEnumerable<Fdc3App>, IChangeToken)>(
return Task.FromResult<(IEnumerable<Fdc3App>, IChangeToken?)>(
(
LoadAppsFromStream(stream),
new FileSystemChangeToken(fileName, _fileSystem)));
}

private async Task<(IEnumerable<Fdc3App>, IChangeToken?)> LoadAppsFromHttp(string relativeUri)
{
var httpClient = GetHttpClient();
var response = await httpClient.GetAsync(relativeUri);
var stream = await response.Content.ReadAsStreamAsync();

return (LoadAppsFromStream(stream), null);
}

private HttpClient GetHttpClient()
{
if (_httpClient != null)
return _httpClient;

Check warning on line 138 in src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs

View check run for this annotation

Codecov / codecov/patch

src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs#L138

Added line #L138 was not covered by tests

if (!string.IsNullOrEmpty(_options.HttpClientName))
{
// Let configuration changes propagate. The factory manages the lifetime of underlying resources.
return _httpClientFactory?.CreateClient(_options.HttpClientName)
?? throw new InvalidOperationException(
$"{nameof(AppDirectoryOptions)} is configured with {nameof(AppDirectoryOptions.HttpClientName)}, but a suitable {nameof(IHttpClientFactory)} was not provided");
}

if (_httpClientFactory != null)
return _httpClientFactory.CreateClient();

return _httpClient = new HttpClient();

Check warning on line 151 in src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs

View check run for this annotation

Codecov / codecov/patch

src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs#L151

Added line #L151 was not covered by tests
}

private static IEnumerable<Fdc3App> LoadAppsFromStream(Stream stream)
{
var serializer = JsonSerializer.Create(new Fdc3JsonSerializerSettings());
using var textReader = new StreamReader(stream, leaveOpen: true);
using var jsonReader = new JsonTextReader(textReader);
jsonReader.Read();

return serializer.Deserialize<IEnumerable<Fdc3App>>(jsonReader) ?? Enumerable.Empty<Fdc3App>();
return jsonReader.TokenType == JsonToken.StartArray
? serializer.Deserialize<IEnumerable<Fdc3App>>(jsonReader) ?? Enumerable.Empty<Fdc3App>()
: serializer.Deserialize<GetAppsJsonResponse>(jsonReader)?.Applications ?? Enumerable.Empty<Fdc3App>();
}

private sealed class FileSystemChangeToken : IChangeToken
Expand Down Expand Up @@ -158,16 +212,12 @@ public IDisposable RegisterChangeCallback(Action<object> callback, object state)
private readonly ConcurrentDictionary<object, Action> _callbacks = new();
}

private sealed class NullChangeToken : IChangeToken
/// <summary>
/// Wrapper type for the /v2/apps response
/// </summary>
private sealed class GetAppsJsonResponse
{
public bool HasChanged => false;
public bool ActiveChangeCallbacks => true;

public IDisposable RegisterChangeCallback(Action<object> callback, object state)
{
return Disposable.Empty;
}

public static readonly NullChangeToken Singleton = new();
[JsonProperty("applications")]
public IEnumerable<Fdc3App>? Applications { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
<PackageReference Include="MorganStanley.Fdc3.AppDirectory" Version="2.0.0-alpha.6" />
<PackageReference Include="MorganStanley.Fdc3.NewtonsoftJson" Version="2.0.0-alpha.6" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,33 @@ public sealed class AppDirectoryOptions : IOptions<AppDirectoryOptions>
/// <remarks>
/// Supported schemes are <c>file</c>, <c>http</c> and <c>https</c>.
/// The static file must contain a single array of <see cref="Fdc3App" /> objects,
/// using the schema defined by the FDC3 AppDirectory API specification
/// (https://fdc3.finos.org/schemas/2.0/app-directory.html#tag/Application/paths/~1v2~1apps~1%7BappId%7D/get)
/// or the response object defined in the FDC3 AppDirectory REST API specification
/// for getting all apps(https://fdc3.finos.org/schemas/2.0/app-directory.html#tag/Application/paths/~1v2~1apps/get).
/// UTF8 encoding is assumed unless a byte order mark or encoding header is present.
/// </remarks>
public Uri? Source { get; set; }

// TODO: Implement fetching from AppD REST API
///// <summary>
///// Gets or sets the name of the <see cref="HttpClient"/> that is used to fetch
///// the application definitions from a REST API defined by the FDC3 AppDirectory API specification (https://fdc3.finos.org/schemas/2.0/app-directory.html)
///// </summary>
///// <remarks>
///// This property is only used when <see cref="Source"/> is set to <c>null</c>.
///// When this property is set, an <see cref="IHttpClientFactory"/> instance must be provided as a constructor parameter
///// for <see cref="AppDirectory"/>, with a matching HTTP client configured.
///// </remarks>
//public string? HttpClientName { get; set; }
/// <summary>
/// Gets or sets the name of the <see cref="HttpClient" /> that is used to fetch
/// the application definitions from a web URL or a REST API defined by the FDC3 AppDirectory API specification
/// (https://fdc3.finos.org/schemas/2.0/app-directory.html)
/// </summary>
/// <remarks>
/// When this property is set, an <see cref="IHttpClientFactory" /> instance must be provided as a constructor
/// parameter for <see cref="AppDirectory" />, with a matching HTTP client configured.
/// </remarks>
public string? HttpClientName { get; set; }

/// <summary>
/// Gets or sets the cache expiration time in seconds. This value is ignored when loading the apps from a local file.
/// </summary>
public int CacheExpirationInSeconds { get; set; } = (int)DefaultCacheExpiration.TotalSeconds;

/// <inheritdoc />
public AppDirectoryOptions Value => this;

/// <summary>
/// Gets the default cache expiration time
/// </summary>
public static readonly TimeSpan DefaultCacheExpiration = TimeSpan.FromHours(1);
}
Loading

0 comments on commit 87fb045

Please sign in to comment.