Skip to content
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
5 changes: 5 additions & 0 deletions SimpleModule.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@
<Project Path="modules/AuditLogs/src/SimpleModule.AuditLogs/SimpleModule.AuditLogs.csproj" />
<Project Path="modules/AuditLogs/tests/SimpleModule.AuditLogs.Tests/SimpleModule.AuditLogs.Tests.csproj" />
</Folder>
<Folder Name="/modules/Marketplace/">
<Project Path="modules/Marketplace/src/SimpleModule.Marketplace.Contracts/SimpleModule.Marketplace.Contracts.csproj" />
<Project Path="modules/Marketplace/src/SimpleModule.Marketplace/SimpleModule.Marketplace.csproj" />
<Project Path="modules/Marketplace/tests/SimpleModule.Marketplace.Tests/SimpleModule.Marketplace.Tests.csproj" />
</Folder>
<Folder Name="/template/">
<Project Path="template/SimpleModule.Host/SimpleModule.Host.csproj" />
</Folder>
Expand Down
82 changes: 82 additions & 0 deletions cli/SimpleModule.Cli/Commands/Install/InstallCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Diagnostics;
using SimpleModule.Cli.Infrastructure;
using Spectre.Console;
using Spectre.Console.Cli;

namespace SimpleModule.Cli.Commands.Install;

public sealed class InstallCommand : Command<InstallSettings>
{
public override int Execute(CommandContext context, InstallSettings settings)
{
var solution = SolutionContext.Discover();
if (solution is null)
{
AnsiConsole.MarkupLine(
"[red]No .slnx file found. Run this command from inside a SimpleModule project.[/]"
);
return 1;
}

var hostCsproj = solution.ApiCsprojPath;
if (!File.Exists(hostCsproj))
{
AnsiConsole.MarkupLine(
$"[red]Host project not found at {Markup.Escape(hostCsproj)}[/]"
);
return 1;
}

AnsiConsole.MarkupLine(
$"Installing [green]{Markup.Escape(settings.PackageId)}[/] into [blue]{Markup.Escape(Path.GetFileName(hostCsproj))}[/]..."
);

var args = $"add \"{hostCsproj}\" package {settings.PackageId}";
if (!string.IsNullOrWhiteSpace(settings.Version))
{
args += $" --version {settings.Version}";
}

using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = args,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
},
};

process.Start();
var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();
process.WaitForExit();
var output = outputTask.GetAwaiter().GetResult();
var error = errorTask.GetAwaiter().GetResult();

if (process.ExitCode != 0)
{
AnsiConsole.MarkupLine("[red]Failed to install package.[/]");
if (!string.IsNullOrWhiteSpace(error))
{
AnsiConsole.MarkupLine($"[red]{Markup.Escape(error.Trim())}[/]");
}

return 1;
}

if (!string.IsNullOrWhiteSpace(output))
{
AnsiConsole.WriteLine(output.Trim());
}

AnsiConsole.MarkupLine(
$"[green]Successfully installed {Markup.Escape(settings.PackageId)}![/]"
);
AnsiConsole.MarkupLine("[dim]Run 'dotnet build' to discover the new module.[/]");

return 0;
}
}
15 changes: 15 additions & 0 deletions cli/SimpleModule.Cli/Commands/Install/InstallSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.ComponentModel;
using Spectre.Console.Cli;

namespace SimpleModule.Cli.Commands.Install;

public sealed class InstallSettings : CommandSettings
{
[CommandArgument(0, "<package-id>")]
[Description("The NuGet package ID to install (e.g. SimpleModule.MyModule)")]
public string PackageId { get; set; } = string.Empty;

[CommandOption("--version <VERSION>")]
[Description("Specific package version to install")]
public string? Version { get; set; }
}
5 changes: 5 additions & 0 deletions cli/SimpleModule.Cli/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using SimpleModule.Cli.Commands.Doctor;
using SimpleModule.Cli.Commands.Install;
using SimpleModule.Cli.Commands.New;
using Spectre.Console.Cli;

Expand All @@ -25,6 +26,10 @@
}
);

config
.AddCommand<InstallCommand>("install")
.WithDescription("Install a SimpleModule package from NuGet");

config
.AddCommand<DoctorCommand>("doctor")
.WithDescription("Validate project structure and conventions");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace SimpleModule.Marketplace.Contracts;

public interface IMarketplaceContracts
{
Task<MarketplaceSearchResult> SearchPackagesAsync(MarketplaceSearchRequest request);
Task<MarketplacePackageDetail?> GetPackageDetailsAsync(string packageId);
Task<HashSet<string>> GetInstalledPackageIdsAsync();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace SimpleModule.Marketplace.Contracts;

public enum MarketplaceCategory
{
All,
Auth,
Storage,
UI,
Analytics,
Integration,
Communication,
Monitoring,
Other,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using SimpleModule.Core;

namespace SimpleModule.Marketplace.Contracts;

[Dto]
public class MarketplacePackage
{
public string Id { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Authors { get; set; } = string.Empty;
public string Icon { get; set; } = string.Empty;
public long TotalDownloads { get; set; }
public List<string> Tags { get; set; } = [];
public string LatestVersion { get; set; } = string.Empty;
public string ProjectLink { get; set; } = string.Empty;
public MarketplaceCategory Category { get; set; }
public bool IsInstalled { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using SimpleModule.Core;

namespace SimpleModule.Marketplace.Contracts;

[Dto]
public class MarketplacePackageDetail : MarketplacePackage
{
public string LicenseLink { get; set; } = string.Empty;
public List<MarketplacePackageVersion> Versions { get; set; } = [];
public List<string> Dependencies { get; set; } = [];
}

[Dto]
public class MarketplacePackageVersion
{
public string Version { get; set; } = string.Empty;
public long Downloads { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace SimpleModule.Marketplace.Contracts;

public class MarketplaceSearchRequest
{
public string? Query { get; set; }
public MarketplaceCategory? Category { get; set; }
public MarketplaceSortOption SortBy { get; set; } = MarketplaceSortOption.Relevance;
public int Skip { get; set; }
public int Take { get; set; } = 20;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using SimpleModule.Core;

namespace SimpleModule.Marketplace.Contracts;

[Dto]
public class MarketplaceSearchResult
{
public int TotalHits { get; set; }
public List<MarketplacePackage> Packages { get; set; } = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace SimpleModule.Marketplace.Contracts;

public enum MarketplaceSortOption
{
Relevance,
Downloads,
RecentlyUpdated,
Alphabetical,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\framework\SimpleModule.Core\SimpleModule.Core.csproj" />
</ItemGroup>
</Project>
53 changes: 53 additions & 0 deletions modules/Marketplace/src/SimpleModule.Marketplace/CategoryMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using SimpleModule.Marketplace.Contracts;

namespace SimpleModule.Marketplace;

public static class CategoryMapper
{
private static readonly Dictionary<string, MarketplaceCategory> TagCategoryMap =
new(StringComparer.OrdinalIgnoreCase)
{
["auth"] = MarketplaceCategory.Auth,
["authentication"] = MarketplaceCategory.Auth,
["authorization"] = MarketplaceCategory.Auth,
["identity"] = MarketplaceCategory.Auth,
["openiddict"] = MarketplaceCategory.Auth,
["oauth"] = MarketplaceCategory.Auth,
["storage"] = MarketplaceCategory.Storage,
["blob"] = MarketplaceCategory.Storage,
["file-storage"] = MarketplaceCategory.Storage,
["s3"] = MarketplaceCategory.Storage,
["azure-storage"] = MarketplaceCategory.Storage,
["ui"] = MarketplaceCategory.UI,
["components"] = MarketplaceCategory.UI,
["blazor"] = MarketplaceCategory.UI,
["react"] = MarketplaceCategory.UI,
["analytics"] = MarketplaceCategory.Analytics,
["reporting"] = MarketplaceCategory.Analytics,
["dashboard"] = MarketplaceCategory.Analytics,
["integration"] = MarketplaceCategory.Integration,
["api"] = MarketplaceCategory.Integration,
["webhook"] = MarketplaceCategory.Integration,
["email"] = MarketplaceCategory.Communication,
["sms"] = MarketplaceCategory.Communication,
["notification"] = MarketplaceCategory.Communication,
["messaging"] = MarketplaceCategory.Communication,
["monitoring"] = MarketplaceCategory.Monitoring,
["logging"] = MarketplaceCategory.Monitoring,
["health-check"] = MarketplaceCategory.Monitoring,
["audit"] = MarketplaceCategory.Monitoring,
};

public static MarketplaceCategory MapCategory(IEnumerable<string> tags)
{
foreach (var tag in tags)
{
if (TagCategoryMap.TryGetValue(tag, out var category))
{
return category;
}
}

return MarketplaceCategory.Other;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using SimpleModule.Core;
using SimpleModule.Core.Endpoints;
using SimpleModule.Marketplace.Contracts;

namespace SimpleModule.Marketplace.Endpoints.Marketplace;

public class GetByIdEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder app) =>
app.MapGet(
"/{id}",
(IMarketplaceContracts marketplace, string id) =>
CrudEndpoints.GetById(() => marketplace.GetPackageDetailsAsync(id))
)
.AllowAnonymous();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using SimpleModule.Core;
using SimpleModule.Marketplace.Contracts;

namespace SimpleModule.Marketplace.Endpoints.Marketplace;

public class SearchEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder app) =>
app.MapGet(
"/",
(
IMarketplaceContracts marketplace,
string? q,
MarketplaceCategory? category,
MarketplaceSortOption? sort,
int? skip,
int? take
) =>
marketplace.SearchPackagesAsync(
new MarketplaceSearchRequest
{
Query = q,
Category = category,
SortBy = sort ?? MarketplaceSortOption.Relevance,
Skip = skip ?? 0,
Take = take ?? 20,
}
)
)
.AllowAnonymous();
}
Loading