Skip to content

Commit

Permalink
Fdc3 App Directory based Module Catalog (morganstanley#396)
Browse files Browse the repository at this point in the history
* Change IModuleCatalog methods to async

* feat(fdc3): Added FDC3 App Directory based Module Catalog implementation
  • Loading branch information
ZKRobi authored Nov 21, 2023
1 parent 7aff273 commit f98446f
Show file tree
Hide file tree
Showing 13 changed files with 268 additions and 22 deletions.
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
<PackageVersion Include="Moq" Version="4.20.69" />
<PackageVersion Include="MorganStanley.Fdc3.AppDirectory" Version="2.0.0-alpha.7" />
<PackageVersion Include="MorganStanley.Fdc3.NewtonsoftJson" Version="2.0.0-alpha.7" />
<PackageVersion Include="MorganStanley.Fdc3" Version="2.0.0-alpha.6" />
<PackageVersion Include="MorganStanley.Fdc3" Version="2.0.0-alpha.7" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Nito.AsyncEx.Coordination" Version="5.1.2" />
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
Expand Down
7 changes: 7 additions & 0 deletions src/fdc3/dotnet/AppDirectory/AppDirectory.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MorganStanley.ComposeUI.Tes
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{976748F5-4871-48F4-8AE0-99ABFB733D29}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MorganStanley.ComposeUI.ModuleLoader.Abstractions", "..\..\..\module-loader\dotnet\src\MorganStanley.ComposeUI.ModuleLoader.Abstractions\MorganStanley.ComposeUI.ModuleLoader.Abstractions.csproj", "{7038B36A-ADD6-4882-BFBC-ED7E4B422520}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -33,6 +35,10 @@ Global
{08AEA9CD-1DA8-4A73-BB10-90B0EF0EE8DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{08AEA9CD-1DA8-4A73-BB10-90B0EF0EE8DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{08AEA9CD-1DA8-4A73-BB10-90B0EF0EE8DC}.Release|Any CPU.Build.0 = Release|Any CPU
{7038B36A-ADD6-4882-BFBC-ED7E4B422520}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7038B36A-ADD6-4882-BFBC-ED7E4B422520}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7038B36A-ADD6-4882-BFBC-ED7E4B422520}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7038B36A-ADD6-4882-BFBC-ED7E4B422520}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -41,6 +47,7 @@ Global
{4B8CA047-2023-4E1A-BD73-85F7EB1D56B3} = {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}
{7038B36A-ADD6-4882-BFBC-ED7E4B422520} = {62C29BA4-0D3B-4764-A898-38F29FF5C36C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D2C43446-C01E-438A-8A88-554F1351D134}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@
<ItemGroup>
<Folder Include="DependencyInjection\" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\..\module-loader\dotnet\src\MorganStanley.ComposeUI.ModuleLoader.Abstractions\MorganStanley.ComposeUI.ModuleLoader.Abstractions.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

using Microsoft.Extensions.DependencyInjection.Extensions;
using MorganStanley.ComposeUI.Fdc3.AppDirectory;
using MorganStanley.ComposeUI.ModuleLoader;
using MorganStanley.Fdc3.AppDirectory;

namespace Microsoft.Extensions.DependencyInjection;
Expand All @@ -24,7 +25,7 @@ public static class ServiceCollectionAppDirectoryExtensions
public static IServiceCollection AddFdc3AppDirectory(this IServiceCollection serviceCollection)
{
serviceCollection.TryAddSingleton<IAppDirectory, AppDirectory>();

serviceCollection.TryAddSingleton<IModuleCatalog, Fdc3ModuleCatalog>();
return serviceCollection;
}

Expand All @@ -33,7 +34,7 @@ public static IServiceCollection AddFdc3AppDirectory(
Action<AppDirectoryOptions> configureOptions)
{
serviceCollection.AddFdc3AppDirectory();
serviceCollection.AddOptions().Configure(configureOptions);
serviceCollection.AddOptions().Configure(configureOptions);

return serviceCollection;
}
Expand Down
79 changes: 79 additions & 0 deletions src/fdc3/dotnet/AppDirectory/src/AppDirectory/Fdc3ModuleCatalog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Morgan Stanley makes this available to you under the Apache License,
* Version 2.0 (the "License"). You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0.
*
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership. Unless required by applicable law or agreed
* to in writing, software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions
* and limitations under the License.
*/

using MorganStanley.ComposeUI.ModuleLoader;
using MorganStanley.Fdc3.AppDirectory;

namespace MorganStanley.ComposeUI.Fdc3.AppDirectory;

public sealed class Fdc3ModuleCatalog : IModuleCatalog
{
private readonly IAppDirectory _appDirectory;

public Fdc3ModuleCatalog(IAppDirectory fdc3AppDirectory)
{
_appDirectory = fdc3AppDirectory;
}

public async Task<IModuleManifest> GetManifest(string moduleId)
{
var app = await _appDirectory.GetApp(moduleId);

switch (app.Type)
{
case AppType.Web:
return new Fdc3WebModuleManifest(app);

default:
throw new NotSupportedException($"Unsupported module type: {Enum.GetName(app.Type)}");
}
}

public async Task<IEnumerable<string>> GetModuleIds()
{
var apps = await _appDirectory.GetApps();
return apps.Select(x => x.AppId);
}

private class Fdc3WebModuleManifest : IModuleManifest<WebManifestDetails>
{
public Fdc3WebModuleManifest(Fdc3App app)
{
if (app.Type != AppType.Web)
{
throw new ArgumentException("The provided app is not a web app.", nameof(app));
}

Id = app.AppId;
Name = app.Name;

var iconSrc = app.Icons?.FirstOrDefault()?.Src;
var url = new Uri(((WebAppDetails) app.Details).Url, UriKind.Absolute);

Details = new WebManifestDetails
{
Url = url,
IconUrl = iconSrc != null ? new Uri(iconSrc, UriKind.Absolute) : null
};
}

public WebManifestDetails Details { get; init; }

public string Id { get; init; }

public string Name { get; init; }

public string ModuleType => ModuleLoader.ModuleType.Web;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Morgan Stanley makes this available to you under the Apache License,
* Version 2.0 (the "License"). You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0.
*
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership. Unless required by applicable law or agreed
* to in writing, software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions
* and limitations under the License.
*/

using MorganStanley.ComposeUI.ModuleLoader;

namespace MorganStanley.ComposeUI.Fdc3.AppDirectory;

public partial class Fdc3ModuleCatalogTests
{
private readonly IModuleCatalog _catalog;

public Fdc3ModuleCatalogTests()
{
var fileSystem = TestUtils.SetUpFileSystemWithSingleFile(
path: "/apps.json",
contents: """
[
{
"appId": "app1",
"name": "App",
"type": "web",
"icons": [ {
"src": "https://example.com/app1/icon.png",
"size": "256x256",
"type": "image/png"
},
{
"src": "https://example.com/app1/icon_small.png",
"size": "64x64",
"type": "image/png"
}],
"details": { "url": "https://example.com/app1" }
},
{
"appId": "app2",
"name": "AppWithoutIcon",
"type": "web",
"details": { "url": "https://example.com/app2" }
}
]
""");

var appDirectory = new AppDirectory(
new AppDirectoryOptions { Source = new Uri("file:///apps.json") },
fileSystem: fileSystem);

_catalog = new Fdc3ModuleCatalog(appDirectory);
}

[Fact]
public async Task GetManifest_returns_web_module_manifest_by_appId()
{
const string appId = "app1";
const string appName = "App";
Uri appUri = new Uri("https://example.com/app1", UriKind.Absolute);
Uri iconUri = new Uri("https://example.com/app1/icon.png", UriKind.Absolute);
var manifest = await _catalog.GetManifest(appId);
manifest.Should().NotBeNull();
manifest.Id.Should().Be(appId);
manifest.ModuleType.Should().Be(ModuleType.Web);
manifest.Name.Should().Be(appName);
manifest.TryGetDetails<WebManifestDetails>(out var details).Should().BeTrue();
details.Should().NotBeNull();
details.Url.Should().Be(appUri);
details.IconUrl.Should().Be(iconUri);
}

[Fact]
public async Task GetManifest_without_icon_returns_web_module_manifest_by_appId()
{
const string appId = "app2";
const string appName = "AppWithoutIcon";
Uri appUri = new Uri("https://example.com/app2", UriKind.Absolute);
var manifest = await _catalog.GetManifest(appId);
manifest.Should().NotBeNull();
manifest.Id.Should().Be(appId);
manifest.ModuleType.Should().Be(ModuleType.Web);
manifest.Name.Should().Be(appName);
manifest.TryGetDetails<WebManifestDetails>(out var details).Should().BeTrue();
details.Should().NotBeNull();
details.Url.Should().Be(appUri);
details.IconUrl.Should().BeNull();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Morgan Stanley makes this available to you under the Apache License,
* Version 2.0 (the "License"). You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0.
*
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership. Unless required by applicable law or agreed
* to in writing, software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions
* and limitations under the License.
*/

using MorganStanley.Fdc3.AppDirectory;

namespace MorganStanley.ComposeUI.Fdc3.AppDirectory;

public partial class Fdc3ModuleCatalogTests
{
[Fact]
public async Task GetModuleIds_returns_available_appIds()
{
var moduleIds = await _catalog.GetModuleIds();

moduleIds.Should().HaveCount(2).And.Contain(new[] { "app1", "app2" });
}

[Fact]
public async Task GetModuleIds_returns_empty_collection_on_empty_directory()
{
var directory = new Mock<IAppDirectory>();
directory.Setup(x => x.GetApps()).Returns(Task.FromResult(Enumerable.Empty<Fdc3App>()));

var catalog = new Fdc3ModuleCatalog(directory.Object);

var moduleIds = await catalog.GetModuleIds();
moduleIds.Should().NotBeNull().And.BeEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ public interface IModuleCatalog
/// </summary>
/// <param name="moduleId"></param>
/// <returns></returns>
IModuleManifest GetManifest(string moduleId);
Task<IModuleManifest> GetManifest(string moduleId);

/// <summary>
/// Gets the IDs of the modules in the catalog.
/// </summary>
/// <returns></returns>
IEnumerable<string> GetModuleIds();
Task<IEnumerable<string>> GetModuleIds();
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public ModuleLoader(

public async Task<IModuleInstance> StartModule(StartRequest request)
{
var manifest = _moduleCatalog.GetManifest(request.ModuleId);
var manifest = await _moduleCatalog.GetManifest(request.ModuleId);
if (manifest == null)
{
throw new Exception($"Unknown Module id: {request.ModuleId}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public void GivenNullArguments_WhenCtor_ThrowsArgumentNullException()
public void GivenUnknownModuleId_WhenStart_ThrowsException()
{
var moduleCatalogMock = new Mock<IModuleCatalog>();
moduleCatalogMock.Setup(c => c.GetManifest(It.IsAny<string>())).Returns((IModuleManifest?) null!);
moduleCatalogMock.Setup(c => c.GetManifest(It.IsAny<string>())).Returns(Task.FromResult<IModuleManifest?>(null));

var moduleLoader = new ModuleLoader(moduleCatalogMock.Object, Enumerable.Empty<IModuleRunner>(), Enumerable.Empty<IStartupAction>());
Assert.ThrowsAsync<Exception>(async () => await moduleLoader.StartModule(new StartRequest("invalid")));
Expand All @@ -41,7 +41,7 @@ public void WhenNoModuleRunnerAvailable_WhenStart_ThrowsException()
var moduleManifestMock = new Mock<IModuleManifest>();
moduleManifestMock.Setup(m => m.ModuleType).Returns("test");
var moduleCatalogMock = new Mock<IModuleCatalog>();
moduleCatalogMock.Setup(c => c.GetManifest(It.IsAny<string>())).Returns(moduleManifestMock.Object);
moduleCatalogMock.Setup(c => c.GetManifest(It.IsAny<string>())).Returns(Task.FromResult<IModuleManifest>(moduleManifestMock.Object));
var testModuleRunnerMock = new Mock<IModuleRunner>();
testModuleRunnerMock.Setup(r => r.ModuleType).Returns("other");

Expand All @@ -66,7 +66,7 @@ public async Task StartModule_EndToEndTest()

moduleManifestMock.Setup(m => m.ModuleType).Returns(ModuleType.Web);
moduleManifestMock.Setup(m => m.Details).Returns(webManifestDetails);
moduleCatalogMock.Setup(c => c.GetManifest(moduleId)).Returns(moduleManifestMock.Object);
moduleCatalogMock.Setup(c => c.GetManifest(moduleId)).Returns(Task.FromResult<IModuleManifest>(moduleManifestMock.Object));
startupActionMock.Setup(s => s.InvokeAsync(It.IsAny<StartupContext>(), It.IsAny<Func<Task>>()))
.Callback<StartupContext, Func<Task>>((startupContext, next) =>
{
Expand Down
3 changes: 2 additions & 1 deletion src/shell/dotnet/Shell/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ See the License for the specific language governing permissions and limitations
Height="450"
Width="600"
Background="{DynamicResource {x:Static SystemColors.AppWorkspaceBrushKey}}"
WindowStartupLocation="CenterScreen">
WindowStartupLocation="CenterScreen"
Initialized="RibbonWindow_Initialized">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
Expand Down
Loading

0 comments on commit f98446f

Please sign in to comment.